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

@ -1,28 +1,31 @@
{
"name" : "mall-app-t",
"appid" : "__UNI__B201544",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"name": "mall-app-t",
"appid": "__UNI__B201544",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"compatible": {
"ignoreVersion": true
},
/* */
"modules" : {},
"modules": {},
/* */
"distribute" : {
"distribute": {
/* android */
"android" : {
"permissions" : [
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
@ -41,32 +44,32 @@
]
},
/* ios */
"ios" : {},
"ios": {},
/* SDK */
"sdkConfigs" : {}
"sdkConfigs": {}
}
},
/* */
"quickapp" : {},
"quickapp": {},
/* */
"mp-weixin" : {
"appid" : "wx64387dc8bba916ec",
"setting" : {
"urlCheck" : false
"mp-weixin": {
"appid": "wx64387dc8bba916ec",
"setting": {
"urlCheck": false
},
"usingComponents" : true
"usingComponents": true
},
"mp-alipay" : {
"usingComponents" : true
"mp-alipay": {
"usingComponents": true
},
"mp-baidu" : {
"usingComponents" : true
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao" : {
"usingComponents" : true
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics" : {
"enable" : false
"uniStatistics": {
"enable": false
},
"vueVersion" : "3"
"vueVersion": "3"
}

View File

@ -1,21 +1,43 @@
<template>
<pb-layout 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 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"
@click="peach.$router.go('/pages/product/manageGoods', { id: item.id, mark: 'detail' })" />
</view>
</view>
<pb-layout
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 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"
@click="peach.$router.go('/pages/product/manageGoods', { id: item.id, mark: 'detail' })"
/>
</view>
</view>
<uni-load-more v-if="state.pagination.total > 0" :status="state.loadStatus" :content-text="{
contentdown: '上拉加载更多',
}" @click="loadMore" />
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多',
}"
@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" />
</pb-layout>
<p-empty
v-if="state.pagination.total === 0"
icon="/static/soldout-empty.png"
text="暂无产品"
bgColor="transparent"
/>
</pb-layout>
</template>
<script setup>
@ -27,167 +49,172 @@ import _ from 'lodash'
import { resetPagination } from '@/peach/utils'
const bgStyle = {
backgroundImage: '',
backgroundColor: 'var(--ui-BG-1)',
description: '',
backgroundImage: '',
backgroundColor: 'var(--ui-BG-1)',
description: '',
}
const state = ref({
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 6,
name: '',
createTime: [],
},
loadStatus: '',
pagination: {
list: [],
total: 0,
pageNo: 1,
pageSize: 6,
name: '',
createTime: [],
},
loadStatus: '',
})
function emptyList() {
resetPagination(state.value.pagination)
resetPagination(state.value.pagination)
}
function onSearch() {
emptyList()
getList()
emptyList()
getList()
}
async function getList() {
let { data } = await GoodApi.getProductList({
pageNo: state.value.pagination.pageNo,
pageSize: state.value.pagination.pageSize,
})
let { data } = await GoodApi.getProductList({
pageNo: state.value.pagination.pageNo,
pageSize: state.value.pagination.pageSize,
})
state.value.pagination.list = _.concat(state.value.pagination.list, data.list)
state.value.pagination.total = data.total
let currentPageTotal = state.value.pagination.length
state.value.pagination.list = _.concat(state.value.pagination.list, data.list)
state.value.pagination.total = data.total
let currentPageTotal = state.value.pagination.length
state.value.loadStatus = currentPageTotal < state.value.pagination.total ? 'more' : 'noMore'
state.value.loadStatus = currentPageTotal < state.value.pagination.total ? 'more' : 'noMore'
}
function addGoods() {
peach.$router.go('/pages/product/manageGoods', {
title: '添加商品'
})
peach.$store('trade').$patch({
selectedProperty: null,
goodsInfo: null,
skus: null,
})
peach.$router.go('/pages/product/manageGoods', {
title: '添加商品',
})
}
function loadMore() {
if (state.value.loadStatus === 'noMore') {
return
}
state.value.pagination.pageNo++
getList()
if (state.value.loadStatus === 'noMore') {
return
}
state.value.pagination.pageNo++
getList()
}
onLoad(async (options) => {
getList()
getList()
})
onReachBottom(() => {
loadMore()
loadMore()
})
</script>
<style lang="scss" scoped>
.product-list {
.add-product {
position: fixed;
color: var(--ui-BG-Main);
bottom: 70px;
right: 20px;
font-size: 80rpx;
}
.goods-list-box {
width: 50%;
box-sizing: border-box;
.left-list {
margin-right: 10rpx;
margin-bottom: 20rpx;
.add-product {
position: fixed;
color: var(--ui-BG-Main);
bottom: 70px;
right: 20px;
font-size: 80rpx;
}
.right-list {
margin-left: 10rpx;
margin-bottom: 20rpx;
}
}
.goods-list-box {
width: 50%;
box-sizing: border-box;
.goods-box {
&:nth-last-of-type(1) {
margin-bottom: 0 !important;
.left-list {
margin-right: 10rpx;
margin-bottom: 20rpx;
}
.right-list {
margin-left: 10rpx;
margin-bottom: 20rpx;
}
}
&:nth-child(2n) {
margin-right: 0;
}
}
.goods-box {
&:nth-last-of-type(1) {
margin-bottom: 0 !important;
}
.list-icon {
width: 80rpx;
.sicon-goods-card {
font-size: 40rpx;
&:nth-child(2n) {
margin-right: 0;
}
}
.sicon-goods-list {
font-size: 40rpx;
}
}
.list-icon {
width: 80rpx;
.goods-card {
margin-left: 20rpx;
}
.sicon-goods-card {
font-size: 40rpx;
}
.list-filter-tabs {
background-color: #fff;
}
.filter-list-box {
padding: 28rpx 52rpx;
.filter-item {
font-size: 28rpx;
font-weight: 500;
color: #333333;
line-height: normal;
margin-bottom: 24rpx;
&:nth-last-child(1) {
margin-bottom: 0;
}
.sicon-goods-list {
font-size: 40rpx;
}
}
.filter-item-active {
color: var(--ui-BG-Main);
}
}
.tab-item {
height: 50px;
position: relative;
z-index: 11;
.tab-title {
font-size: 30rpx;
.goods-card {
margin-left: 20rpx;
}
.cur-tab-title {
font-weight: $font-weight-bold;
.list-filter-tabs {
background-color: #fff;
}
.tab-line {
width: 60rpx;
height: 6rpx;
border-radius: 6rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 10rpx;
background-color: var(--ui-BG-Main);
z-index: 12;
.filter-list-box {
padding: 28rpx 52rpx;
.filter-item {
font-size: 28rpx;
font-weight: 500;
color: #333333;
line-height: normal;
margin-bottom: 24rpx;
&:nth-last-child(1) {
margin-bottom: 0;
}
}
.filter-item-active {
color: var(--ui-BG-Main);
}
}
.tab-item {
height: 50px;
position: relative;
z-index: 11;
.tab-title {
font-size: 30rpx;
}
.cur-tab-title {
font-weight: $font-weight-bold;
}
.tab-line {
width: 60rpx;
height: 6rpx;
border-radius: 6rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 10rpx;
background-color: var(--ui-BG-Main);
z-index: 12;
}
}
}
}
</style>

View File

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

View File

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

View File

@ -8,3 +8,27 @@ export const SPEC_TYPE = [
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,179 +1,277 @@
import { ref, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import peach from "@/peach";
import GoodsApi from "@/peach/api/trade/goods";
import { SPEC_TYPE } from "./config";
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import peach from '@/peach'
import GoodsApi from '@/peach/api/trade/goods'
import { SPEC_TYPE, SKU_RULE_CONFIG } from './config'
const pickerRef = ref(null);
const pickerRef = ref(null)
// 多属性商品 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({
specType: true,
specText: SPEC_TYPE[0].label,
});
specType: true,
specText: SPEC_TYPE[0].label,
})
async function showPropertyList() {
await getGoodsProperty();
propertyListRef.value.onOpen();
await getGoodsProperty()
propertyListRef.value.onOpen()
}
function onRDPickerConfirm(e) {
peach.$store("trade").specType = SPEC_TYPE[e.value[0]].value;
formData.value.specText = SPEC_TYPE[e.value[0]].label;
formData.value.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
// 如果商品规格不一致,则需要重新初始化 sku 列表
initSku()
peach.$store('trade').specType = SPEC_TYPE[e.value[0]].value
}
function pickerProperty() {
if (canEdit.value) {
let index = specType.value ? 1 : 0;
pickerRef.value.onOpen([index]);
}
if (canEdit.value) {
let index = formData.value.specType ? 1 : 0
console.log(index)
pickerRef.value.onOpen([index])
}
}
async function onPropertyConfirm(e) {
await getGoodsProperty();
console.log(e);
await getGoodsProperty()
console.log(e)
}
function onConfirm() {
console.log(skus.value);
console.log(skus.value)
}
async function getGoodsProperty() {
let { data } = await GoodsApi.getHistoryProperty();
let { data } = await GoodsApi.getHistoryProperty()
// 把 propertyList 中 id 相同的属性合并,并去重
propertyList.value = peach.$store("trade").selectedProperty;
// 把 propertyList 中 id 相同的属性合并,并去重
propertyList.value = peach.$store('trade').selectedProperty
console.log(propertyList.value);
console.log(propertyList.value)
// 根据已经选择数据,设置默认选中
data.forEach((item) => {
// 判断属性是否已经选中
let propertyParent = propertyList.value.find(
(sitem) => sitem?.id === item.id
);
// 根据已经选择数据,设置默认选中
data.forEach((item) => {
// 判断属性是否已经选中
let propertyParent = propertyList.value.find((sitem) => sitem?.id === item.id)
item.checked = propertyParent ? true : false;
item.checked = propertyParent ? true : false
// 如果属性已经选中,查询子类中是否有选中
if (item.checked) {
item.propertyValues.forEach((child) => {
let childResult = propertyParent?.children.some(
(schild) => schild === child.id
);
child.checked = childResult ? true : false;
});
}
});
// 如果属性已经选中,查询子类中是否有选中
if (item.checked) {
item.propertyValues.forEach((child) => {
let childResult = propertyParent?.children.some((schild) => schild === child.id)
child.checked = childResult ? true : false
})
}
})
goodsPropertyList.value = data;
console.log(goodsPropertyList.value);
goodsPropertyList.value = data
console.log(goodsPropertyList.value)
}
function changeSubProperty() {
// 修改子属性状态,需要同步更新 skus 的显示
console.log(goodsPropertyList.value);
// 过滤父属性 checked 选项,深拷贝避免后面循环改变元数据内容
let temp = JSON.parse(
JSON.stringify(goodsPropertyList.value.filter((item) => item.checked))
);
temp.forEach((item) => {
item.propertyValues = item.propertyValues.filter((child) => child.checked);
});
// 修改子属性状态,需要同步更新 skus 的显示
console.log(goodsPropertyList.value)
// 过滤父属性 checked 选项,深拷贝避免后面循环改变元数据内容
let temp = JSON.parse(JSON.stringify(goodsPropertyList.value.filter((item) => item.checked)))
temp.forEach((item) => {
item.propertyValues = item.propertyValues.filter((child) => child.checked)
})
let result = temp.map((item) => {
return item.propertyValues.map((child) => ({
propertyId: item.id,
propertyName: item.name,
valueId: child.id,
valueName: child.name,
}));
});
let result = temp.map((item) => {
return item.propertyValues.map((child) => ({
propertyId: item.id,
propertyName: item.name,
valueId: child.id,
valueName: child.name,
}))
})
let tempSkus = [];
let tempSkus = []
for (let item of reduceArr(result)) {
let obj = {
picUrl: "",
barCode: "",
price: 0,
marketPrice: 0,
costPrice: 0,
stock: 0,
weight: 0,
volume: 0,
properties: item,
};
tempSkus.push(obj);
}
for (let item of reduceArr(result)) {
let obj = {
picUrl: '',
barCode: '',
price: 0,
marketPrice: 0,
costPrice: 0,
stock: 0,
weight: 0,
volume: 0,
properties: item,
}
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) {
return arr.reduce((acc, cur) => {
let tempAcc = [];
return arr.reduce((acc, cur) => {
let tempAcc = []
if (acc.length < 1) {
cur.forEach((item, index) => {
if (tempAcc[index]) {
tempAcc[index].push(item);
if (acc.length < 1) {
cur.forEach((item, index) => {
if (tempAcc[index]) {
tempAcc[index].push(item)
} else {
tempAcc[index] = [item]
}
})
} else {
tempAcc[index] = [item];
}
});
} else {
acc.forEach((item, index) => {
cur.forEach((sitem, sindex) => {
tempAcc.push([...item, sitem]);
});
});
acc.forEach((item, index) => {
cur.forEach((sitem, sindex) => {
tempAcc.push([...item, sitem])
})
})
if (cur.length < 1) {
tempAcc = acc;
}
}
return tempAcc;
}, []);
if (cur.length < 1) {
tempAcc = acc
}
}
return tempAcc
}, [])
}
const specType = computed(() => peach.$store("trade").goodsInfo.specType);
const specType = computed(() => peach.$store('trade').goodsInfo?.specType || false)
function initial() {
onLoad(() => {
formData.value.specType = specType.value ? true : false;
formData.value.specText = SPEC_TYPE[specType.value ? 1 : 0].label;
skus.value = peach.$store("trade").skus;
if (specType.value) {
getGoodsProperty();
}
});
onLoad(() => {
formData.value.specType = specType.value ? true : false
formData.value.specText = SPEC_TYPE[specType.value ? 1 : 0].label
skus.value = JSON.parse(JSON.stringify(peach.$store('trade').skus))
// 如果新增商品 sku并且是单规格初始化 sku
if (!skus.value) {
initSku()
}
if (specType.value) {
getGoodsProperty()
}
})
}
export {
initial,
canEdit,
skus,
pickerRef,
pickerProperty,
onConfirm,
propertyListRef,
formData,
onRDPickerConfirm,
onPropertyConfirm,
propertyList,
showPropertyList,
goodsPropertyList,
changeSubProperty,
};
initial,
canEdit,
skus,
pickerRef,
pickerProperty,
validateSku,
onConfirm,
submitProperty,
propertyListRef,
formData,
onRDPickerConfirm,
onPropertyConfirm,
propertyList,
showPropertyList,
goodsPropertyList,
changeSubProperty,
}

View File

@ -1,187 +1,285 @@
<template>
<pb-layout class="manage-goods" :title="goodsTitle" leftIcon="leftIcon" navbar="normal" :bgStyle="bgStyle"
opacityBgUi="bg-white" color="black">
<view class="goods-form">
<uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160">
<uni-forms-item label="商品封面图" name="picUrl" required>
<p-uploader v-model:url="formData.picUrl" :readonly="!canEdit" fileMediatype="image" limit="1" mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }" />
</uni-forms-item>
<uni-forms-item label="商品轮播图" name="sliderPicUrls" required>
<p-uploader v-model:url="formData.sliderPicUrls" :readonly="!canEdit" fileMediatype="image" limit="6"
mode="grid" :imageStyles="{ width: '168rpx', height: '168rpx' }" />
</uni-forms-item>
<uni-forms-item label="商品名称" name="name" required>
<uni-easyinput type="text" trim="all" v-model="formData.name" :disabled="!canEdit" placeholder="请输入商品名称" />
</uni-forms-item>
<uni-forms-item label="商品分类" @tap="openPicker('category', 'multiple')" name="categoryId" label-position="left"
required>
<uni-easyinput type="text" v-model="formData.categoryText" :disabled="!canEdit" :styles="selfStyles"
placeholderStyle="color:#8a8a8a" :clearable="false" :inputBorder="false" placeholder="请选择商品分类" disabled>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品品牌" name="brandId" label-position="left" required @tap="openPicker('brand', 'single')">
<uni-easyinput type="text" v-model="formData.brandText" :disabled="!canEdit" :styles="selfStyles"
placeholderStyle="color:#8a8a8a" :clearable="false" :inputBorder="false" placeholder="请选择商品品牌" disabled>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品规格" name="skus" required label-position="left">
<view class="btn-group">
<button class="ss-reset-button ss-set-property" @tap="clickSetProperty">规格设置</button>
</view>
</uni-forms-item>
<uni-forms-item label="商品关键词" name="keyword" required>
<uni-easyinput type="text" v-model="formData.keyword" :disabled="!canEdit" placeholder="请输入商品关键词" />
</uni-forms-item>
<uni-forms-item label="商品简介" name="introduction" required>
<uni-easyinput type="textarea" :disabled="!canEdit" trim="all" autoHeight v-model="formData.introduction"
placeholder="请输入商品简介" />
</uni-forms-item>
<uni-forms-item label="物流设置" @tap="openPicker('delivery', 'single')" 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>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<view @tap="onSubmit" v-if="canEdit">
<button class="ss-reset-button submit-button ui-Shadow-Main">提交</button>
</view>
</view>
<pb-layout
class="manage-goods"
:title="goodsTitle"
leftIcon="leftIcon"
navbar="normal"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<view class="goods-form">
<uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160">
<uni-forms-item label="商品封面图" name="picUrl" required>
<p-uploader
v-model:url="formData.picUrl"
:readonly="!canEdit"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item>
<uni-forms-item label="商品轮播图" name="sliderPicUrls" required>
<p-uploader
v-model:url="formData.sliderPicUrls"
:readonly="!canEdit"
fileMediatype="image"
limit="6"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item>
<uni-forms-item label="商品名称" name="name" required>
<uni-easyinput
type="text"
trim="all"
v-model="formData.name"
:disabled="!canEdit"
placeholder="请输入商品名称"
/>
</uni-forms-item>
<uni-forms-item
label="商品分类"
@tap="openPicker('category', 'multiple')"
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>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item
label="商品品牌"
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>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品规格" name="skus" required label-position="left">
<view class="btn-group">
<button class="ss-reset-button ss-set-property" @tap="clickSetProperty">规格设置</button>
</view>
</uni-forms-item>
<uni-forms-item label="商品关键词" name="keyword" required>
<uni-easyinput
type="text"
v-model="formData.keyword"
:disabled="!canEdit"
placeholder="请输入商品关键词"
/>
</uni-forms-item>
<uni-forms-item label="商品简介" name="introduction" required>
<uni-easyinput
type="textarea"
:disabled="!canEdit"
trim="all"
autoHeight
v-model="formData.introduction"
placeholder="请输入商品简介"
/>
</uni-forms-item>
<uni-forms-item
label="物流设置"
@tap="openPicker('delivery', 'single')"
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>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item label="商品详情">
<piaoyiEditor
:values="richValues"
:maxlength="3000"
:readOnly="richReadOnly"
:photoUrl="photoUrl"
:api="richApi"
/>
</uni-forms-item>
</uni-forms>
<view @tap="onSubmit" v-if="canEdit">
<button class="ss-reset-button submit-button ui-Shadow-Main">提交</button>
</view>
</view>
<p-picker ref="pickerRef" :mode="pickerMode" :options-cols="optionsCols" @confirm="onRDPickerConfirm"></p-picker>
</pb-layout>
<p-picker
ref="pickerRef"
:mode="pickerMode"
:options-cols="optionsCols"
@confirm="onRDPickerConfirm"
></p-picker>
</pb-layout>
</template>
<script setup>
import { ref, computed } from 'vue'
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 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 = {
backgroundImage: '',
backgroundColor: '#fff',
description: '',
backgroundImage: '',
backgroundColor: '#fff',
description: '',
}
const DELIVERY_TYPES = [
{
value: 3,
label: '到店核销',
},
{
value: 4,
label: '商家配送',
},
{
value: 3,
label: '到店核销',
},
{
value: 4,
label: '商家配送',
},
]
const selfStyles = {
backgroundColor: '#f9f9f9',
backgroundColor: '#f9f9f9',
}
const pickerRef = ref()
const richValues = ref('')
const photoUrl = baseUrl + apiPath
const richApi = '/infra/file/upload'
const richReadOnly = ref(false)
const formData = ref({
picUrl: 'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
sliderPicUrls: [
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
],
name: '测试商品',
categoryId: [],
categoryText: '',
brandId: '',
keyword: '香酥鸭,但家',
deliveryTypes: [],
deliveryText: '',
introduction: '但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭',
picUrl: 'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
sliderPicUrls: [
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
],
name: '测试商品',
categoryId: [],
categoryText: '',
brandId: '',
keyword: '香酥鸭,但家',
deliveryTypes: [],
deliveryText: '',
introduction: '但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭但家贵阳香酥鸭现榨香酥鸭无任何添加剂香酥鸭',
})
const rules = {
name: {
rules: [
{
required: true,
errorMessage: '请输入商品名称',
},
],
},
picUrl: {
rules: [
{
required: true,
errorMessage: '请上传商品封面图',
},
],
},
sliderPicUrls: {
rules: [
{
required: true,
errorMessage: '请上传商品轮播图',
},
],
},
categoryId: {
rules: [
{
required: true,
errorMessage: '请选择商品分类',
},
],
},
brandId: {
rules: [
{
required: true,
errorMessage: '请选择商品品牌',
},
],
},
skus: {
rules: [
{
required: true,
errorMessage: '请选择商品规格',
},
],
},
keyword: {
rules: [
{
required: true,
errorMessage: '请输入商品关键字',
},
],
},
introduction: {
rules: [
{
required: true,
errorMessage: '请输入商品简介',
},
],
},
deliveryTypes: {
rules: [
{
required: true,
errorMessage: '请选择商品物流',
},
],
},
name: {
rules: [
{
required: true,
errorMessage: '请输入商品名称',
},
],
},
picUrl: {
rules: [
{
required: true,
errorMessage: '请上传商品封面图',
},
],
},
sliderPicUrls: {
rules: [
{
required: true,
errorMessage: '请上传商品轮播图',
},
],
},
categoryId: {
rules: [
{
required: true,
errorMessage: '请选择商品分类',
},
],
},
brandId: {
rules: [
{
required: true,
errorMessage: '请选择商品品牌',
},
],
},
skus: {
rules: [
{
required: true,
errorMessage: '请选择商品规格',
},
],
},
keyword: {
rules: [
{
required: true,
errorMessage: '请输入商品关键字',
},
],
},
introduction: {
rules: [
{
required: true,
errorMessage: '请输入商品简介',
},
],
},
deliveryTypes: {
rules: [
{
required: true,
errorMessage: '请选择商品物流',
},
],
},
}
const formRef = ref(null)
@ -194,223 +292,254 @@ const optionsCols = ref([])
const canEdit = computed(() => peach.$store('trade').canEdit)
function openPicker(mark, mode) {
if (!canEdit.value) return
pickerMode.value = mode
popMark.value = mark
if (mark === 'delivery') {
optionsCols.value = DELIVERY_TYPES
pickerRef.value.onOpen([0])
} else if (mark === 'category') {
optionsCols.value = categoryList.value
pickerRef.value.onOpen([0, 0])
} else if (mark === 'brand') {
optionsCols.value = brandList.value
pickerRef.value.onOpen([0])
}
if (!canEdit.value) return
pickerMode.value = mode
popMark.value = mark
if (mark === 'delivery') {
optionsCols.value = DELIVERY_TYPES
pickerRef.value.onOpen([0])
} else if (mark === 'category') {
optionsCols.value = categoryList.value
pickerRef.value.onOpen([0, 0])
} else if (mark === 'brand') {
optionsCols.value = brandList.value
pickerRef.value.onOpen([0])
}
}
function onRDPickerConfirm(e) {
if (popMark.value === 'delivery') {
formData.value.deliveryTypes = []
formData.value.deliveryText = DELIVERY_TYPES[e.value[0]].label
formData.value.deliveryTypes.push(DELIVERY_TYPES[e.value[0]].value)
}
if (popMark.value === 'delivery') {
formData.value.deliveryTypes = []
formData.value.deliveryText = DELIVERY_TYPES[e.value[0]].label
formData.value.deliveryTypes.push(DELIVERY_TYPES[e.value[0]].value)
}
if (popMark.value === 'category') {
formData.value.categoryId = categoryList.value[e.value[0]].children[e.value[1]].id
formData.value.categoryText =
categoryList.value[e.value[0]].name + '/' + categoryList.value[e.value[0]].children[e.value[1]].name
}
if (popMark.value === 'category') {
formData.value.categoryId = categoryList.value[e.value[0]].children[e.value[1]].id
formData.value.categoryText =
categoryList.value[e.value[0]].name + '/' + categoryList.value[e.value[0]].children[e.value[1]].name
}
if (popMark.value === 'brand') {
formData.value.brandId = brandList.value[e.value[0]].id
}
if (popMark.value === 'brand') {
formData.value.brandId = brandList.value[e.value[0]].id
formData.value.brandText = brandList.value[e.value[0]].name
}
}
function clickSetProperty() {
//
let temp = formData.value.skus.map((item) => {
return item.properties.map((sitem) => ({
id: sitem.propertyId,
children: [sitem.valueId],
}));
})
.flat(1);
if (formData.value.skus) {
//
let temp = formData.value.skus
.map((item) => {
return item.properties.map((sitem) => ({
id: sitem.propertyId,
children: [sitem.valueId],
}))
})
.flat(1)
//
let result = temp.reduce((pre, cur) => {
let index = pre.findIndex((item) => item.id === cur.id);
if (index !== -1) {
pre[index].children.push(...new Set(cur.children));
} else {
pre.push(cur);
//
let result = temp.reduce((pre, cur) => {
let index = pre.findIndex((item) => item.id === cur.id)
if (index !== -1) {
pre[index].children.push(...new Set(cur.children))
} else {
pre.push(cur)
}
return pre
}, [])
peach.$store('trade').$patch({
selectedProperty: result,
})
}
return pre;
}, []);
peach.$store('trade').$patch({
selectedProperty: result,
goodsInfo: formData.value,
skus: formData.value.skus
})
peach.$router.go('/pages/product/sku')
peach.$router.go('/pages/product/sku')
}
function getProduct(id) {
GoodsApi.getProduct({ id }).then((res) => {
formData.value = res.data
GoodsApi.getProduct({ id }).then((res) => {
formData.value = res.data
// categoryList formData.value.categoryId
let tempCategory = categoryList.value.find((item) => {
return item.children.find((child) => {
return child.id === formData.value.categoryId
})
richValues.value = res.data.description
// categoryList formData.value.categoryId
let tempCategory = categoryList.value.find((item) => {
return item.children.find((child) => {
return child.id === formData.value.categoryId
})
})
formData.value.categoryText =
tempCategory.name +
'/' +
tempCategory.children.find((item) => {
return item.id === formData.value.categoryId
}).name
// brandList, formData.value.brandId
let tempBrand = brandList.value.find((item) => {
return item.id === formData.value.brandId
})
formData.value.brandText = tempBrand.name
// DELIVERY_TYPES formData.value.deliveryTypes
formData.value.deliveryText = DELIVERY_TYPES.find((item) => {
return item.value === formData.value.deliveryTypes[0]
}).label
peach.$store('trade').$patch({
goodsInfo: formData.value,
skus: formData.value.skus,
})
})
formData.value.categoryText =
tempCategory.name +
'/' +
tempCategory.children.find((item) => {
return item.id === formData.value.categoryId
}).name
// brandList, formData.value.brandId
let tempBrand = brandList.value.find((item) => {
return item.id === formData.value.brandId
})
formData.value.brandText = tempBrand.name
// DELIVERY_TYPES formData.value.deliveryTypes
formData.value.deliveryText = DELIVERY_TYPES.find((item) => {
return item.value === formData.value.deliveryTypes[0]
}).label
})
}
function onSubmit() {
console.log('res', formData.value)
console.log('res', formData.value)
console.log('richtext', richValues.value)
formRef.value
.validate()
.then(async (res) => {
let tempObj = { ...res }
formData.value.description = richValues.value
formData.value.skus = peach.$store('trade').skus
// if (formData.value.id) {
// tempObj.id = formData.value.id
// await GoodsApi.editProduct(tempObj)
// } else {
// await GoodsApi.addProduct(tempObj)
// }
})
.catch((err) => {
console.log('err', err)
})
formRef.value
.validate()
.then(async (res) => {
// skus
validateSku()
let tempObj = _.cloneDeep(formData.value)
tempObj.skus.forEach((item) => {
item.name = formData.value.name
})
if (formData.value.id) {
tempObj.id = formData.value.id
await GoodsApi.editProduct(tempObj)
} else {
await GoodsApi.addProduct(tempObj)
}
})
.catch((err) => {
uni.showToast({
title: err[0].errorMessage,
icon: 'none',
duration: 4000,
})
console.log('err', err)
})
}
//
async function getCategoryList() {
let { data } = await GoodsApi.getGoodsCategory()
categoryList.value = handleTree(data)
let { data } = await GoodsApi.getGoodsCategory()
categoryList.value = handleTree(data)
}
//
async function getBrandList() {
let { data } = await GoodsApi.getBrand()
brandList.value = data
let { data } = await GoodsApi.getBrand()
brandList.value = data
}
onLoad(async (options) => {
await getCategoryList()
await getBrandList()
await getCategoryList()
await getBrandList()
goodsTitle.value = options.title
goodsTitle.value = options.title
if (options.id) {
getProduct(options.id)
peach.$store('trade').detailTag = options.mark
}
//
richReadOnly.value = true
/**
* todo 滚动一定距离后修改富文本状态和 canEdit 一致
*/
if (options.id) {
getProduct(options.id)
peach.$store('trade').detailTag = options.mark
}
})
</script>
<style lang="scss" scoped>
@mixin ss-set-property {
width: 80px;
height: 60rpx;
line-height: normal;
background: var(--ui-BG-Main);
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 500;
color: #fff;
width: 80px;
height: 60rpx;
line-height: normal;
background: var(--ui-BG-Main);
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 500;
color: #fff;
}
.manage-goods {
.goods-form {
margin: 40rpx;
.goods-form {
margin: 40rpx;
:deep() {
.uni-easyinput__content-input {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
:deep() {
.uni-easyinput__content-input {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
.uni-easyinput__placeholder-class {
font-size: 14px;
}
.uni-easyinput__placeholder-class {
font-size: 14px;
}
.is-direction-left {
.uni-forms-item__label {
padding-left: 10px;
background-color: #f9f9f9;
border-radius: 10px 0 0 10px;
.is-direction-left {
.uni-forms-item__label {
padding-left: 10px;
background-color: #f9f9f9;
border-radius: 10px 0 0 10px;
}
uni-icons {
margin-right: 10px;
}
.uni-easyinput__content {
border-radius: 0 10px 10px 0;
}
}
.is-direction-left {
.is-disabled {
color: #333333;
text-align: right;
}
.uni-forms-item__error {
left: -160rpx !important;
}
}
}
uni-icons {
margin-right: 10px;
.btn-group {
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #f9f9f9;
border-radius: 0 10px 10px 0;
.ss-set-property {
@include ss-set-property;
}
}
.uni-easyinput__content {
border-radius: 0 10px 10px 0;
.submit-button {
width: 100%;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
}
.is-direction-left {
.is-disabled {
color: #333333;
text-align: right;
}
.uni-forms-item__error {
left: -160rpx !important;
}
}
}
.btn-group {
height: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #f9f9f9;
border-radius: 0 10px 10px 0;
.ss-set-property {
@include ss-set-property
}
}
.submit-button {
width: 100%;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
}
}
</style>

View File

@ -1,45 +1,68 @@
<template>
<pb-layout class="goods-property" title="商品属性" leftIcon="leftIcon" navbar="normal" :bgStyle="bgStyle"
opacityBgUi="bg-white" color="black">
<view class="property">
<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-easyinput type="text" :clearable="false" :styles="selfStyles" placeholderStyle="color:#8a8a8a"
:inputBorder="false" v-model="formData.specText" placeholder="请选择商品规格" disabled>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
<template v-if="formData.specType">
<!-- 添加商品 -->
<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"
@changeSubProperty="changeSubProperty"></property-detail>
<!-- 多规格商品 -->
<mutiple-sku :skus="skus"></mutiple-sku>
</template>
<pb-layout
class="goods-property"
title="商品属性"
leftIcon="leftIcon"
navbar="normal"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<view class="property">
<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-easyinput
type="text"
:clearable="false"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:inputBorder="false"
v-model="formData.specText"
placeholder="请选择商品规格"
disabled
>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
<template v-if="formData.specType">
<!-- 添加商品 -->
<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"
@changeSubProperty="changeSubProperty"
></property-detail>
<!-- 多规格商品 -->
<mutiple-sku :skus="skus"></mutiple-sku>
</template>
<template v-else>
<!-- 单规格商品 -->
<SkuItem :skus="skus" />
</template>
<template v-else>
<!-- 单规格商品 -->
<SkuItem :skus="skus" />
</template>
<!-- 确认选择 -->
<view style="padding: 0 40rpx 40rpx;" @tap="onConfirm" v-if="canEdit">
<button class="ss-reset-button submit-button ui-Shadow-Main">提交</button>
</view>
<!-- 确认选择 -->
<view style="padding: 0 40rpx 40rpx" @tap="submitProperty" v-if="canEdit">
<button class="ss-reset-button submit-button ui-Shadow-Main">保存</button>
</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"
@confirm="onPropertyConfirm" />
</pb-layout>
<!-- 商品属性列表 -->
<PropertyList
ref="propertyListRef"
v-model="propertyList"
:goodsPropertyList="goodsPropertyList"
@confirm="onPropertyConfirm"
/>
</pb-layout>
</template>
<script setup>
@ -49,25 +72,26 @@ import PropertyList from './components/propertyList'
import PropertyDetail from './components/propertyDetail'
import { SPEC_TYPE } from './js/config'
import {
initial,
canEdit,
pickerRef,
propertyListRef,
onRDPickerConfirm,
formData,
propertyList,
onPropertyConfirm,
showPropertyList,
goodsPropertyList,
pickerProperty,
skus,
changeSubProperty,
initial,
canEdit,
pickerRef,
propertyListRef,
onRDPickerConfirm,
formData,
propertyList,
onPropertyConfirm,
showPropertyList,
goodsPropertyList,
pickerProperty,
skus,
changeSubProperty,
submitProperty,
} from './js/sku'
const bgStyle = {
backgroundImage: '',
backgroundColor: '#fff',
description: '',
backgroundImage: '',
backgroundColor: '#fff',
description: '',
}
initial()
@ -75,57 +99,57 @@ initial()
<style lang="scss" scoped>
.goods-property {
.property {
margin: 40rpx;
.property {
margin: 40rpx;
:deep() {
.uni-easyinput__content-input {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
:deep() {
.uni-easyinput__content-input {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
.uni-easyinput__placeholder-class {
font-size: 14px;
}
.uni-easyinput__placeholder-class {
font-size: 14px;
}
.is-direction-left {
.uni-forms-item__label {
padding-left: 10px;
background-color: #f9f9f9;
border-radius: 10px 0 0 10px;
.is-direction-left {
.uni-forms-item__label {
padding-left: 10px;
background-color: #f9f9f9;
border-radius: 10px 0 0 10px;
}
uni-icons {
margin-right: 10px;
}
.uni-easyinput__content {
border-radius: 0 10px 10px 0;
}
}
.is-disabled {
color: #333333;
text-align: right;
}
}
uni-icons {
margin-right: 10px;
}
.uni-easyinput__content {
border-radius: 0 10px 10px 0;
}
}
.is-disabled {
color: #333333;
text-align: right;
}
}
}
.add-property {
margin: 40rpx;
border: 1px dotted var(--ui-BG-Main);
color: var(--ui-BG-Main);
border-radius: 10px;
text-align: center;
}
.add-property {
margin: 40rpx;
border: 1px dotted var(--ui-BG-Main);
color: var(--ui-BG-Main);
border-radius: 10px;
text-align: center;
}
.submit-button {
width: 100%;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
.submit-button {
width: 100%;
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
}
</style>

View File

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

View File

@ -1,221 +1,238 @@
<template>
<view class="ss-goods-wrap">
<view v-if="size === 'lg'" class="lg-goods-card ss-flex ss-col-stretch" :style="[elStyles]" @click="onClick">
<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>
<view v-if="goodsFields.title?.show || goodsFields.name?.show" class="lg-goods-title ss-line-2"
:style="[{ color: titleColor }]">
{{ data.title || data.name }}
</view>
<view v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
class="lg-goods-subtitle ss-m-t-10 ss-line-1"
:style="[{ color: subTitleColor, background: subTitleBackground }]">
{{ data.subtitle || data.introduction }}
</view>
</view>
<view>
<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"
:style="[{ color: goodsFields.price.color }]">
<text class="ss-font-24">{{ priceUnit }}</text>
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
<view class="ss-goods-wrap">
<view v-if="size === 'lg'" class="lg-goods-card ss-flex ss-col-stretch" :style="[elStyles]" @click="onClick">
<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>
<view
v-if="goodsFields.title?.show || goodsFields.name?.show"
class="lg-goods-title ss-line-2"
:style="[{ color: titleColor }]"
>
{{ data.title || data.name }}
</view>
<view
v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
class="lg-goods-subtitle ss-m-t-10 ss-line-1"
:style="[{ color: subTitleColor, background: subTitleBackground }]"
>
{{ data.subtitle || data.introduction }}
</view>
</view>
<view>
<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"
:style="[{ color: goodsFields.price.color }]"
>
<text class="ss-font-24">{{ priceUnit }}</text>
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
</view>
<view
v-if="
(goodsFields.original_price?.show || goodsFields.marketPrice?.show) &&
(data.original_price > 0 || data.marketPrice > 0)
"
class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS"
:style="[{ color: originPriceColor }]"
>
<text class="price-unit ss-font-20">{{ priceUnit }}</text>
<view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
</view>
</view>
<view class="ss-m-t-8 ss-flex ss-col-center ss-flex-wrap">
<view class="sales-text">{{ salesAndStock }}</view>
</view>
</view>
</view>
<view v-if="
(goodsFields.original_price?.show || goodsFields.marketPrice?.show) &&
(data.original_price > 0 || data.marketPrice > 0)
" class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS" :style="[{ color: originPriceColor }]">
<text class="price-unit ss-font-20">{{ priceUnit }}</text>
<view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
</view>
</view>
<view class="ss-m-t-8 ss-flex ss-col-center ss-flex-wrap">
<view class="sales-text">{{ salesAndStock }}</view>
</view>
</view>
</view>
<view class="ss-flex ss-row-around" :style="btnStyles">
<button class="ss-reset-button btn-group" @click="clickGoods('detail')">详情</button>
<button class="ss-reset-button btn-group" @click="clickGoods('edit')">编辑</button>
<button class="ss-reset-button btn-group btn-del" @click="clickGoods('del')">删除</button>
</view>
</view>
<view class="ss-flex ss-row-around" :style="btnStyles">
<button class="ss-reset-button btn-group" @click="clickGoods('detail')">详情</button>
<button class="ss-reset-button btn-group" @click="clickGoods('edit')">编辑</button>
<button class="ss-reset-button btn-group btn-del" @click="clickGoods('del')">删除</button>
</view>
</view>
</template>
<script setup>
import peach from '@/peach'
import { ref, computed } from 'vue'
import { isArray } from 'lodash'
import peach from '@/peach'
import { fen2yuan, formatSales, formatStock } from '@/peach/hooks/useGoods'
import { unix } from 'dayjs'
const props = defineProps({
goodsFields: {
type: [Array, Object],
default() {
return {
price: { show: true },
stock: { show: true },
name: { show: true },
introduction: { show: true },
marketPrice: { show: true },
salesCount: { show: true },
}
goodsFields: {
type: [Array, Object],
default() {
return {
price: { show: true },
stock: { show: true },
name: { show: true },
introduction: { show: true },
marketPrice: { show: true },
salesCount: { show: true },
}
},
},
data: {
type: Object,
default: {},
},
size: {
type: String,
default: '',
},
originPriceColor: {
type: String,
default: '#C4C4C4',
},
topRadius: {
type: Number,
default: 0,
},
bottomRadius: {
type: Number,
default: 0,
},
priceUnit: {
type: String,
default: '¥',
},
titleColor: {
type: String,
default: '#333',
},
subTitleColor: {
type: String,
default: '#999999',
},
subTitleBackground: {
type: String,
default: '',
},
},
data: {
type: Object,
default: {},
},
size: {
type: String,
default: '',
},
originPriceColor: {
type: String,
default: '#C4C4C4',
},
topRadius: {
type: Number,
default: 0,
},
bottomRadius: {
type: Number,
default: 0,
},
priceUnit: {
type: String,
default: '¥',
},
titleColor: {
type: String,
default: '#333',
},
subTitleColor: {
type: String,
default: '#999999',
},
subTitleBackground: {
type: String,
default: '',
},
})
const emits = defineEmits(['click'])
function onClick() {
emits('click')
emits('click')
}
const elStyles = computed(() => {
return {
background: props.background,
'border-top-left-radius': props.topRadius + 'px',
'border-top-right-radius': props.topRadius + 'px',
}
return {
background: props.background,
'border-top-left-radius': props.topRadius + 'px',
'border-top-right-radius': props.topRadius + 'px',
}
})
const btnStyles = computed(() => {
return {
background: '#fff',
'border-bottom-left-radius': props.bottomRadius + 'px',
'border-bottom-right-radius': props.bottomRadius + 'px',
padding: '8px 0',
}
return {
background: '#fff',
'border-bottom-left-radius': props.bottomRadius + 'px',
'border-bottom-right-radius': props.bottomRadius + 'px',
padding: '8px 0',
}
})
//
const salesAndStock = computed(() => {
let text = []
if (props.goodsFields.salesCount?.show) {
text.push(formatSales(props.data.sales_show_type, props.data.salesCount))
}
if (props.goodsFields.stock?.show) {
text.push(formatStock(props.data.stock_show_type, props.data.stock))
}
return text.join(' | ')
let text = []
if (props.goodsFields.salesCount?.show) {
text.push(formatSales(props.data.sales_show_type, props.data.salesCount))
}
if (props.goodsFields.stock?.show) {
text.push(formatStock(props.data.stock_show_type, props.data.stock))
}
return text.join(' | ')
})
function clickGoods(mark) {
if (mark === 'detail' || mark === 'edit') {
peach.$router.go('/pages/product/manageGoods', {
id: props.data.id,
mark: mark,
title: mark === 'detail' ? '商品详情' : '编辑商品'
})
} else if (mark === 'del') {
uni.showModal({
title: '提示',
content: '是否删除该商品?',
success: (res) => {
if (res.confirm) {
}
},
})
}
if (mark === 'detail' || mark === 'edit') {
peach.$store('trade').$patch({
selectedProperty: null,
goodsInfo: null,
skus: null,
})
peach.$router.go('/pages/product/manageGoods', {
id: props.data.id,
mark: mark,
title: mark === 'detail' ? '商品详情' : '编辑商品',
})
} else if (mark === 'del') {
uni.showModal({
title: '提示',
content: '是否删除该商品?',
success: (res) => {
if (res.confirm) {
}
},
})
}
}
</script>
<style lang="scss" scoped>
.ss-goods-wrap {
.lg-goods-card {
overflow: hidden;
position: relative;
z-index: 1;
background-color: $white;
height: 280rpx;
.lg-goods-card {
overflow: hidden;
position: relative;
z-index: 1;
background-color: $white;
height: 280rpx;
.lg-img-box {
width: 280rpx;
height: 280rpx;
margin-right: 20rpx;
.lg-img-box {
width: 280rpx;
height: 280rpx;
margin-right: 20rpx;
}
.lg-goods-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
// line-height: 36rpx;
// width: 410rpx;
}
.lg-goods-subtitle {
font-size: 24rpx;
font-weight: 400;
color: #999999;
// line-height: 30rpx;
// width: 410rpx;
}
.lg-goods-price {
font-size: 30rpx;
color: $red;
line-height: 36rpx;
}
.sales-text {
display: table;
font-size: 24rpx;
transform: scale(0.8);
margin-left: 0rpx;
color: #c4c4c4;
}
}
.lg-goods-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
// line-height: 36rpx;
// width: 410rpx;
.btn-group {
width: 140rpx;
height: 55rpx;
line-height: 55rpx;
background: var(--ui-BG-1);
border-radius: 25rpx;
font-size: 24rpx;
color: #000;
}
.lg-goods-subtitle {
font-size: 24rpx;
font-weight: 400;
color: #999999;
// line-height: 30rpx;
// width: 410rpx;
.btn-del {
color: var(--ui-BG-Main);
background-color: var(--ui-BG-Main-opacity-1);
}
.lg-goods-price {
font-size: 30rpx;
color: $red;
line-height: 36rpx;
}
.sales-text {
display: table;
font-size: 24rpx;
transform: scale(0.8);
margin-left: 0rpx;
color: #c4c4c4;
}
}
.btn-group {
width: 120rpx;
height: 50rpx;
background: var(--ui-BG-1);
border-radius: 25rpx;
font-size: 24rpx;
color: #000;
}
.btn-del {
color: var(--ui-BG-Main);
background-color: var(--ui-BG-Main-opacity-1);
}
}
</style>

View File

@ -1,35 +1,47 @@
<template>
<view class="custom-picker">
<!-- 普通弹窗 -->
<uni-popup type="bottom" ref="pickerPopupRef" background-color="#fff">
<view class="popup-header">
<view class="button-cancel" @click="onClosePopup">取消</view>
<view class="button-link" @click="onConfirmPopup">确定</view>
</view>
<view class="popup-content">
<picker-view :indicator-style="indicatorStyle" :value="pickerViewValue" @change="bindChange"
class="picker-view">
<template v-if="mode === 'single'">
<picker-view-column>
<view class="item" v-for="(item, index) in props.optionsCols" :key="`${item.value}-${index}`">{{
item.label ?? item.name }}</view>
</picker-view-column>
</template>
<template v-else>
<picker-view-column>
<view class="item" v-for="(item, index) in props.optionsCols" :key="`${item.value}-${index}`">{{ item.name
}}</view>
</picker-view-column>
<picker-view-column>
<view class="item" v-for="(item, index) in childrenList" :key="`${item.value}-${index}`">{{
item.name
}}</view>
</picker-view-column>
</template>
</picker-view>
</view>
</uni-popup>
</view>
<view class="custom-picker">
<!-- 普通弹窗 -->
<uni-popup type="bottom" ref="pickerPopupRef" background-color="#fff">
<view class="popup-header">
<view class="button-cancel" @click="onClosePopup">取消</view>
<view class="button-link" @click="onConfirmPopup">确定</view>
</view>
<view class="popup-content">
<picker-view
:indicator-style="indicatorStyle"
:value="pickerViewValue"
@change="bindChange"
class="picker-view"
>
<template v-if="mode === 'single'">
<picker-view-column>
<view
class="item"
v-for="(item, index) in props.optionsCols"
:key="`${item.value}-${index}`"
>{{ item.label ?? item.name }}</view
>
</picker-view-column>
</template>
<template v-else>
<picker-view-column>
<view
class="item"
v-for="(item, index) in props.optionsCols"
:key="`${item.value}-${index}`"
>{{ item.name }}</view
>
</picker-view-column>
<picker-view-column>
<view class="item" v-for="(item, index) in childrenList" :key="`${item.value}-${index}`">{{
item.name
}}</view>
</picker-view-column>
</template>
</picker-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
@ -37,20 +49,20 @@ import { defineEmits, defineProps, ref, onMounted, computed, defineExpose } from
const pickerViewValue = ref([])
const indicatorStyle = `height: 50px`
const props = defineProps({
// pickerview value label
optionsCols: {
default: () => [],
required: true,
type: Array,
},
mode: {
type: String,
default: 'single',
},
// pickerview value label
optionsCols: {
default: () => [],
required: true,
type: Array,
},
mode: {
type: String,
default: 'single',
},
})
const childrenList = computed(() => {
return props.optionsCols[pickerViewValue.value[0]]?.children
return props.optionsCols[pickerViewValue.value[0]]?.children
})
// confirm change
@ -64,8 +76,8 @@ const pickerPopupRef = ref(null)
* @return {*}
*/
const onOpen = (defaultValue) => {
pickerViewValue.value = defaultValue
pickerPopupRef.value.open('bottom')
pickerViewValue.value = defaultValue
pickerPopupRef.value.open('bottom')
}
/**
* @author: joey wong
@ -74,17 +86,17 @@ const onOpen = (defaultValue) => {
* @return {*}
*/
const bindChange = (e) => {
pickerViewValue.value = e.detail.value
pickerViewValue.value = e.detail.value
if (props.mode === 'multiple') {
if (pickerViewValue.value[0] !== e.detail.value[0]) {
pickerViewValue.value[1] = 0
if (props.mode === 'multiple') {
if (pickerViewValue.value[0] !== e.detail.value[0]) {
pickerViewValue.value[1] = 0
}
}
}
emit('change', {
value: pickerViewValue.value,
})
emit('change', {
value: pickerViewValue.value,
})
}
/**
* @author: joey wong
@ -92,7 +104,7 @@ const bindChange = (e) => {
* @return {*}
*/
const onClosePopup = () => {
pickerPopupRef.value.close()
pickerPopupRef.value.close()
}
/**
* @author: joey wong
@ -100,61 +112,61 @@ const onClosePopup = () => {
* @return {*}
*/
const onConfirmPopup = () => {
emit('confirm', {
value: pickerViewValue.value,
})
onClosePopup()
emit('confirm', {
value: pickerViewValue.value,
})
onClosePopup()
}
defineExpose({
onOpen,
onOpen,
})
</script>
<style lang="scss" scoped>
.custom-picker {
display: flex;
align-items: center;
&-text {
margin-right: 5.7rpx;
}
&-icon {
width: 19rpx;
height: 12rpx;
margin-bottom: 3rpx;
}
.popup-content {
height: 500rpx;
.item {
text-align: center;
line-height: 34px;
}
}
.picker-view {
width: 100%;
height: 100%;
margin-top: 20rpx;
}
.button-link {
color: #1892ea;
font-size: 28rpx;
}
.button-cancel {
color: #888;
font-size: 28rpx;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 38rpx 0;
}
&-text {
margin-right: 5.7rpx;
}
&-icon {
width: 19rpx;
height: 12rpx;
margin-bottom: 3rpx;
}
.popup-content {
height: 500rpx;
.item {
text-align: center;
line-height: 50px;
}
}
.picker-view {
width: 100%;
height: 100%;
margin-top: 20rpx;
}
.button-link {
color: #1892ea;
font-size: 28rpx;
}
.button-cancel {
color: #888;
font-size: 28rpx;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 38rpx 0;
}
}
</style>

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