feat(积分)

This commit is contained in:
unknown 2024-06-03 01:09:01 +08:00
parent 87aaba008e
commit 94ee2a3d22
21 changed files with 3462 additions and 987 deletions

View File

@ -119,6 +119,45 @@
"style": {
"navigationBarTitleText": "提现"
},
"meta": {
"auth": true
}
},
{
"path": "point/buy",
"style": {
"navigationBarTitleText": "购买积分"
},
"meta": {
"auth": true
}
}, {
"path": "point/share",
"style": {
"navigationBarTitleText": "分发积分"
},
"meta": {
"auth": true
}
}, {
"path": "point/loglist",
"style": {
"navigationBarTitleText": "历史积分"
},
"meta": {
"auth": true
}
}
]
},
{
"root": "pages/product",
"pages": [
{
"path": "manageGoods",
"style": {
"navigationBarTitleText": "商品管理"
},
"meta": {
"auth": false
}

View File

@ -27,7 +27,7 @@
<view class="menu ss-m-t-70">
<view class="title ss-m-b-30">我的服务</view>
<view class="items">
<view class="item" v-for="item in state.menus" :key="item.name">
<view class="item" v-for="item in state.menus" :key="item.name" @click="navService(item)">
<image class="icon" :src="item.icon" mode="aspectFill"></image>
<view class="label">{{ item.name }}</view>
</view>
@ -56,17 +56,17 @@ const state = ref({
{
name: '余额提现',
icon: '/static/withdraw.png',
path: '',
path: '/pages/user/wallet/withdraw',
},
{
name: '购买积分',
icon: '/static/buycent.png',
path: '',
path: '/pages/user/point/buy',
},
{
name: '分发积分',
icon: '/static/dispensecent.png',
path: '',
path: '/pages/user/point/share',
},
{
name: '提现规则',
@ -86,6 +86,10 @@ const userInfo = computed(() => {
return userStore.userInfo
})
function navService(item) {
peach.$router.go(item.path)
}
function logOut() {
uni.showModal({
title: '提示',
@ -110,10 +114,12 @@ function logOut() {
height: 80rpx;
margin-right: 20rpx;
}
.detail {
.name {
color: #fff;
}
.description {
color: #fff;
opacity: 0.4;
@ -127,21 +133,25 @@ function logOut() {
opacity: 0.9;
border-radius: 26rpx;
padding: 31rpx 40rpx;
.title {
font-weight: 500;
color: var(--ui-TC);
font-size: 24rpx;
}
.remain {
.left {
font-weight: 600;
flex: 1;
color: var(--ui-BG-Main);
.unit {
position: relative;
top: 4px;
}
}
.right-btn {
width: 161rpx;
height: 63rpx;
@ -149,6 +159,7 @@ function logOut() {
color: #fff;
}
}
.cent {
color: var(--ui-TC-2);
font-size: 24rpx;
@ -159,20 +170,24 @@ function logOut() {
.title {
font-weight: 600;
}
.items {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30rpx 30rpx;
.item {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 23rpx;
.icon {
width: 94rpx;
height: 94rpx;
}
.label {
font-size: 22rpx;
color: #0c0c0cff;
@ -181,10 +196,12 @@ function logOut() {
}
}
}
.bottom {
position: absolute;
bottom: 70px;
width: calc(100% - 60rpx);
.login-btn-start {
height: 82rpx;
line-height: normal;

View File

@ -1,32 +1,19 @@
<template>
<pb-layout
title="产品"
navbar="normal"
tabbar="/pages/index/product"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<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" 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/goods/index', { id: item.id })"
/>
<p-goods-column size="lg" :data="item" :topRadius="10" bottomRadius="10"
@click="peach.$router.go('/pages/goods/index', { id: item.id })" />
</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: '上拉加载更多',
}"
@click="loadMore"
/>
}" @click="loadMore" />
<view class="_icon-add-round add-product" @click="addGoods"></view>
<p-empty v-if="state.pagination.total === 0" icon="/static/soldout-empty.png" text="暂无产品" />
</pb-layout>
</template>
@ -40,7 +27,7 @@ import { resetPagination } from '@/peach/utils'
const bgStyle = {
backgroundImage: '',
backgroundColor: '',
backgroundColor: '#fff',
description: '',
}
@ -64,6 +51,10 @@ function onSearch() {
function getList() { }
function addGoods() {
peach.$router.go('/pages/product/manageGoods')
}
function loadMore() {
if (state.value.loadStatus === 'noMore') {
return
@ -80,3 +71,15 @@ onReachBottom(() => {
loadMore()
})
</script>
<style lang="scss" scoped>
.product-list {
.add-product {
position: fixed;
color: var(--ui-BG-Main);
bottom: 70px;
right: 20px;
font-size: 80rpx;
}
}
</style>

View File

@ -1,22 +1,13 @@
<template>
<pb-layout
title="订单"
navbar="normal"
tabbar="/pages/order/list"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<pb-layout title="订单" navbar="normal" tabbar="/pages/order/list" :bgStyle="bgStyle" opacityBgUi="bg-white"
color="black">
<pb-sticky bgColor="#fff">
<pb-tabs :list="tabMaps" :scrollable="false" @change="onTabsChange" :current="state.currentTab" />
</pb-sticky>
<p-empty v-if="state.pagination.total === 0" icon="/static/order-empty.png" text="暂无订单" />
<view v-if="state.pagination.total > 0">
<view
class="bg-white order-list-card-box ss-r-10 ss-m-t-14 ss-m-20"
v-for="order in state.pagination.list"
:key="order.payOrderId"
>
<view class="bg-white order-list-card-box ss-r-10 ss-m-t-14 ss-m-20" v-for="order in state.pagination.list"
:key="order.payOrderId">
<view v-for="(sorder, sindex) in order.order" @tap="onOrderDetail(sorder.id)">
<view class="order-card-header ss-flex ss-col-center ss-row-between ss-p-x-20">
<!-- <view class="order-no">订单号{{ sorder.no }}</view> -->
@ -26,92 +17,61 @@
</view>
</view>
<view class="border-bottom" v-for="item in sorder.items" :key="item.id">
<p-goods-item
:img="item.picUrl"
:title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')"
:price="item.price"
:num="item.count"
/>
<p-goods-item :img="item.picUrl" :title="item.spuName"
:skuText="item.properties.map((property) => property.valueName).join(' ')" :price="item.price"
:num="item.count" />
</view>
<view
class="order-card-footer ss-flex ss-col-center ss-p-x-20"
:class="sorder.buttons.length > 3 ? 'ss-row-between' : 'ss-row-right'"
>
<view class="order-card-footer ss-flex ss-col-center ss-p-x-20"
:class="sorder.buttons.length > 3 ? 'ss-row-between' : 'ss-row-right'">
<view class="ss-flex ss-col-center">
<button
v-if="sorder.buttons.includes('combination')"
class="tool-btn ss-reset-button"
@tap.stop="onOrderGroupon(sorder)"
>
<button v-if="sorder.buttons.includes('combination')" class="tool-btn ss-reset-button"
@tap.stop="onOrderGroupon(sorder)">
拼团详情
</button>
<button
v-if="sorder.buttons.length === 0"
class="tool-btn ss-reset-button"
@tap.stop="onOrderDetail(sorder.id)"
>
<button v-if="sorder.buttons.length === 0" class="tool-btn ss-reset-button"
@tap.stop="onOrderDetail(sorder.id)">
查看详情
</button>
<button
v-if="sorder.buttons.includes('confirm')"
class="tool-btn ss-reset-button"
@tap.stop="onConfirm(sorder)"
>
<button v-if="sorder.buttons.includes('confirm')" class="tool-btn ss-reset-button"
@tap.stop="onConfirm(sorder)">
确认收货
</button>
<button
v-if="sorder.buttons.includes('express')"
class="tool-btn ss-reset-button"
@tap.stop="onExpress(sorder.id)"
>
<button v-if="sorder.buttons.includes('express')" class="tool-btn ss-reset-button"
@tap.stop="onExpress(sorder.id)">
查看物流
</button>
<button
v-if="sorder.buttons.includes('cancel')"
class="tool-btn ss-reset-button"
@tap.stop="onCancel(sorder.id, sindex)"
>
<button v-if="sorder.buttons.includes('cancel')" class="tool-btn ss-reset-button"
@tap.stop="onCancel(sorder.id, sindex)">
取消订单
</button>
<button
v-if="sorder.buttons.includes('comment')"
class="tool-btn ss-reset-button"
@tap.stop="onComment(sorder.id)"
>
<button v-if="sorder.buttons.includes('comment')" class="tool-btn ss-reset-button"
@tap.stop="onComment(sorder.id)">
评价
</button>
<button
v-if="sorder.buttons.includes('delete')"
class="delete-btn ss-reset-button"
@tap.stop="onDelete(sorder.id)"
>
<button v-if="sorder.buttons.includes('delete')" class="delete-btn ss-reset-button"
@tap.stop="onDelete(sorder.id)">
删除订单
</button>
</view>
</view>
<view class="pay-box ss-m-t-30 ss-flex ss-gap-40 ss-p-b-40 ss-row-right ss-p-r-20">
<view class="ss-flex ss-col-center">
<view class="discounts-title pay-color"
> {{ totalNumsPerOrder(order) }} 件商品,总金额:</view
>
<view class="discounts-title pay-color"> {{ totalNumsPerOrder(order) }} 件商品,总金额:</view>
<view class="discounts-money pay-color"> {{ fen2yuan(totalPricePerOrder(order)) }} </view>
</view>
</view>
</view>
</view>
</view>
<uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{ contentdown: '上拉加载更多' }"
/>
<uni-load-more v-if="state.pagination.total > 0" :status="state.loadStatus"
:content-text="{ contentdown: '上拉加载更多' }" />
</pb-layout>
</template>
<script setup>
import { ref } from 'vue'
import OrderApi from '@/peach/api/trade/order'
import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
import { fen2yuan, formatOrderColor, formatOrderStatus, handleOrderButtons } from '@/peach/hooks/useGoods'
import peach from '@/peach'
@ -190,28 +150,25 @@ function onOrderDetail(id) {
//
async function getOrderList() {
state.loadStatus = 'loading'
// let { code, data } = await OrderApi.getOrderPage({
// pageNo: state.pagination.pageNo,
// pageSize: state.pagination.pageSize,
// status: tabMaps[state.currentTab].value,
// })
// if (code !== 0) {
// return
// }
state.value.loadStatus = 'loading'
let { data } = await OrderApi.getOrderPage({
pageNo: state.value.pagination.pageNo,
pageSize: state.value.pagination.pageSize,
status: tabMaps[state.value.currentTab].value,
})
data.list.forEach((item) => {
item.order.forEach((sitem) => {
handleOrderButtons(sitem)
})
})
state.pagination.list = _.concat(state.pagination.list, data.list)
console.log(state.pagination.list)
state.pagination.total = data.total
let currentPageTotal = state.pagination.list.reduce((pre, cur) => {
state.value.pagination.list = _.concat(state.value.pagination.list, data.list)
console.log(state.value.pagination.list)
state.value.pagination.total = data.total
let currentPageTotal = state.value.pagination.list.reduce((pre, cur) => {
return pre + cur.order.length
}, 0)
state.loadStatus = currentPageTotal < state.pagination.total ? 'more' : 'noMore'
state.value.loadStatus = currentPageTotal < state.value.pagination.total ? 'more' : 'noMore'
}
onLoad(async (options) => {

View File

@ -0,0 +1,245 @@
<template>
<pb-layout class="manage-goods" title="发布商品" leftIcon="leftIcon" navbar="normal" tabbar="/pages/product/manageGoods"
: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" 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" 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" placeholder="请输入商品名称" />
</uni-forms-item>
<uni-forms-item label="商品分类" name="categoryId" label-position="left" required>
<uni-easyinput type="text" v-model="formData.categoryId" :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>
<uni-easyinput type="text" v-model="formData.brandId" :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" label-position="left" required>
<uni-easyinput type="text" v-model="formData.skus" :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="keyword" required>
<uni-easyinput type="text" v-model="formData.keyword" placeholder="请输入商品关键词" />
</uni-forms-item>
<uni-forms-item label="商品简介" name="introduction" required>
<uni-easyinput type="textarea" trim="all" autoHeight v-model="formData.introduction" placeholder="请输入商品简介" />
</uni-forms-item>
<uni-forms-item label="物流设置" name="deliveryTypes" label-position="left" required>
<uni-easyinput type="text" :clearable="false" :styles="selfStyles" placeholderStyle="color:#8a8a8a"
:inputBorder="false" v-model="formData.keyword" placeholder="请选择配送方式" disabled>
<template v-slot:right>
<uni-icons type="right" />
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
</pb-layout>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import peach from '@/peach';
import GoodsApi from '@/peach/api/trade/goods';
import _ from 'lodash';
const bgStyle = {
backgroundImage: '',
backgroundColor: '#fff',
description: '',
}
const selfStyles = {
backgroundColor: '#f9f9f9'
}
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: 91,
brandId: 4,
keyword: '香酥鸭,但家',
deliveryTypes: [3],
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: '请选择商品物流',
},
]
}
};
const formRef = ref(null);
function onSubmit() {
console.log('res', formData.value);
formRef.value
.validate()
.then(async (res) => {
let tempObj = { ...res };
if (formData.value.id) {
tempObj.id = formData.value.id;
await GoodsApi.editProduct(tempObj);
} else {
await GoodsApi.addProduct(tempObj);
}
})
.catch((err) => {
console.log('err', err);
});
}
//
function getGoodsInfo() {
}
onLoad((options) => {
if (options.id) {
getProduct();
}
});
</script>
<style lang="scss" scoped>
.manage-goods {
.goods-form {
margin: 40rpx;
:deep() {
.uni-easyinput__content-input {
font-size: 28rpx !important;
color: #333333 !important;
line-height: normal !important;
}
.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;
}
uni-icons {
margin-right: 10px;
}
.uni-easyinput__content {
border-radius: 0 10px 10px 0;
}
}
.is-disabled {
color: #333333;
text-align: right;
}
}
}
}
</style>

167
pages/user/point/buy.vue Normal file
View File

@ -0,0 +1,167 @@
<template>
<pb-layout navbar="inner" iconColor="#fff" class="buy-point-wrap" leftIcon="leftIcon" color="#fff" title="购买积分"
:bgStyle="bgStyle">
<view class="title ss-m-t-80">
<view class="main">购买商城积分</view>
<view class="sub ss-flex ss-row-center ss-m-t-55"><text style="color: #F3D3BF;"
class="cicon-safe"></text>订单多多·出单多多<text style="color: #F3D3BF;" class="cicon-safe"></text></view>
</view>
<view class="middle ss-m-t-150 ss-flex-col ss-row-center ss-col-center ss-gap-40">
<view class="title">积分额度选择</view>
<view class="sub">专属您的积分</view>
</view>
<view class="combo ss-m-t-90 ss-m-b-50 ss-p-x-40">
<button class="ss-reset-button draw-btn ui-Shadow-Main">购买5000积分/50</button>
<button class="ss-reset-button draw-btn-raw ui-Shadow-Main">购买5000积分/50</button>
</view>
<view class="agreement-box ss-flex ss-row-center" :class="{ shake: state.isShaking }">
<label class="radio ss-flex" @tap="onChange">
<radio :checked="state.agreeStatus" color="rgba(243, 192, 156, 1)" style="transform: scale(0.8)"
@tap.stop="onChange" />
<view class="agreement-text ss-flex ss-m-l-8">
我已阅读并遵守
<view class="tcp-text" @tap.stop="onProtocol('积分须知')"> 积分须知 </view>
</view>
</label>
</view>
</pb-layout>
</template>
<script setup>
import { ref } from 'vue'
const state = ref({
isShaking: false,
agreeStatus: false,
})
const bgStyle = {
backgroundImage: '/static/point.png',
imageType: 'local',
backgroundColor: '#fff',
description: '',
}
function onProtocol(title) {
}
</script>
<style lang="scss" scoped>
.buy-point-wrap {
.title {
.main {
font-size: 80rpx;
color: rgba(232, 181, 150, 1);
letter-spacing: 20px;
position: relative;
left: 20px;
}
.sub {
font-size: 32rpx;
color: #f3d3bf;
letter-spacing: 4px;
}
}
.middle {
.title {
font-size: 39rpx;
color: #fccdb1;
}
.title::before {
content: "";
display: block;
width: 80px;
position: relative;
left: -100px;
top: 14px;
border-bottom: 1px dotted #e7b493;
}
.title::after {
content: "";
display: block;
width: 80px;
position: relative;
left: 130px;
top: -12px;
border-bottom: 1px dotted #e7b493;
}
.sub {
background-color: #e7b493;
border-radius: 18rpx;
border: 1px solid #979797;
letter-spacing: 5px;
padding: 0 10rpx 0 18rpx;
font-size: 24rpx;
}
}
.combo {
.draw-btn {
height: 82rpx;
line-height: normal;
background: linear-gradient(-90deg, rgba(230, 179, 147, 1), rgba(250, 232, 218, 1));
border-radius: 28rpx;
font-size: 32rpx;
font-weight: 500;
}
.draw-btn-raw {
height: 82rpx;
line-height: normal;
background: transparent;
border-radius: 28rpx;
font-size: 32rpx;
font-weight: 500;
color: #e7b493;
border: 1px solid #E7B493;
margin-top: 20px;
}
}
.agreement-box {
width: 100%;
.protocol-check {
transform: scale(0.7);
}
.agreement-text {
font-size: 26rpx;
font-weight: 500;
color: #999999;
.tcp-text {
color: rgba(243, 192, 156, 1);
}
}
}
.shake {
animation: shake 0.05s linear 4 alternate;
}
@keyframes shake {
from {
transform: translateX(-5rpx);
}
to {
transform: translateX(5rpx);
}
}
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<pb-layout navbar="inner" iconColor="#fff" class="loglist-point-wrap" leftIcon="leftIcon" color="#fff" title="分发积分"
:bgStyle="bgStyle">
<view>
<p-empty v-if="state.pagination.total === 0" icon="/static/order-empty.png" text="暂无记录" bgColor="transparent" />
<view v-if="state.pagination.total > 0">
<view v-for="item in state.pagination.list" class=" log-point-item ss-font-26">
<view class="top ss-flex ss-row-between ss-m-b-20">
<view class="label">积分</view>
<view class="date">{{ item.date }}</view>
</view>
<view class="bottom ss-flex ss-row-between">
<view class="last">{{ item.last }}</view>
<view class="change">{{ item.income }}</view>
</view>
</view>
</view>
</view>
<uni-load-more v-if="state.pagination.total > 0" :status="state.loadStatus"
:content-text="{ contentdown: '上拉加载更多' }" />
</pb-layout>
</template>
<script setup>
import { ref } from 'vue'
import { resetPagination } from '@/peach/utils'
import { onLoad, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
const bgStyle = {
backgroundImage: '/static/point.png',
imageType: 'local',
backgroundColor: '#fff'
}
const state = ref({
currentTab: 0,
pagination: {
list: [
{
last: 50000,
date: '2022-05-30',
income: '+1000'
},
{
last: 50000,
date: '2022-05-30',
income: '-1000'
}
],
total: 2,
pageNo: 1,
pageSize: 6,
},
loadStatus: '',
})
function getLogList() { }
//
function loadMore() {
if (state.value.loadStatus === 'noMore') {
return
}
state.value.pagination.pageNo++
getLogList()
}
//
onReachBottom(() => {
loadMore()
})
//
onPullDownRefresh(() => {
resetPagination(state.value.pagination)
getLogList()
setTimeout(function () {
uni.stopPullDownRefresh()
}, 800)
})
</script>
<style lang="scss" scoped>
.loglist-point-wrap {
.log-point-item {
border-bottom: 1px solid rgba(231, 180, 147, .2);
margin: 30rpx 40rpx;
padding-bottom: 30rpx;
.top,
.bottom {
color: #E7B493;
}
}
.log-point-item:last-child {
border-bottom: none;
}
}
</style>

124
pages/user/point/share.vue Normal file
View File

@ -0,0 +1,124 @@
<template>
<pb-layout navbar="inner" iconColor="#fff" class="share-point-wrap" leftIcon="leftIcon" color="#fff" title="分发积分"
:bgStyle="bgStyle">
<view class="title ss-m-t-80">
<view class="main">分发积分</view>
<view class="sub ss-flex ss-row-center ss-m-t-55"><text style="color: #F3D3BF;"
class="cicon-safe"></text>订单多多·出单多多<text style="color: #F3D3BF;" class="cicon-safe"></text></view>
</view>
<view class="user-search">
<view class="search">
<uni-easyinput :styles="{ backgroundColor: 'transparent', color: '#E7B493' }" placeholderStyle="color: #E7B493;"
:clearable="false" :inputBorder="false" type="number" trim="all" suffixIcon="search" placeholder="请输入用户名" />
</view>
<view class="result" :style="{ height: resultHeight, overflow: 'auto' }">
<view v-for="item in 10" class="user ss-flex ss-row-between ss-col-center">
<view class="user-info ss-flex ss-col-center ss-gap-20">
<image style="width: 48rpx; height: 48rpx;" src="/static/default_avatar.png" />
<view class="nickname">哈哈哈</view>
</view>
<view class="input-point">
<uni-easyinput :styles="{ backgroundColor: 'transparent', color: '#E7B493' }"
:placeholderStyle="placeholderStyle" :clearable="false" :inputBorder="false" type="number" trim="all"
placeholder="请输入积分额度" />
</view>
</view>
</view>
</view>
</pb-layout>
</template>
<script setup>
import { ref, computed } from 'vue'
import peach from '@/peach'
const placeholderStyle = ref('color: #E7B493')
const resultHeight = computed(() => {
return `calc(100vh - 728rpx)`
})
const state = ref({
isShaking: false,
agreeStatus: false,
})
const bgStyle = {
backgroundImage: '/static/point.png',
imageType: 'local',
backgroundColor: '#fff',
description: '',
}
function onProtocol(title) {
}
</script>
<style lang="scss" scoped>
.share-point-wrap {
.title {
.main {
font-size: 80rpx;
color: rgba(232, 181, 150, 1);
letter-spacing: 20px;
position: relative;
left: 10px;
text-align: center;
}
.sub {
font-size: 32rpx;
color: #f3d3bf;
letter-spacing: 4px;
}
}
.user-search {
margin: 50rpx 40rpx 0;
.search {
border: 1px solid #E7B493;
border-radius: 30px;
}
:deep(.uniui-search) {
color: #E7B493 !important;
padding-right: 10px;
}
:deep(.uni-easyinput__content-input) {
padding-left: 15px !important;
}
.result {
.user {
margin-top: 20px;
border: 1px solid #E7B493;
border-radius: 30px;
padding: 0 40rpx;
.user-info {
.nickname {
font-size: 26rpx;
color: #E7B493;
}
}
.input-point {
:deep(.uni-easyinput__content-input) {
text-align: right;
}
}
}
}
}
}
</style>

View File

@ -1,19 +1,26 @@
<template>
<pb-layout navbar="normal" class="withdraw-wrap" leftIcon="leftIcon" title="提现" :bgStyle="bgStyle">
<view class="alert">
<view class="alert ss-m-x-40 ss-m-y-20">
提现至
</view>
<view class="method">
<view class="method ss-m-x-40 ss-flex ss-col-center ss-row-between">
<view class="left ss-flex ss-gap-20 ss-col-center">
<view class="cicon-weixin" style="color:#2f9326;font-size: 60rpx"></view>
<view class="info">
<view class="label">其选择提现方式</view>
<view class="cicon-angle"></view>
<view class="note ss-m-t-10">立即到账</view>
</view>
<view class="detail">
<view class="label">提现金额</view>
<view class="account">
<uni-easyinput type="number" />
<view class="all">全部提现</view>
</view>
<view class="note">
<view class="cicon-angle m-t-40"></view>
</view>
<view class="detail ss-m-40 ss-p-30">
<view class="label ss-m-b-50">提现金额</view>
<view class="account ss-m-t-135 ss-m-b-20 ss-flex ss-row-between">
<uni-easyinput :styles="{ backgroundColor: 'transparent' }" :clearable="false" :inputBorder="false"
type="number" trim="all" placeholder="请输入提现金额" />
<view class="all self-end">全部提现</view>
</view>
<view class="note ss-flex ss-row-between">
<view class="last">
剩余额度{{ state.last }}
</view>
@ -22,6 +29,11 @@
</view>
</view>
</view>
<view class="footer-box">
<button class="ss-reset-button draw-btn ui-Shadow-Main" @tap="onSubmit">提现</button>
</view>
</pb-layout>
</template>
@ -38,3 +50,74 @@ const state = ref({
canUse: 10000
})
</script>
<style lang="scss" scoped>
.withdraw-wrap {
.alert {
font-size: 26rpx;
}
.method {
.left {
.info {
font-size: 26rpx;
.note {
color: #b5b5b5
}
}
}
}
.detail {
background-color: rgba(236, 236, 236, .3);
border-radius: 20rpx;
.label {
color: #000;
font-size: 26rpx;
}
.account {
:deep(.uni-easyinput__content-input) {
font-size: 28px;
}
.all {
width: 150rpx;
text-align: right;
color: var(--ui-BG-Main);
font-size: 26rpx;
}
}
.account::before {
content: '¥';
font-size: 48rpx;
color: #333
}
.note {
color: #a3a3a3;
font-size: 26rpx;
}
}
.footer-box {
position: absolute;
padding: 0 40rpx;
width: calc(100% - 80rpx);
bottom: 60rpx;
.draw-btn {
height: 80rpx;
border-radius: 40rpx;
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
color: $white;
}
}
}
</style>

53
peach/api/common/file.js Normal file
View File

@ -0,0 +1,53 @@
import { baseUrl, apiPath } from "@/peach/config";
const FileApi = {
// 上传文件
uploadFile: (file) => {
const token = uni.getStorageSync("token");
uni.showLoading({
title: "上传中",
});
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + apiPath + "/infra/file/upload",
filePath: file,
name: "file",
header: {
// Accept: 'text/json',
Accept: "*/*",
"tenant-id": "1",
// Authorization: 'Bearer test247',
},
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data);
console.log(result);
if (result.error === 1) {
uni.showToast({
icon: "none",
title: result.msg,
});
} else {
uni.showToast({
icon: "none",
title: "上传成功",
});
return resolve(result);
}
},
fail: (error) => {
console.log("上传失败:", error);
uni.showToast({
title: "上传失败",
icon: "none",
});
return resolve(false);
},
complete: () => {
uni.hideLoading();
},
});
});
},
};
export default FileApi;

14
peach/api/member/point.js Normal file
View File

@ -0,0 +1,14 @@
import request from "@/peach/request";
const PointApi = {
// 派发积分
sendPoint: (data) => {
return request({
url: "/particulars/point/send-member",
method: "POST",
data,
});
},
};
export default PointApi;

30
peach/api/trade/goods.js Normal file
View File

@ -0,0 +1,30 @@
import request from "@/peach/request";
const GoodsApi = {
// 商品详情
getProduct: (data) => {
return request({
url: "/trade/order/page",
method: "GET",
params: data,
});
},
// 添加商品
addProduct: (data) => {
return request({
url: "/trade/order/page",
method: "POST",
data,
});
},
// 修改商品
editProduct: (data) => {
return request({
url: "/trade/order/page",
method: "POST",
data,
});
},
};
export default GoodsApi;

14
peach/api/trade/order.js Normal file
View File

@ -0,0 +1,14 @@
import request from "@/peach/request";
const OrderUtil = {
// 获取订单列表
getOrderPage: (data) => {
return request({
url: "/trade/order/page",
method: "GET",
params: data,
});
},
};
export default OrderUtil;

View File

@ -0,0 +1,227 @@
"use strict";
import FileApi from "@/peach/api/common/file";
const ERR_MSG_OK = "chooseAndUploadFile:ok";
const ERR_MSG_FAIL = "chooseAndUploadFile:fail";
function chooseImage(opts) {
const {
count,
sizeType = ["original", "compressed"],
sourceType = ["album", "camera"],
extension,
} = opts;
return new Promise((resolve, reject) => {
uni.chooseImage({
count,
sizeType,
sourceType,
extension,
success(res) {
resolve(normalizeChooseAndUploadFileRes(res, "image"));
},
fail(res) {
reject({
errMsg: res.errMsg.replace("chooseImage:fail", ERR_MSG_FAIL),
});
},
});
});
}
function chooseVideo(opts) {
const {
camera,
compressed,
maxDuration,
sourceType = ["album", "camera"],
extension,
} = opts;
return new Promise((resolve, reject) => {
uni.chooseVideo({
camera,
compressed,
maxDuration,
sourceType,
extension,
success(res) {
const { tempFilePath, duration, size, height, width } = res;
resolve(
normalizeChooseAndUploadFileRes(
{
errMsg: "chooseVideo:ok",
tempFilePaths: [tempFilePath],
tempFiles: [
{
name: (res.tempFile && res.tempFile.name) || "",
path: tempFilePath,
size,
type: (res.tempFile && res.tempFile.type) || "",
width,
height,
duration,
fileType: "video",
cloudPath: "",
},
],
},
"video"
)
);
},
fail(res) {
reject({
errMsg: res.errMsg.replace("chooseVideo:fail", ERR_MSG_FAIL),
});
},
});
});
}
function chooseAll(opts) {
const { count, extension } = opts;
return new Promise((resolve, reject) => {
let chooseFile = uni.chooseFile;
if (
typeof wx !== "undefined" &&
typeof wx.chooseMessageFile === "function"
) {
chooseFile = wx.chooseMessageFile;
}
if (typeof chooseFile !== "function") {
return reject({
errMsg:
ERR_MSG_FAIL + " 请指定 type 类型,该平台仅支持选择 image 或 video。",
});
}
chooseFile({
type: "all",
count,
extension,
success(res) {
resolve(normalizeChooseAndUploadFileRes(res));
},
fail(res) {
reject({
errMsg: res.errMsg.replace("chooseFile:fail", ERR_MSG_FAIL),
});
},
});
});
}
function normalizeChooseAndUploadFileRes(res, fileType) {
res.tempFiles.forEach((item, index) => {
if (!item.name) {
item.name = item.path.substring(item.path.lastIndexOf("/") + 1);
}
if (fileType) {
item.fileType = fileType;
}
item.cloudPath =
Date.now() +
"_" +
index +
item.name.substring(item.name.lastIndexOf("."));
});
if (!res.tempFilePaths) {
res.tempFilePaths = res.tempFiles.map((file) => file.path);
}
return res;
}
function uploadCloudFiles(files, max = 5, onUploadProgress) {
files = JSON.parse(JSON.stringify(files));
const len = files.length;
let count = 0;
let self = this;
return new Promise((resolve) => {
while (count < max) {
next();
}
function next() {
let cur = count++;
if (cur >= len) {
!files.find((item) => !item.url && !item.errMsg) && resolve(files);
return;
}
const fileItem = files[cur];
const index = self.files.findIndex((v) => v.uuid === fileItem.uuid);
fileItem.url = "";
delete fileItem.errMsg;
uniCloud
.uploadFile({
filePath: fileItem.path,
cloudPath: fileItem.cloudPath,
fileType: fileItem.fileType,
onUploadProgress: (res) => {
res.index = index;
onUploadProgress && onUploadProgress(res);
},
})
.then((res) => {
fileItem.url = res.fileID;
fileItem.index = index;
if (cur < len) {
next();
}
})
.catch((res) => {
fileItem.errMsg = res.errMsg || res.message;
fileItem.index = index;
if (cur < len) {
next();
}
});
}
});
}
function uploadFiles(choosePromise, { onChooseFile, onUploadProgress }) {
return choosePromise
.then((res) => {
if (onChooseFile) {
const customChooseRes = onChooseFile(res);
if (typeof customChooseRes !== "undefined") {
return Promise.resolve(customChooseRes).then((chooseRes) =>
typeof chooseRes === "undefined" ? res : chooseRes
);
}
}
return res;
})
.then((res) => {
if (res === false) {
return {
errMsg: ERR_MSG_OK,
tempFilePaths: [],
tempFiles: [],
};
}
return res;
})
.then(async (files) => {
for (let file of files.tempFiles) {
const { data } = await FileApi.uploadFile(file.path);
file.url = data;
}
return files;
});
}
function chooseAndUploadFile(
opts = {
type: "all",
}
) {
if (opts.type === "image") {
return uploadFiles(chooseImage(opts), opts);
} else if (opts.type === "video") {
return uploadFiles(chooseVideo(opts), opts);
}
return uploadFiles(chooseAll(opts), opts);
}
export { chooseAndUploadFile, uploadCloudFiles };

View File

@ -0,0 +1,666 @@
<!-- 文件上传基于 upload-file upload-image 实现 -->
<template>
<view class="uni-file-picker">
<view v-if="title" class="uni-file-picker__header">
<text class="file-title">{{ title }}</text>
<text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
</view>
<view v-if="subtitle" class="file-subtitle">
<view>{{ subtitle }}</view>
</view>
<upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly"
:image-styles="imageStyles" :files-list="url" :limit="limitLength" :disablePreview="disablePreview"
:delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">
<slot>
<view class="is-add">
<image :src="imgsrc" class="add-icon"></image>
</view>
</slot>
</upload-image>
<upload-file v-if="fileMediatype !== 'image' || showType !== 'grid'" :readonly="readonly" :list-styles="listStyles"
:files-list="filesList" :showType="showType" :delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose"
@delFile="delFile">
<slot><button type="primary" size="mini">选择文件</button></slot>
</upload-file>
</view>
</template>
<script>
import { chooseAndUploadFile, uploadCloudFiles } from './choose-and-upload-file.js';
import {
get_file_ext,
get_extname,
get_files_and_is_max,
get_file_info,
get_file_data,
} from './utils.js';
import uploadImage from './upload-image.vue';
import uploadFile from './upload-file.vue';
import peach from '@/peach';
let fileInput = null;
/**
* FilePicker 文件选择上传
* @description 文件选择上传组件可以选择图片视频等任意文件并上传到当前绑定的服务空间
* @tutorial https://ext.dcloud.net.cn/plugin?id=4079
* @property {Object|Array} value 组件数据通常用来回显 ,类型由return-type属性决定
* @property {String|Array} url url数据
* @property {Boolean} disabled = [true|false] 组件禁用
* @value true 禁用
* @value false 取消禁用
* @property {Boolean} readonly = [true|false] 组件只读不可选择不显示进度不显示删除按钮
* @value true 只读
* @value false 取消只读
* @property {Boolean} disable-preview = [true|false] 禁用图片预览 mode:grid 时生效
* @value true 禁用图片预览
* @value false 取消禁用图片预览
* @property {Boolean} del-icon = [true|false] 是否显示删除按钮
* @value true 显示删除按钮
* @value false 不显示删除按钮
* @property {Boolean} auto-upload = [true|false] 是否自动上传值为true则只触发@select,可自行上传
* @value true 自动上传
* @value false 取消自动上传
* @property {Number|String} limit 最大选择个数 h5 会自动忽略多选的部分
* @property {String} title 组件标题右侧显示上传计数
* @property {String} mode = [list|grid] 选择文件后的文件列表样式
* @value list 列表显示
* @value grid 宫格显示
* @property {String} file-mediatype = [image|video|all] 选择文件类型
* @value image 只选择图片
* @value video 只选择视频
* @value all 选择所有文件
* @property {Array} file-extname 选择文件后缀根据 file-mediatype 属性而不同
* @property {Object} list-style mode:list 时的样式
* @property {Object} image-styles 选择文件后缀根据 file-mediatype 属性而不同
* @event {Function} select 选择文件后触发
* @event {Function} progress 文件上传时触发
* @event {Function} success 上传成功触发
* @event {Function} fail 上传失败触发
* @event {Function} delete 文件从列表移除时触发
*/
export default {
name: 'sUploader',
components: {
uploadImage,
uploadFile,
},
options: {
virtualHost: true,
},
emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'update:url'],
props: {
modelValue: {
type: [Array, Object],
default() {
return [];
},
},
url: {
type: [Array, String],
default() {
return [];
},
},
disabled: {
type: Boolean,
default: false,
},
disablePreview: {
type: Boolean,
default: false,
},
delIcon: {
type: Boolean,
default: true,
},
//
autoUpload: {
type: Boolean,
default: true,
},
// h5
limit: {
type: [Number, String],
default: 9,
},
// grid | list | list-card
mode: {
type: String,
default: 'grid',
},
// image/video/all
fileMediatype: {
type: String,
default: 'image',
},
//
fileExtname: {
type: [Array, String],
default() {
return [];
},
},
title: {
type: String,
default: '',
},
listStyles: {
type: Object,
default() {
return {
//
border: true,
// 线
dividline: true,
// 线
borderStyle: {},
};
},
},
imageStyles: {
type: Object,
default() {
return {
width: 'auto',
height: 'auto',
};
},
},
readonly: {
type: Boolean,
default: false,
},
sizeType: {
type: Array,
default() {
return ['original', 'compressed'];
},
},
driver: {
type: String,
default: 'local', // local= | oss | unicloud
},
subtitle: {
type: String,
default: '',
},
},
data() {
return {
files: [],
localValue: [],
imgsrc: peach.$url.static('/static/upload-camera.png', 'local'),
};
},
watch: {
modelValue: {
handler(newVal, oldVal) {
this.setValue(newVal, oldVal);
},
immediate: true,
},
url: {
handler(newVal, oldVal) {
this.setValue(newVal, oldVal);
},
immediate: true,
},
},
computed: {
returnType() {
if (this.limit > 1) {
return 'array';
}
return 'object';
},
filesList() {
let files = [];
this.files.forEach((v) => {
files.push(v);
});
return files;
},
showType() {
if (this.fileMediatype === 'image') {
return this.mode;
}
return 'list';
},
limitLength() {
if (this.returnType === 'object') {
return 1;
}
if (!this.limit) {
return 1;
}
if (this.limit >= 9) {
return 9;
}
return this.limit;
},
},
created() {
if (this.driver === 'local') {
uniCloud.chooseAndUploadFile = chooseAndUploadFile;
}
this.form = this.getForm('uniForms');
this.formItem = this.getForm('uniFormsItem');
if (this.form && this.formItem) {
if (this.formItem.name) {
this.rename = this.formItem.name;
this.form.inputChildrens.push(this);
}
}
},
methods: {
/**
* 公开用户使用清空文件
* @param {Object} index
*/
clearFiles(index) {
if (index !== 0 && !index) {
this.files = [];
this.$nextTick(() => {
this.setEmit();
});
} else {
this.files.splice(index, 1);
}
this.$nextTick(() => {
this.setEmit();
});
},
/**
* 公开用户使用继续上传
*/
upload() {
let files = [];
this.files.forEach((v, index) => {
if (v.status === 'ready' || v.status === 'error') {
files.push(Object.assign({}, v));
}
});
return this.uploadFiles(files);
},
async setValue(newVal, oldVal) {
const newData = async (v) => {
const reg = /cloud:\/\/([\w.]+\/?)\S*/;
let url = '';
if (v.fileID) {
url = v.fileID;
} else {
url = v.url;
}
if (reg.test(url)) {
v.fileID = url;
v.url = await this.getTempFileURL(url);
}
if (v.url) v.path = v.url;
return v;
};
if (this.returnType === 'object') {
if (newVal) {
await newData(newVal);
} else {
newVal = {};
}
} else {
if (!newVal) newVal = [];
for (let i = 0; i < newVal.length; i++) {
let v = newVal[i];
await newData(v);
}
}
this.localValue = newVal;
if (this.form && this.formItem && !this.is_reset) {
this.is_reset = false;
this.formItem.setValue(this.localValue);
}
let filesData = Object.keys(newVal).length > 0 ? newVal : [];
this.files = [].concat(filesData);
},
/**
* 选择文件
*/
choose() {
if (this.disabled) return;
if (
this.files.length >= Number(this.limitLength) &&
this.showType !== 'grid' &&
this.returnType === 'array'
) {
uni.showToast({
title: `您最多选择 ${this.limitLength} 个文件`,
icon: 'none',
});
return;
}
this.chooseFiles();
},
/**
* 选择文件并上传
*/
chooseFiles() {
const _extname = get_extname(this.fileExtname);
//
uniCloud
.chooseAndUploadFile({
type: this.fileMediatype,
compressed: false,
sizeType: this.sizeType,
// TODO video
extension: _extname.length > 0 ? _extname : undefined,
count: this.limitLength - this.files.length, //9
onChooseFile: this.chooseFileCallback,
onUploadProgress: (progressEvent) => {
this.setProgress(progressEvent, progressEvent.index);
},
})
.then((result) => {
this.setSuccessAndError(result.tempFiles);
})
.catch((err) => {
console.log('选择失败', err);
});
},
/**
* 选择文件回调
* @param {Object} res
*/
async chooseFileCallback(res) {
const _extname = get_extname(this.fileExtname);
const is_one =
(Number(this.limitLength) === 1 && this.disablePreview && !this.disabled) ||
this.returnType === 'object';
//
if (is_one) {
this.files = [];
}
let { filePaths, files } = get_files_and_is_max(res, _extname);
if (!(_extname && _extname.length > 0)) {
filePaths = res.tempFilePaths;
files = res.tempFiles;
}
let currentData = [];
for (let i = 0; i < files.length; i++) {
if (this.limitLength - this.files.length <= 0) break;
files[i].uuid = Date.now();
let filedata = await get_file_data(files[i], this.fileMediatype);
filedata.progress = 0;
filedata.status = 'ready';
this.files.push(filedata);
currentData.push({
...filedata,
file: files[i],
});
}
this.$emit('select', {
tempFiles: currentData,
tempFilePaths: filePaths,
});
res.tempFiles = files;
//
if (!this.autoUpload) {
res.tempFiles = [];
}
},
/**
* 批传
* @param {Object} e
*/
uploadFiles(files) {
files = [].concat(files);
return uploadCloudFiles
.call(this, files, 5, (res) => {
this.setProgress(res, res.index, true);
})
.then((result) => {
this.setSuccessAndError(result);
return result;
})
.catch((err) => {
console.log(err);
});
},
/**
* 成功或失败
*/
async setSuccessAndError(res, fn) {
let successData = [];
let errorData = [];
let tempFilePath = [];
let errorTempFilePath = [];
for (let i = 0; i < res.length; i++) {
const item = res[i];
const index = item.uuid ? this.files.findIndex((p) => p.uuid === item.uuid) : item.index;
if (index === -1 || !this.files) break;
if (item.errMsg === 'request:fail') {
this.files[index].url = item.path;
this.files[index].status = 'error';
this.files[index].errMsg = item.errMsg;
// this.files[index].progress = -1
errorData.push(this.files[index]);
errorTempFilePath.push(this.files[index].url);
} else {
this.files[index].errMsg = '';
this.files[index].fileID = item.url;
const reg = /cloud:\/\/([\w.]+\/?)\S*/;
if (reg.test(item.url)) {
this.files[index].url = await this.getTempFileURL(item.url);
} else {
this.files[index].url = item.url;
}
this.files[index].status = 'success';
this.files[index].progress += 1;
successData.push(this.files[index]);
tempFilePath.push(this.files[index].fileID);
}
}
if (successData.length > 0) {
this.setEmit();
//
this.$emit('success', {
tempFiles: this.backObject(successData),
tempFilePaths: tempFilePath,
});
}
if (errorData.length > 0) {
this.$emit('fail', {
tempFiles: this.backObject(errorData),
tempFilePaths: errorTempFilePath,
});
}
},
/**
* 获取进度
* @param {Object} progressEvent
* @param {Object} index
* @param {Object} type
*/
setProgress(progressEvent, index, type) {
const fileLenth = this.files.length;
const percentNum = (index / fileLenth) * 100;
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
let idx = index;
if (!type) {
idx = this.files.findIndex((p) => p.uuid === progressEvent.tempFile.uuid);
}
if (idx === -1 || !this.files[idx]) return;
// fix by mehaotian 100 -1
this.files[idx].progress = percentCompleted - 1;
//
this.$emit('progress', {
index: idx,
progress: parseInt(percentCompleted),
tempFile: this.files[idx],
});
},
/**
* 删除文件
* @param {Object} index
*/
delFile(index) {
this.$emit('delete', {
tempFile: this.files[index],
tempFilePath: this.files[index].url,
});
this.files.splice(index, 1);
this.$nextTick(() => {
this.setEmit();
});
},
/**
* 获取文件名和后缀
* @param {Object} name
*/
getFileExt(name) {
const last_len = name.lastIndexOf('.');
const len = name.length;
return {
name: name.substring(0, last_len),
ext: name.substring(last_len + 1, len),
};
},
/**
* 处理返回事件
*/
setEmit() {
let data = [];
let updateUrl = [];
if (this.returnType === 'object') {
data = this.backObject(this.files)[0];
this.localValue = data ? data : null;
updateUrl = data ? data.url : '';
} else {
data = this.backObject(this.files);
if (!this.localValue) {
this.localValue = [];
}
this.localValue = [...data];
if (this.localValue.length > 0) {
this.localValue.forEach((item) => {
updateUrl.push(item.url);
});
}
}
this.$emit('update:modelValue', this.localValue);
this.$emit('update:url', updateUrl);
},
/**
* 处理返回参数
* @param {Object} files
*/
backObject(files) {
let newFilesData = [];
files.forEach((v) => {
newFilesData.push({
extname: v.extname,
fileType: v.fileType,
image: v.image,
name: v.name,
path: v.path,
size: v.size,
fileID: v.fileID,
url: v.url,
});
});
return newFilesData;
},
async getTempFileURL(fileList) {
fileList = {
fileList: [].concat(fileList),
};
const urls = await uniCloud.getTempFileURL(fileList);
return urls.fileList[0].tempFileURL || '';
},
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false;
parentName = parent.$options.name;
}
return parent;
},
},
};
</script>
<style lang="scss" scoped>
.uni-file-picker {
/* #ifndef APP-NVUE */
box-sizing: border-box;
overflow: hidden;
/* width: 100%; */
/* #endif */
/* flex: 1; */
position: relative;
}
.uni-file-picker__header {
padding-top: 5px;
padding-bottom: 10px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: space-between;
}
.file-title {
font-size: 14px;
color: #333;
}
.file-count {
font-size: 14px;
color: #999;
}
.is-add {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
}
.add-icon {
width: 57rpx;
height: 49rpx;
}
.file-subtitle {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0;
width: 140rpx;
height: 36rpx;
z-index: 1;
display: flex;
justify-content: center;
color: #fff;
font-weight: 500;
background: rgba(#000, 0.3);
font-size: 24rpx;
}
</style>

View File

@ -0,0 +1,318 @@
<template>
<view class="uni-file-picker__files">
<view v-if="!readonly" class="files-button" @click="choose">
<slot></slot>
</view>
<!-- :class="{'is-text-box':showType === 'list'}" -->
<view v-if="list.length > 0" class="uni-file-picker__lists is-text-box" :style="borderStyle">
<!-- ,'is-list-card':showType === 'list-card' -->
<view class="uni-file-picker__lists-box" v-for="(item, index) in list" :key="index" :class="{
'files-border': index !== 0 && styles.dividline,
}" :style="index !== 0 && styles.dividline && borderLineStyle">
<view class="uni-file-picker__item">
<!-- :class="{'is-text-image':showType === 'list'}" -->
<!-- <view class="files__image is-text-image">
<image class="header-image" :src="item.logo" mode="aspectFit"></image>
</view> -->
<view class="files__name">{{ item.name }}</view>
<view v-if="delIcon && !readonly" class="icon-del-box icon-files" @click="delFile(index)">
<view class="icon-del icon-files"></view>
<view class="icon-del rotate"></view>
</view>
</view>
<view v-if="(item.progress && item.progress !== 100) || item.progress === 0" class="file-picker__progress">
<progress class="file-picker__progress-item" :percent="item.progress === -1 ? 0 : item.progress"
stroke-width="4" :backgroundColor="item.errMsg ? '#ff5a5f' : '#EBEBEB'" />
</view>
<view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="uploadFiles(item, index)">
点击重试
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'uploadFile',
emits: ['uploadFiles', 'choose', 'delFile'],
props: {
filesList: {
type: Array,
default() {
return [];
},
},
delIcon: {
type: Boolean,
default: true,
},
limit: {
type: [Number, String],
default: 9,
},
showType: {
type: String,
default: '',
},
listStyles: {
type: Object,
default() {
return {
//
border: true,
// 线
dividline: true,
// 线
borderStyle: {},
};
},
},
readonly: {
type: Boolean,
default: false,
},
},
computed: {
list() {
let files = [];
this.filesList.forEach((v) => {
files.push(v);
});
return files;
},
styles() {
let styles = {
border: true,
dividline: true,
'border-style': {},
};
return Object.assign(styles, this.listStyles);
},
borderStyle() {
let { borderStyle, border } = this.styles;
let obj = {};
if (!border) {
obj.border = 'none';
} else {
let width = (borderStyle && borderStyle.width) || 1;
width = this.value2px(width);
let radius = (borderStyle && borderStyle.radius) || 5;
radius = this.value2px(radius);
obj = {
'border-width': width,
'border-style': (borderStyle && borderStyle.style) || 'solid',
'border-color': (borderStyle && borderStyle.color) || '#eee',
'border-radius': radius,
};
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
borderLineStyle() {
let obj = {};
let { borderStyle } = this.styles;
if (borderStyle && borderStyle.color) {
obj['border-color'] = borderStyle.color;
}
if (borderStyle && borderStyle.width) {
let width = (borderStyle && borderStyle.width) || 1;
let style = (borderStyle && borderStyle.style) || 0;
if (typeof width === 'number') {
width += 'px';
} else {
width = width.indexOf('px') ? width : width + 'px';
}
obj['border-width'] = width;
if (typeof style === 'number') {
style += 'px';
} else {
style = style.indexOf('px') ? style : style + 'px';
}
obj['border-top-style'] = style;
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
},
methods: {
uploadFiles(item, index) {
this.$emit('uploadFiles', {
item,
index,
});
},
choose() {
this.$emit('choose');
},
delFile(index) {
this.$emit('delFile', index);
},
value2px(value) {
if (typeof value === 'number') {
value += 'px';
} else {
value = value.indexOf('px') !== -1 ? value : value + 'px';
}
return value;
},
},
};
</script>
<style lang="scss">
.uni-file-picker__files {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: flex-start;
}
.files-button {
// border: 1px red solid;
}
.uni-file-picker__lists {
position: relative;
margin-top: 5px;
overflow: hidden;
}
.file-picker__mask {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: 0;
color: #fff;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.4);
}
.uni-file-picker__lists-box {
position: relative;
}
.uni-file-picker__item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
padding: 8px 10px;
padding-right: 5px;
padding-left: 10px;
}
.files-border {
border-top: 1px #eee solid;
}
.files__name {
flex: 1;
font-size: 14px;
color: #666;
margin-right: 25px;
/* #ifndef APP-NVUE */
word-break: break-all;
word-wrap: break-word;
/* #endif */
}
.icon-files {
/* #ifndef APP-NVUE */
position: static;
background-color: initial;
/* #endif */
}
// .icon-files .icon-del {
// background-color: #333;
// width: 12px;
// height: 1px;
// }
.is-list-card {
border: 1px #eee solid;
margin-bottom: 5px;
border-radius: 5px;
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1);
padding: 5px;
}
.files__image {
width: 40px;
height: 40px;
margin-right: 10px;
}
.header-image {
width: 100%;
height: 100%;
}
.is-text-box {
border: 1px #eee solid;
border-radius: 5px;
}
.is-text-image {
width: 25px;
height: 25px;
margin-left: 5px;
}
.rotate {
position: absolute;
transform: rotate(90deg);
}
.icon-del-box {
/* #ifndef APP-NVUE */
display: flex;
margin: auto 0;
/* #endif */
align-items: center;
justify-content: center;
position: absolute;
top: 0px;
bottom: 0;
right: 5px;
height: 26px;
width: 26px;
// border-radius: 50%;
// background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
transform: rotate(-45deg);
}
.icon-del {
width: 15px;
height: 1px;
background-color: #333;
// border-radius: 1px;
}
/* #ifdef H5 */
@media all and (min-width: 768px) {
.uni-file-picker__files {
max-width: 375px;
}
}
/* #endif */
</style>

View File

@ -0,0 +1,302 @@
<template>
<view class="uni-file-picker__container">
<view class="file-picker__box" v-for="(url, index) in list" :key="index" :style="boxStyle">
<view class="file-picker__box-content" :style="borderStyle">
<image class="file-image" :src="getImageUrl(url)" mode="aspectFill" @click.stop="previewImage(url, index)">
</image>
<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
<view class="icon-del"></view>
<view class="icon-del rotate"></view>
</view>
<!-- <view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item, index)">
点击重试
</view> -->
</view>
</view>
<view v-if="list.length < limit && !readonly" class="file-picker__box" :style="boxStyle">
<view class="file-picker__box-content is-add" :style="borderStyle" @click="choose">
<slot>
<view class="icon-add"></view>
<view class="icon-add rotate"></view>
</slot>
</view>
</view>
</view>
</template>
<script>
import peach from '@/peach';
export default {
name: 'uploadImage',
emits: ['uploadFiles', 'choose', 'delFile'],
props: {
filesList: {
type: [Array, String],
default() {
return [];
},
},
disabled: {
type: Boolean,
default: false,
},
disablePreview: {
type: Boolean,
default: false,
},
limit: {
type: [Number, String],
default: 9,
},
imageStyles: {
type: Object,
default() {
return {
width: 'auto',
height: 'auto',
border: {},
};
},
},
delIcon: {
type: Boolean,
default: true,
},
readonly: {
type: Boolean,
default: false,
},
},
computed: {
list() {
if (typeof this.filesList === 'string') {
if (this.filesList) {
return [this.filesList];
} else {
return [];
}
}
return this.filesList;
},
styles() {
let styles = {
width: 'auto',
height: 'auto',
border: {},
};
return Object.assign(styles, this.imageStyles);
},
boxStyle() {
const { width = 'auto', height = 'auto' } = this.styles;
let obj = {};
if (height === 'auto') {
if (width !== 'auto') {
obj.height = this.value2px(width);
obj['padding-top'] = 0;
} else {
obj.height = 0;
}
} else {
obj.height = this.value2px(height);
obj['padding-top'] = 0;
}
if (width === 'auto') {
if (height !== 'auto') {
obj.width = this.value2px(height);
} else {
obj.width = '33.3%';
}
} else {
obj.width = this.value2px(width);
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
borderStyle() {
let { border } = this.styles;
let obj = {};
const widthDefaultValue = 1;
const radiusDefaultValue = 3;
if (typeof border === 'boolean') {
obj.border = border ? '1px #eee solid' : 'none';
} else {
let width = (border && border.width) || widthDefaultValue;
width = this.value2px(width);
let radius = (border && border.radius) || radiusDefaultValue;
radius = this.value2px(radius);
obj = {
'border-width': width,
'border-style': (border && border.style) || 'solid',
'border-color': (border && border.color) || '#eee',
'border-radius': radius,
};
}
let classles = '';
for (let i in obj) {
classles += `${i}:${obj[i]};`;
}
return classles;
},
},
methods: {
getImageUrl(url) {
if ('blob:http:' === url.substr(0, 10)) {
return url;
} else {
return peach.$url.cdn(url);
}
},
uploadFiles(item, index) {
this.$emit('uploadFiles', item);
},
choose() {
this.$emit('choose');
},
delFile(index) {
this.$emit('delFile', index);
},
previewImage(img, index) {
let urls = [];
if (Number(this.limit) === 1 && this.disablePreview && !this.disabled) {
this.$emit('choose');
}
if (this.disablePreview) return;
this.list.forEach((i) => {
urls.push(this.getImageUrl(i));
});
uni.previewImage({
urls: urls,
current: index,
});
},
value2px(value) {
if (typeof value === 'number') {
value += 'px';
} else {
if (value.indexOf('%') === -1) {
value = value.indexOf('px') !== -1 ? value : value + 'px';
}
}
return value;
},
},
};
</script>
<style lang="scss">
.uni-file-picker__container {
/* #ifndef APP-NVUE */
display: flex;
box-sizing: border-box;
/* #endif */
flex-wrap: wrap;
margin: -5px;
}
.file-picker__box {
position: relative;
// flex: 0 0 33.3%;
width: 33.3%;
height: 0;
padding-top: 33.33%;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
}
.file-picker__box-content {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: 5px;
border: 1px #eee solid;
border-radius: 5px;
overflow: hidden;
}
.file-picker__progress {
position: absolute;
bottom: 0;
left: 0;
right: 0;
/* border: 1px red solid; */
z-index: 2;
}
.file-picker__progress-item {
width: 100%;
}
.file-picker__mask {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: 0;
color: #fff;
font-size: 12px;
background-color: rgba(0, 0, 0, 0.4);
}
.file-image {
width: 100%;
height: 100%;
}
.is-add {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
}
.icon-add {
width: 50px;
height: 5px;
background-color: #f1f1f1;
border-radius: 2px;
}
.rotate {
position: absolute;
transform: rotate(90deg);
}
.icon-del-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
position: absolute;
top: 3px;
right: 3px;
height: 26px;
width: 26px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
transform: rotate(-45deg);
}
.icon-del {
width: 15px;
height: 2px;
background-color: #fff;
border-radius: 2px;
}
</style>

View File

@ -0,0 +1,110 @@
/**
* 获取文件名和后缀
* @param {String} name
*/
export const get_file_ext = (name) => {
const last_len = name.lastIndexOf(".");
const len = name.length;
return {
name: name.substring(0, last_len),
ext: name.substring(last_len + 1, len),
};
};
/**
* 获取扩展名
* @param {Array} fileExtname
*/
export const get_extname = (fileExtname) => {
if (!Array.isArray(fileExtname)) {
let extname = fileExtname.replace(/(\[|\])/g, "");
return extname.split(",");
} else {
return fileExtname;
}
return [];
};
/**
* 获取文件和检测是否可选
*/
export const get_files_and_is_max = (res, _extname) => {
let filePaths = [];
let files = [];
if (!_extname || _extname.length === 0) {
return {
filePaths,
files,
};
}
res.tempFiles.forEach((v) => {
let fileFullName = get_file_ext(v.name);
const extname = fileFullName.ext.toLowerCase();
if (_extname.indexOf(extname) !== -1) {
files.push(v);
filePaths.push(v.path);
}
});
if (files.length !== res.tempFiles.length) {
uni.showToast({
title: `当前选择了${res.tempFiles.length}个文件 ${
res.tempFiles.length - files.length
} 个文件格式不正确`,
icon: "none",
duration: 5000,
});
}
return {
filePaths,
files,
};
};
/**
* 获取图片信息
* @param {Object} filepath
*/
export const get_file_info = (filepath) => {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: filepath,
success(res) {
resolve(res);
},
fail(err) {
reject(err);
},
});
});
};
/**
* 获取封装数据
*/
export const get_file_data = async (files, type = "image") => {
// 最终需要上传数据库的数据
let fileFullName = get_file_ext(files.name);
const extname = fileFullName.ext.toLowerCase();
let filedata = {
name: files.name,
uuid: files.uuid,
extname: extname || "",
cloudPath: files.cloudPath,
fileType: files.fileType,
url: files.path || files.path,
size: files.size, //单位是字节
image: {},
path: files.path,
video: {},
};
if (type === "image") {
const imageinfo = await get_file_info(files.path);
delete filedata.video;
filedata.image.width = imageinfo.width;
filedata.image.height = imageinfo.height;
filedata.image.location = imageinfo.path;
} else {
delete filedata.image;
}
return filedata;
};

View File

@ -104,7 +104,7 @@
margin padding: 内外边距
==================== */
@for $i from 0 through 100 {
@for $i from 0 through 200 {
// 只要双数和能被5除尽的数
@if $i % 2==0 or $i % 5==0 {
// 得出u-margin-30或者u-m-30

BIN
static/upload-camera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -112,10 +112,10 @@
// default: ''
// },
// 1.4.0 使 form label
// labelPosition: {
// type: String,
// default: ''
// },
labelPosition: {
type: String,
default: ''
},
// 1.4.0 使 #label
leftIcon: String,
iconColor: {
@ -419,6 +419,7 @@
},
// label
_labelPosition() {
if (this.labelPosition) return this.labelPosition;
if (this.form) return this.form.labelPosition || 'left'
return 'left'