第一次上传

This commit is contained in:
Ls
2026-03-09 16:39:03 +08:00
commit 3d9efaf15c
924 changed files with 326227 additions and 0 deletions

1
.svn/entries Normal file
View File

@@ -0,0 +1 @@
12

1
.svn/format Normal file
View File

@@ -0,0 +1 @@
12

View File

@@ -0,0 +1,143 @@
{
"name" : "科讯代购",
"appid" : "__UNI__06C2D6A",
"description" : "",
"versionName" : "1.0.8",
"versionCode" : 108,
"transformPx" : false,
/* 5+App特有相关 */
"app-plus" : {
"compatible" : {
"ignoreVersion" : true //true表示忽略版本检查提示框HBuilderX1.9.0及以上版本支持
},
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : false,
"waiting" : false,
"autoclose" : true,
"delay" : 0
},
/* 模块配置 */
"modules" : {
"Barcode" : {},
"Maps" : {},
"Geolocation" : {}
},
/* 应用发布信息 */
"distribute" : {
/* android打包配置 */
"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\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
],
"minSdkVersion" : 25,
"targetSdkVersion" : 25,
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ]
},
/* ios打包配置 */
"ios" : {
"idfa" : false,
"dSYMs" : false
},
/* SDK配置 */
"sdkConfigs" : {
"ad" : {},
"maps" : {
"amap" : {
"name" : "amapZAvZjTHj",
"appkey_ios" : "3caf9e6f01b0085be1e75e0d0e281fe7",
"appkey_android" : "3caf9e6f01b0085be1e75e0d0e281fe7"
}
},
"geolocation" : {
"amap" : {
"name" : "amapZAvZjTHj",
"__platform__" : [ "android" ],
"appkey_ios" : "",
"appkey_android" : "3caf9e6f01b0085be1e75e0d0e281fe7"
}
}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
}
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
"mp-weixin" : {
"appid" : "wx48366fb1ba81d1ea",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"h5" : {
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "7DIBZ-K4HCJ-ZR2FE-FOOOP-SALFT-RLFYW"
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,100 @@
<template>
<view style="padding: 20rpx;">
<view class="" v-for="(item,index) in noticeList " :key="index" @click="Service.GoPage('/pages/article/articleCom?noticeId='+item.noticeId)"
style=" padding: 20rpx; margin-top: 20rpx; border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #e2e2e2; ">
<view class="" style="display: flex; align-items: center; ">
<view class="tag" v-if="index==0"
style=" color: #fff; border-radius: 12rpx; background-color: #FF6B35; padding: 4rpx 20rpx; ">
<img :src="Service.GetIconImg('/static/index/community/top.png')"
style="width: 30rpx; height: 30rpx; " alt="" />
<text style="margin-left: 10rpx; font-size: 24rpx;">置顶</text>
</view>
<view class="tag" v-else
style=" color: #fff; border-radius: 12rpx; background-color: #FF6B35; padding: 4rpx 20rpx; ">
<img :src="Service.GetIconImg('/static/index/community/top.png')"
style="width: 30rpx; height: 30rpx; " alt="" />
<text style="margin-left: 10rpx; font-size: 24rpx;">{{item.sign}}</text>
</view>
<text style="font-size: 32rpx; font-weight: 600; margin-left: 15rpx; ">{{item.title}}</text>
</view>
<view class=""
style=" margin: 16rpx 0; color: #666666; font-size: 26rpx; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; ">
{{item.remark}}
</view>
<view class="" style=" font-size: 22rpx; color: #999999; display: flex; align-items: center;">
<up-icon name="clock" size="12" color='#999999' ></up-icon>
<text style="margin-left: 10rpx;" >{{Service.formatDate(item.addTime,2)}}</text>
<!-- <text style="margin: 0 15rpx;">·</text>
<view class="" style="display: flex;align-items: center;">
<img :src="Service.GetIconImg('/static/index/community/see.png')"
style="width: 30rpx; height: 30rpx; " alt="" />
<text style="margin-left: 10rpx;">1200人阅读</text>
</view> -->
</view>
</view>
<up-loadmore :status="status" />
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
import { vpCommunityService } from '@/Service/vp/vpCommunityService'
interface notice{
addTime:string
noticeId:string
remark:string
sign:string
title:string
}
let status = ref('nomore')
let page=ref(1)
let noticeList=ref<notice[]>([])
let comId=ref()
onLoad((data:any) => {
comId.value=data.comId
getData()
});
onShow(() => {
});
onReachBottom(()=>{
getList()
})
const getData=()=>{
status.value='loadmore'
page.value=1
noticeList.value=[]
getList()
}
const getList=()=>{
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpCommunityService.GetCommunityNoticeList(comId.value,page.value).then(res=>{
noticeList.value=[...noticeList.value,...res.data.noticeList]
status.value = res.data.noticeList.length == 10 ? 'loadmore' : 'nomore'
page.value++
})
}
</script>
<style lang="scss">
.tag {
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
font-size: 24rpx;
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
</template>
<script>
import {parent, children} from '../common/relation';
export default {
name: 'lime-painter-image',
mixins:[children('painter')],
props: {
id: String,
css: [String, Object],
src: String
},
data() {
return {
type: 'image',
el: {
css: {},
src: null
},
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,442 @@
<template>
<view class="member-code-page">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="Service.GoPageBack()" mode="aspectFit" />
<text class="nav-title">会员码</text>
<view class="nav-placeholder"></view>
</view>
<!-- 主要内容区域 -->
<view class="content">
<!-- 会员卡片 -->
<view class="member-card">
<!-- 用户信息 -->
<view class="user-section">
<image class="user-avatar" :src="Service.GetMateUrlByImg(userData.headImg)" mode="aspectFill" />
<view class="user-info">
<text class="user-name">{{ userData.nick }}</text>
<!-- ID和会员等级标签在一排 -->
<view class="tags-row">
<view class="user-id-tag" @click="copyMemberId">
<text class="ri-vip-crown-2-fill id-icon"></text>
<text class="id-text">ID: {{ userData.userNo }}</text>
<text class="ri-file-copy-line id-copy"></text>
</view>
<!-- <view class="user-member-tag">
<text class="ri-vip-crown-fill member-icon"></text>
<text class="member-text">黄金会员</text>
</view> -->
</view>
</view>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 条形码区域 -->
<view class="barcode-section">
<u-barcode :value="code" :displayValue='false' :width="300" :height="100" />
<text class="barcode-number">{{ code }}</text>
</view>
<!-- 二维码区域 -->
<view class="qrcode-section">
<view class="qrcode-wrapper" >
<view class="qrcode-placeholder">
<up-qrcode cid="ex1" :size="180" :val="erCode"></up-qrcode>
</view>
</view>
<text class="code-tip">向商家出示会员码,扫码享受积分优惠</text>
</view>
<!-- 刷新提示 -->
<view class="refresh-section">
<text class="ri-time-line refresh-icon"></text>
<text class="refresh-text">{{ refreshCountdown }}秒后自动刷新</text>
<view class="refresh-btn" @click="refreshCode">
<text class="ri-refresh-line btn-icon"></text>
<text class="btn-text">刷新</text>
</view>
</view>
</view>
<!-- 温馨提示 -->
<view class="tips-section">
<view class="tips-title">
<text class="ri-lightbulb-line tips-icon"></text>
<text class="title-text">温馨提示</text>
</view>
<view class="tips-list">
<text class="tips-item">• 会员码每分钟自动更新,确保使用安全</text>
<text class="tips-item">• 结账时请向商家出示此码</text>
<text class="tips-item">• 消费可获得积分,积分可抵扣现金</text>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { vpUserService, Service } from '@/Service/vp/vpUserService'
import { onLoad } from '@dcloudio/uni-app'
// 用户数据
const user = ref({
nickname: '美食达人',
avatar: 'https://picsum.photos/200/200?random=100',
points: 2580,
memberLevel: 'gold',
memberId: '8888888',
memberCode: 'VIP8888888888888'
})
let code = ref('')
let erCode = ref('')
let userData = ref<any>({})
// 刷新倒计时(秒)
const refreshCountdown = ref(60)
let refreshTimer = ref(0)
let countdownTimer = ref(0)
onLoad(() => {
fetchUserInfo()
getCode()
startAutoRefresh()
})
// 获取用户信息
const fetchUserInfo = () => {
vpUserService.GetUserInfo().then(res => {
if (res.code == 0) {
userData.value = res.data.userInfo
}
})
}
const getCode=()=>{
vpUserService.GetUserCode().then(res => {
if (res.code==0){
code.value=res.data.code
erCode.value=res.data.erCode
}
})
}
// 刷新会员码
const refreshCode = () => {
getCode()
// 重置倒计时
refreshCountdown.value = 60
Service.Msg('会员码已刷新')
}
// 复制会员ID
const copyMemberId = () => {
uni.setClipboardData({
data: userData.value.userNo,
success: () => {
Service.Msg('会员ID已复制')
}
})
}
// 开始自动刷新
const startAutoRefresh = () => {
// 清除之前的定时器
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
}
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
}
// 倒计时定时器(每秒更新)
countdownTimer.value = setInterval(() => {
refreshCountdown.value--
if (refreshCountdown.value <= 0) {
refreshCountdown.value = 60
getCode()
}
}, 1000)
}
// 清理定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer.value)
}
if (countdownTimer) {
clearInterval(countdownTimer.value)
}
})
</script>
<style lang="scss" scoped>
/* 引入全局标签样式 */
@import '@/styles/member-tags.scss';
/* 现代化会员码页面 */
.member-code-page {
min-height: 100vh;
background: linear-gradient(180deg, #FFF4E6 0%, #F5F5F5 100%);
display: flex;
flex-direction: column;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 40rpx;
}
/* 内容区域 */
.content {
flex: 1;
padding: 32rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
}
/* 会员卡片 */
.member-card {
width: 100%;
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(255, 107, 0, 0.12);
margin-bottom: 24rpx;
}
/* 用户信息区域 */
.user-section {
display: flex;
align-items: center;
margin-bottom: 32rpx;
}
.user-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 48rpx;
margin-right: 20rpx;
border: 3rpx solid #FF6B00;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #222222;
margin-bottom: 10rpx;
}
/* 分割线 */
.divider {
height: 1rpx;
background: linear-gradient(90deg, transparent, #E0E0E0, transparent);
margin-bottom: 32rpx;
}
/* 条形码区域 */
.barcode-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
}
.barcode-wrapper {
width: 480rpx;
background: #F8F8F8;
border-radius: 12rpx;
padding: 20rpx 32rpx;
margin-bottom: 16rpx;
}
.barcode-placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.barcode-lines {
font-size: 40rpx;
color: #000000;
letter-spacing: 2rpx;
font-weight: 900;
line-height: 1;
}
.barcode-number {
font-size: 32rpx;
font-weight: 600;
color: #222222;
letter-spacing: 4rpx;
}
/* 二维码区域 */
.qrcode-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
}
.qrcode-wrapper {
width: 360rpx;
height: 360rpx;
background: #F8F8F8;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.qrcode-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #000000;
border-radius: 8rpx;
}
.qrcode-icon {
font-size: 200rpx;
color: #FFFFFF;
}
.code-tip {
font-size: 22rpx;
color: #999999;
text-align: center;
margin-top: 10rpx;
}
/* 刷新区域 */
.refresh-section {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.refresh-icon {
font-size: 24rpx;
color: #FF6B00;
}
.refresh-text {
font-size: 24rpx;
color: #666666;
text-align: center;
}
.refresh-btn {
display: flex;
align-items: center;
gap: 4rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
padding: 10rpx 20rpx;
border-radius: 24rpx;
}
.btn-icon {
font-size: 20rpx;
color: #FFFFFF;
}
.btn-text {
font-size: 24rpx;
color: #FFFFFF;
font-weight: 500;
}
/* 温馨提示 */
.tips-section {
width: 100%;
background: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.tips-title {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 16rpx;
}
.tips-icon {
font-size: 28rpx;
color: #FFA500;
}
.title-text {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.tips-item {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,361 @@
<template>
<view style="display: flex; flex-direction: column; height: 100vh; ">
<view class="merchant-info">
<view class="">
<text class="section-title">付款给商户</text>
<text class="merchant-name">{{ storeInfo.name }}</text>
</view>
<image :src="Service.GetMateUrlByImg(storeInfo.logo)" mode="aspectFill" class="merchant-icon">
</image>
</view>
<view class=""
style=" padding: 30rpx 40rpx; flex: 1; background-color: #fff; width: 100%; border-top-right-radius: 30rpx; border-top-left-radius: 30rpx; ">
<view class="" style="font-size: 24rpx; font-weight: 600;">
余额
</view>
<view class="" style="margin: 20rpx 0; padding: 20rpx 0; border-bottom: 1rpx solid #e2e2e2; ">
<!-- <up-input prefixIcon='rmb' :prefixIconStyle="{ 'color':'#000','font-weight': 600,'font-size':'60rpx' }"
fontSize='50rpx'
auto-blur="false"
@focus="focusFunc"
:customStyle="{'color':'#000', height: '90rpx', 'padding-left': 0, 'font-weight': 600,'padding-bottom':'20rpx' }"
border="bottom" v-model="account"></up-input> -->
<view class="" style="display: flex; align-items: center; width: 100%; ">
<view class="" style="height: 70rpx; display: flex; align-items: center; ">
<up-icon name="rmb" :bold='true' size='50rpx' color="#000"></up-icon>
</view>
<view class="" style=" height: 70rpx; line-height: 70rpx; font-weight: 600;font-size: 70rpx; ">
{{account}}
</view>
<view class="" v-if="isShow"
style="margin: 0 10rpx; width: 4rpx; height: 70rpx; background-color: var(--nav-mian); ">
</view>
</view>
</view>
<view v-if="storeInfo.code=='Discounts' && userInfo.integral>0 " class=""
style="font-size: 24rpx; color: #6B6B6B; ">
<text>当前拥有{{ userInfo.integral }}积分</text>
<text style="margin-left: 10rpx;">可抵扣 ¥{{ userInfo.integral }}元</text>
</view>
<view v-else class="">
<view v-if="account>0 " class="" style="font-size: 24rpx; color: #6B6B6B; ">
本次消费可得 {{ computePoints( account ) }} 积分
</view>
</view>
<view class="" style="font-size: 24rpx; color: #6B6B6B; ">
当前可用优惠券3张 <text style="color: blue;">点击查看</text>
</view>
<!-- <view class="" style="font-size: 28rpx; margin-top: 10rpx; ">
<text :style="{'margin-right':!des?'':'15rpx'}">{{des}}</text>
<text @click="showDes=true" style="font-weight: 600; color: #586B95;">添加备注</text>
</view> -->
<view class=""
style="background-color: #f5f5f5; padding: 20rpx; position: fixed; bottom: 0; left: 0; width: 100%; padding-top: 25rpx; padding-bottom: 20rpx; ">
<view class="" style="display: grid; grid-template-columns: repeat(4,1fr); ">
<view class="button" @click="input(item)" v-for="(item,index) in 3" :key="index">
{{item}}
</view>
<view @click="deleInput()" class="button">
<up-icon name="backspace" :bold='true' size="26"></up-icon>
</view>
</view>
<view class="" style="display: grid; grid-template-columns: 3fr 1fr; ">
<view class="">
<view class="" style="display: grid; grid-template-columns: repeat(3,1fr); ">
<view class="button" @click="input(item+3)" v-for="(item,index) in 3" :key="index">
{{item+3}}
</view>
</view>
<view class="" style="display: grid; grid-template-columns: repeat(3,1fr); ">
<view class="button" @click="input(item+6)" v-for="(item,index) in 3" :key="index">
{{item+6}}
</view>
</view>
<view class="" style="display: grid; grid-template-columns: 2fr 1fr; ">
<view @click="input(0)" class="button">
0
</view>
<view @click="input('.')" class="button">
.
</view>
</view>
</view>
<view @click="save()" class="button" style="background-color: var(--nav-mian); color: #fff; ">
付款
</view>
</view>
</view>
</view>
</view>
<!-- <up-popup :show="showDes">
<view style="width: 100%; padding: 50rpx 30rpx; ">
<view class="">
<text style="font-size: 28rpx; font-weight: 600;">添加备注</text>
</view>
<view class=""
style=" margin-top: 30rpx; padding: 20rpx 0; border-bottom: 1rpx solid #e2e2e2; border-top: 1rpx solid #e2e2e2; ">
<up-input placeholder="请输入内容" border="none" v-model="des"></up-input>
</view>
<view class=""
style=" margin: 0 110rpx; margin-top: 50rpx; display: flex; align-items: center; justify-content: space-between; ">
<view class="" @click="showDes=false,des=''"
style=" background-color: #f2f2f2; color: #000; padding: 20rpx 80rpx;border-radius: 20rpx; display: flex; align-items: center; justify-content: center; ">
取消
</view>
<view class="" @click="showDes=false"
style=" background-color: #07c160; color: #fff; padding: 20rpx 80rpx;border-radius: 20rpx; display: flex; align-items: center; justify-content: center; ">
确定
</view>
</view>
</view>
</up-popup> -->
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { onUnmounted, ref } from "vue";
import { vpMerchService } from "@/Service/vp/vpMerchService";
import { vpUserService } from "@/Service/vp/vpUserService";
import { vpLoginService } from "@/Service/vp/vpLoginService";
let account = ref('')
// let showDes = ref(false)
let des = ref()
let isShow = ref(false)
let timeOut = ref()
let payway = ref('')
let par = ref('')
let openId = ref('')
let storeInfo = ref<any>({})
let userInfo = ref<any>({})
let radio = ref(0)
let points = ref('')
let showCoupon = ref(false)
onLoad((data : any) => {
focusFunc()
getOpid()
// 支付宝
// #ifdef MP-ALIPAY
let querdata = Service.GetStorageCache('quer')
payway.value = 'zfb'
par.value = querdata.query.par
// #endif
// #ifdef MP-WEIXIN
payway.value = 'wx'
if (data.q) {
par.value = decodeURIComponent(data.q).split('?')[1].split('=')[1]
}
// #endif
if (!Service.GetUserIsLogin()) {
login()
return
}
getData()
});
onShow(() => {
});
onUnmounted(() => {
clearInterval(timeOut.value)
})
const getData = () => {
vpMerchService.GetUnitMerchInfo(par.value).then(res => {
if (res.code == 0) {
storeInfo.value = res.data.merchInfo
userInfo.value = res.data.accInfo
radio.value = res.data.radio
} else {
Service.Msg('商家获取失败,请重新扫码')
}
})
}
const input = (val : any) => {
if (account.value.split('').length > 8) {
return
}
if (val == '.') {
let arr = account.value.split('').filter((item => item == val))
if (arr.length > 0) {
return
}
if (!account.value) {
account.value = '0.'
return
}
}
account.value = account.value + val
}
const computePoints = (e : any) => {
if (e <= 0.1) {
return 0
}
if (storeInfo.value.code == 'Discounts') {
return Number(e * radio.value).toFixed(2)
} else {
return Number(e * radio.value - e * 0.003).toFixed(2)
}
}
const deleInput = () => {
let arr = account.value.split('')
arr.pop()
account.value = arr.join('')
}
const save = () => {
if (!account.value) {
Service.Msg('请输入金额')
return
}
Service.LoadIng('支付中')
vpUserService.PayMerch(storeInfo.value.merchId, Number(account.value), payway.value, openId.value).then(res => {
if (res.code == 0) {
wx.requestPayment({
timeStamp: res.data.resdata.timeStamp,
nonceStr: res.data.resdata.nonceStr,
package: res.data.resdata.package,
signType: res.data.resdata.signType,
paySign: res.data.resdata.paySign,
success(payRes) {
Service.LoadClose()
//支付完成处理逻辑
Service.Msg("支付成功");
},
fail(err) {
Service.Msg("支付失败");
console.error('pay fail', err)
}
})
} else {
Service.Msg(res.msg)
}
})
}
const focusFunc = () => {
timeOut.value = setInterval(() => {
isShow.value = !isShow.value
}, 1000)
}
const getOpid = () => {
uni.login({
onlyAuthorize: true,
provider: 'weixin',
success: function (loginRes) {
vpLoginService.GetOpenIdByWeixin(loginRes.code, 1).then(res => {
if (res.code == 0) {
openId.value = res.data
} else {
Service.Msg(res.msg)
}
})
}
})
}
const login = () => {
uni.getProvider({
service: 'oauth',
success: function (res : any) {
uni.login({
onlyAuthorize: true,
provider: res.provider,
success: function (loginRes) {
vpLoginService.WxLogin(loginRes.code, res.provider == 'weixin' ? 1 : 2, 0, 0, '').then(content => {
if (content.code == 0) {
Service.SetUserToken(content.data.accToken)
getData()
} else {
Service.Msg(content.msg)
}
})
}
})
}
});
}
</script>
<style lang="scss">
page {
background-color: #f5f5f5;
}
// 按键
.button {
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 18rpx 0;
border-radius: 10rpx;
font-weight: 700;
margin: 8rpx;
font-size: 38rpx;
}
.button:active {
background-color: #ababab;
}
/* 商户信息 */
.merchant-info {
padding: 30rpx 40rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.merchant-name {
font-size: 24rpx;
color: #999999;
display: block;
margin-top: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
}
.merchant-icon {
width: 95rpx;
height: 95rpx;
border-radius: 50%;
margin-right: 20rpx;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

View File

@@ -0,0 +1,72 @@
/*
* Remix Icon Minimal Version - Only Used Icons
* Based on Remix Icon v4.8.0
* https://remixicon.com
* Custom minimal build for VpComUni project
*/
@font-face {
font-family: "remixicon";
src: url("remixicon.woff2?t=1766743011500") format("woff2");
font-display: swap;
}
[class^="ri-"], [class*=" ri-"] {
font-family: 'remixicon' !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Size utilities */
.ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; }
.ri-xl { font-size: 1.5em; line-height: 0.6666em; vertical-align: -.075em; }
.ri-xxs { font-size: .5em; }
.ri-xs { font-size: .75em; }
.ri-sm { font-size: .875em }
.ri-1x { font-size: 1em; }
.ri-2x { font-size: 2em; }
.ri-3x { font-size: 3em; }
.ri-4x { font-size: 4em; }
.ri-5x { font-size: 5em; }
.ri-6x { font-size: 6em; }
.ri-7x { font-size: 7em; }
.ri-8x { font-size: 8em; }
.ri-9x { font-size: 9em; }
.ri-10x { font-size: 10em; }
.ri-fw { text-align: center; width: 1.25em; }
/* Only the icons actually used in the project */
.ri-arrow-right-s-line:before { content: "\ea5e"; }
.ri-bank-card-line:before { content: "\ea92"; }
.ri-calendar-check-line:before { content: "\eb23"; }
.ri-checkbox-circle-fill:before { content: "\eb80"; }
.ri-close-line:before { content: "\eb99"; }
.ri-coin-line:before { content: "\ebb2"; }
.ri-coupon-3-line:before { content: "\ebe6"; }
.ri-customer-service-2-line:before { content: "\ec0c"; }
.ri-edit-line:before { content: "\ec86"; }
.ri-error-warning-line:before { content: "\ec94"; }
.ri-feedback-line:before { content: "\ec9c"; }
.ri-file-copy-line:before { content: "\ea86"; }
.ri-file-list-3-line:before { content: "\ead3"; }
.ri-gift-line:before { content: "\ebd0"; }
.ri-heart-line:before { content: "\ebd6"; }
.ri-information-line:before { content: "\ec9e"; }
.ri-lightbulb-line:before { content: "\ebd7"; }
.ri-map-pin-line:before { content: "\ebda"; }
.ri-medal-line:before { content: "\ebdb"; }
.ri-message-2-line:before { content: "\ebdd"; }
.ri-percent-line:before { content: "\ebe0"; }
.ri-qr-code-line:before { content: "\ebe3"; }
.ri-refresh-line:before { content: "\eb95"; }
.ri-settings-4-line:before { content: "\ebe5"; }
.ri-smartphone-line:before { content: "\ebe7"; }
.ri-sparkling-fill:before { content: "\ebe9"; }
.ri-team-line:before { content: "\ebed"; }
.ri-time-line:before { content: "\ebf1"; }
.ri-user-add-line:before { content: "\ebf3"; }
.ri-vip-crown-2-fill:before { content: "\ebf5"; }
.ri-vip-crown-fill:before { content: "\ebf6"; }
.ri-vip-diamond-fill:before { content: "\ebf7"; }
.ri-wechat-fill:before { content: "\ebf9"; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

View File

@@ -0,0 +1,230 @@
<template>
<view style="margin: 20rpx 30rpx;">
<view class="" style="border-radius: 20rpx; background-color: #fff; padding: 20rpx; ">
<view class="" style="font-weight: 600; font-size: 30rpx;">
商品封面
</view>
<view class="" @click="uploadFImg()"
style=" margin-top: 20rpx; width: 100%; height: 320rpx; border: 4rpx dashed #e2e2e2; border-radius: 20rpx; overflow: hidden; ">
<view class=""
style="width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; ">
<img :src="Service.GetIconImg('/static/goods/photo.png')" style="width: 80rpx; height: 80rpx;"
alt="" />
<view class="" style=" color: #666666; font-size: 28rpx;">
上传图片
</view>
</view>
<!-- <image :src="Service.GetMateUrlByImg('/static/dele/dele4.jpg')" mode="aspectFill" style="width: 100%; height: 100%; " alt="" /> -->
</view>
</view>
<view class="" style=" margin-top: 20rpx; border-radius: 20rpx; background-color: #fff; padding: 20rpx; ">
<up-form labelWidth='90' :labelStyle="{'font-weight': 600 }" labelPosition="top" :model="goodsInfo"
ref="form1">
<up-form-item label="商品名称" prop="goodsInfo.name" :borderBottom="false" ref="item1">
<up-input :customStyle="{'border-radius': '15rpx' }" placeholder="请输入商品名称" border='surround'
v-model="goodsInfo.name"></up-input>
</up-form-item>
<up-form-item label="价格" prop="goodsInfo.sex" :borderBottom="false">
<up-input prefixIcon='rmb' :prefixIconStyle="{ 'color':'#000','font-weight': 600 }"
:customStyle="{'border-radius': '15rpx' }" placeholder="请输入价格" v-model="goodsInfo.prince"
border="surround"></up-input>
</up-form-item>
<up-form-item label="描述" prop="goodsInfo.sex" :borderBottom="false">
<up-textarea v-model="goodsInfo.des" placeholder="请输入商品描述"></up-textarea>
</up-form-item>
<up-form-item label="分类" prop="goodsInfo.sex" :borderBottom="false">
<view class=""
style="padding: 12rpx 18rpx; border-radius: 7px; border: 1rpx solid #e2e2e2; width: 100%; ">
<view class="" @click="showClass=true"
style="display: flex; height: 48rpx; align-items: center; justify-content: space-between;">
<view class="" :style="{ 'color':goodsInfo.class? '#000':'#c0c4cc' } ">
{{ goodsInfo.class?goodsInfo.class:'请选择分类'}}
</view>
<view class="">
<up-icon name="arrow-right" size="14" color='#666666' :bold='true'></up-icon>
</view>
</view>
</view>
</up-form-item>
<up-form-item label="状态" prop="goodsInfo.sex" :borderBottom="false">
<up-radio-group v-model="goodsInfo.status" placement="row">
<up-radio :customStyle="{marginBottom: '8px'}" v-for="(item, index) in radiolist" :key="index"
:label="item.name" :name="item.value" @change="radioChange">
</up-radio>
</up-radio-group>
</up-form-item>
<up-form-item label="标签" :borderBottom="false">
<view v-for="(item, index) in tabsList" @click="clickTab(index)" :key="index"
:class="{active:!goodsInfo.tabs.includes(index),actived:goodsInfo.tabs.includes(index)}">
{{item}}
</view>
</up-form-item>
</up-form>
</view>
<view class="" style="width: 100%; height: 200rpx;">
</view>
<!-- 底部操作按钮 -->
<view class="action-buttons">
<u-button type="primary" @click="Service.GoPageBack()" :custom-style="ButtonStyle">取消</u-button>
<u-button type="primary" @click="save()" :custom-style="ButtonStyle">保存</u-button>
</view>
<up-picker :show="showClass" @cancel="showClass=!showClass" @confirm="confirmClass"
:columns="columns"></up-picker>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
let goodsInfo = ref({
name: "",
prince: '',
des: "",
class: '',
status: '',
tabs: []
})
let showClass = ref(false)
const columns = ref([
['中国', '美国', '日本']
]);
const radiolist = ref([
{
name: '上架中',
value: '0'
},
{
name: '已下架',
value: '1'
}
]);
let tabsList = ref([
'新品',
'热销',
'推荐',
'折扣',
'特惠'
])
const ButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginLeft: '20rpx'
});
onLoad(() => {
});
onShow(() => {
});
const confirmClass = (e) => {
goodsInfo.value.class = e.value[0]
showClass.value = !showClass.value
}
const radioChange = (e) => {
goodsInfo.value.status = e
}
const clickTab = (e : any) => {
if (goodsInfo.value.tabs.includes(e)) {
let a = goodsInfo.value.tabs.findIndex((x) => {
return x == e
})
goodsInfo.value.tabs.splice(a, 1)
return
}
goodsInfo.value.tabs.push(e)
}
const save=()=>{
Service.GoPageBack()
}
const uploadFImg = () => {
uni.chooseImage({
count: 1, // 最多选择3张图片
sizeType: ['original', 'compressed'], // 支持原图和压缩图
sourceType: ['album', 'camera'], // 可从相册选择或使用相机拍照
success: function (res) {
let path = res.tempFiles[0].path
// Service.uploadH5(path, 'Avatar', data => {
// userInfo.value.headImg = data.split(',')[2].split(':')[1].split('"')[1]
// })
},
fail: function (err) {
console.error('选择失败:', err.errMsg);
}
})
}
</script>
<style lang="scss">
page {
background-color: #f6f6f6;
}
.active {
color: #b5b5b5;
border: 1rpx solid #b5b5b5;
margin-right: 15rpx;
border-radius: 23rpx;
padding: 5rpx 16rpx;
width: fit-content;
}
.actived {
color: var(--nav-mian);
border: 1rpx solid var(--nav-mian);
margin-right: 15rpx;
border-radius: 23rpx;
padding: 5rpx 16rpx;
width: fit-content;
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,9 @@
export class BaseConfig {
// protected static servesUrl: string = "http://192.168.0.142:5002";//线下
protected static servesUrl: string = "https://vp.xypays.cn";
protected static imgUrl : string = "https://vp.clouds.xypays.cn";
protected static mediaUrl: string = "http://byc1.xypays.cn/";
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
protected static payuploadUrl: string = "https://vp.xypays.cn";
}

View File

@@ -0,0 +1,85 @@
{
"id": "t-cropper",
"displayName": "t-cropper图片裁剪插件",
"version": "1.0.8",
"description": "vue3图片裁剪插件头像裁剪、支持自定义尺寸、等比例缩放、拖动、图片翻转、剪切圆形/圆角图片,高性能裁剪图片插件",
"keywords": [
"图片裁剪",
",头像裁剪,缩放,旋转,拖动",
""
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "n",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

View File

@@ -0,0 +1,727 @@
/**
* 图片编辑器-手势监听
* 1. wxs 暂不支持 es6 语法
* 2. 支持编译到微信小程序、QQ小程序、app-vue、H5上uni-app 2.2.5及以上版本)
*/
/** 图片偏移量 */
var offset = { x: 0, y: 0 };
/** 图片缩放比例 */
var scale = 1;
/** 图片最小缩放比例 */
var minScale = 1;
/** 图片旋转角度 */
var rotate = 0;
/** 触摸点 */
var touches = [];
/** 图片布局信息 */
var img = {};
/** 系统信息 */
var sys = {};
/** 裁剪区域布局信息 */
var area = {};
/** 触摸行为类型 */
var touchType = '';
/** 操作角的位置 */
var activeAngle = 0;
/** 裁剪区域布局信息偏移量 */
var areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
/** 容错值 */
var fault = 0.000001;
/**
* 获取a、b两数中的最小正数
* @param a
* @param b
*/
function minimum(a, b) {
if (a > 0 && b < 0) return a;
if (a < 0 && b > 0) return b;
if (a > 0 && b > 0) return Math.min(a, b);
return 0;
}
/**
* 在容错访问内获取n近似值
* @param n
*/
function num(n) {
var m = parseFloat((n).toFixed(6));
return m === fault || m === -fault ? 0 : m;
}
/**
* 比较a值在容错值范围内是否等于b值
* @param a
* @param b
*/
function equalsByFault(a, b) {
return Math.abs(a - b) <= fault;
}
/**
* 比较a值在容错值范围内是否小于b值
* @param a
* @param b
*/
function lessThanByFault(a, b) {
var c = a - b;
return c < 0 ? c < -fault : c < fault;
}
/**
* 验证并获取有效最大值
* @param v
* @param max
* @param isInclude
* @param x
* @param y
* @param rate
* @returns
*/
function validMax(v, max, isInclude, x, y, rate) {
if(typeof max === 'number') {
if(isInclude && equalsByFault(max, y)) { // 宽高不等时x轴用y轴值要做等比例转换
var n = num(max * rate);
if (n <= x) return n; // 转化后值在x轴最大值范围内
return x; // 转化后值超出x轴最大值范围则用最大值
}
return max;
}
return v;
}
/**
* 计算两点间距
* @param {Object} touches 触摸点信息
*/
function getDistanceByTouches(touches) {
// 根据勾股定理求两点间距离
var a = touches[1].pageX - touches[0].pageX;
var b = touches[1].pageY - touches[0].pageY;
var c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
// 求两点间的中点坐标
// 1. a、b可能为负值
// 2. 在求a、b时如用touches[1]减touches[0]则求中点坐标也得用touches[1]减a/2、b/2
// 3. 同理在求a、b时也可用touches[0]减touches[1]则求中点坐标也得用touches[0]减a/2、b/2
var x = touches[1].pageX - a / 2;
var y = touches[1].pageY - b / 2;
return { c, x, y };
};
/**
* 修正取值
* @param {Object} a
* @param {Object} b
* @param {Object} c
* @param {Object} reverse 是否反向
*/
function correctValue(a, b, c, reverse) {
return num(reverse ? Math.max(Math.min(a, b), c) : Math.min(Math.max(a, b), c));
}
/**
* 旋转90°或270°时检查边界限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
* @param {Object} xReverse x是否反向
* @param {Object} yReverse y是否反向
*/
function checkRotateRange(e, xReverse, yReverse) {
var o = num((img.height - img.width) / 2); // 宽高差值一半
return {
x: correctValue(e.x, -img.height + o + area.width + area.left, area.left + o, xReverse),
y: correctValue(e.y, -img.width - o + area.height + area.top, area.top - o, yReverse)
};
}
/**
* 检查边界:限制 x、y 拖动范围,禁止滑出边界
* @param {Object} e 点坐标
*/
function checkRange(e) {
var r = rotate / 90 % 2;
if(r === 1) { // 因图片宽高可能不等,翻转 90° 或 270° 后图片宽高需反着计算,且左右和上下边界要根据差值做偏移
if (area.width === area.height) {
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
var isInclude = img.height < area.width && img.width < area.height; // 图片是否包含在裁剪区域内
if (img.width < area.height || img.height < area.width) {
if (area.width < area.height && img.width < img.height) {
return isInclude
? checkRotateRange(e, area.width < area.height, area.width < area.height)
: checkRotateRange(e, false, true);
}
if (area.height < area.width && img.height < img.width) {
return isInclude
? checkRotateRange(e, area.height < area.width, area.height < area.width)
: checkRotateRange(e, true, false);
}
}
if (img.height >= area.width && img.width >= area.height) {
return checkRotateRange(e, false, false);
}
if (isInclude) {
return area.height < area.width
? checkRotateRange(e, true, true)
: checkRotateRange(e, area.width < area.height, area.width < area.height);
}
if (img.height < area.width && !img.width < area.height) {
return checkRotateRange(e, true, false);
}
if (!img.height < area.width && img.width < area.height) {
return checkRotateRange(e, false, true);
}
return checkRotateRange(e, img.height < area.height, img.width < area.width);
}
return {
x: correctValue(e.x, -img.width + area.width + area.left, area.left, img.width < area.width),
y: correctValue(e.y, -img.height + area.height + area.top, area.top, img.height < area.height)
};
};
/**
* 变更图片布局信息
* @param {Object} e 布局信息
*/
function changeImageRect(e) {
offset.x += e.x || 0;
offset.y += e.y || 0;
var image = e.instance.selectComponent('.crop-image');
if(e.check && area.checkRange) { // 检查边界
var point = checkRange(offset);
if(offset.x !== point.x || offset.y !== point.y) {
offset = point;
}
}
// image.setStyle({
// width: img.width + 'px',
// height: img.height + 'px',
// transform: 'translate(' + offset.x + 'px, ' + offset.y + 'px) rotate(' + rotate +'deg)'
// });
var ox = (img.width - img.oldWidth) / 2;
var oy = (img.height - img.oldHeight) / 2;
image.setStyle({
width: img.oldWidth + 'px',
height: img.oldHeight + 'px',
transform: (img.gpu ? 'translateZ(0) ' : '') + 'translate(' + (offset.x + ox) + 'px, ' + (offset.y + oy) + 'px) rotate(' + rotate +'deg) scale(' + scale + ')'
});
e.instance.callMethod('dataChange', {
width: img.width,
height: img.height,
x: offset.x,
y: offset.y,
rotate: rotate
});
};
/**
* 变更裁剪区域布局信息
* @param {Object} e 布局信息
*/
function changeAreaRect(e) {
// 变更蒙版样式
var masks = e.instance.selectAllComponents('.crop-mask-block');
var maskStyles = [
{
left: 0,
width: (area.left + areaOffset.left) + 'px',
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.right + areaOffset.right) + 'px',
right: 0,
top: 0,
bottom: 0,
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: 0,
height: (area.top + areaOffset.top) + 'px',
'z-index': area.zIndex + 2
},
{
left: (area.left + areaOffset.left) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
top: (area.bottom + areaOffset.bottom) + 'px',
// height: (area.top - areaOffset.bottom + sys.offsetBottom) + 'px',
bottom: 0,
'z-index': area.zIndex + 2
}
];
var len = masks.length;
for (var i = 0; i < len; i++) {
masks[i].setStyle(maskStyles[i]);
}
// 变更边框样式
if(area.showBorder) {
var border = e.instance.selectComponent('.crop-border');
border.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
});
}
// 变更参考线样式
if(area.showGrid) {
var grids = e.instance.selectAllComponents('.crop-grid');
var gridStyles = [
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '1px 0 0 0',
left: (area.left + areaOffset.left) + 'px',
right: (area.right + areaOffset.right) + 'px',
top: (area.top + areaOffset.top + (area.height + areaOffset.bottom - areaOffset.top) * 2 / 3 - 0.5) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 1px 0 0',
top: (area.top + areaOffset.top) + 'px',
bottom: (area.bottom + areaOffset.bottom) + 'px',
left: (area.left + areaOffset.left + (area.width + areaOffset.right - areaOffset.left) * 2 / 3 - 0.5) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 3
}
];
var len = grids.length;
for (var i = 0; i < len; i++) {
grids[i].setStyle(gridStyles[i]);
}
}
// 变更四个伸缩角样式
if(area.showAngle) {
var angles = e.instance.selectAllComponents('.crop-angle');
var angleStyles = [
{
'border-width': area.angleBorderWidth + 'px 0 0 ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.top + areaOffset.top - area.angleBorderWidth) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px',
left: (area.left + areaOffset.left - area.angleBorderWidth) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
},
{
'border-width': '0 ' + area.angleBorderWidth + 'px ' + area.angleBorderWidth + 'px 0',
left: (area.right + areaOffset.right - area.angleSize) + 'px',
top: (area.bottom + areaOffset.bottom - area.angleSize) + 'px',
'z-index': area.zIndex + 3
}
];
var len = angles.length;
for (var i = 0; i < len; i++) {
angles[i].setStyle(angleStyles[i]);
}
}
// 变更圆角样式
if(area.radius > 0) {
var circleBox = e.instance.selectComponent('.crop-circle-box');
var circle = e.instance.selectComponent('.crop-circle');
var radius = area.radius;
if(area.width === area.height && area.radius >= area.width / 2) { // 圆形
radius = (area.width / 2);
} else { // 圆角矩形
if(area.width !== area.height) { // 限制圆角半径不能超过短边的一半
radius = Math.min(area.width / 2, area.height / 2, radius);
}
}
circleBox.setStyle({
left: (area.left + areaOffset.left) + 'px',
top: (area.top + areaOffset.top) + 'px',
width: (area.width + areaOffset.right - areaOffset.left) + 'px',
height: (area.height + areaOffset.bottom - areaOffset.top) + 'px',
'z-index': area.zIndex + 2
});
circle.setStyle({
'box-shadow': '0 0 0 ' + Math.max(area.width, area.height) + 'px rgba(51, 51, 51, 0.8)',
'border-radius': radius + 'px'
});
}
};
/**
* 缩放图片
* @param {Object} e 布局信息
*/
function scaleImage(e) {
var last = scale;
scale = Math.min(Math.max(e.scale + scale, minScale), img.maxScale);
if(last !== scale) {
img.width = num(img.oldWidth * scale);
img.height = num(img.oldHeight * scale);
// 参考问题有一个长4000px、宽4000px的四方形ABCDA点的坐标固定在(-2000,-2000)
// 该四边形上有一个点E坐标为(-100,-300)将该四方形复制一份并缩小到90%后,
// 新四边形的A点坐标为多少时可使新四边形的E点与原四边形的E点重合
// 预期效果:从图中选取某点(参照物)为中心点进行缩放,缩放时无论图像怎么变化,该点位置始终固定不变
// 计算方法:以相同起点先计算缩放前后两点间的距离,再加上原图像偏移量即可
e.x = num((e.x - offset.x) * (1 - scale / last));
e.y = num((e.y - offset.y) * (1 - scale / last));
changeImageRect(e);
return true;
}
return false;
};
/**
* 获取触摸点在哪个角
* @param {number} x 触摸点x轴坐标
* @param {number} y 触摸点y轴坐标
* @return {number} 角的位置0=无1=左上2=右上3=左下4=右下;
*/
function getToucheAngle(x, y) {
// console.log('getToucheAngle', x, y, JSON.stringify(area))
var o = area.angleBorderWidth; // 需扩大触发范围则把 o 值加大即可
if(y >= area.top - o && y <= area.top + area.angleSize + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 1; // 左上角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 2; // 右上角
}
} else if(y >= area.bottom - area.angleSize - o && y <= area.bottom + o) {
if(x >= area.left - o && x <= area.left + area.angleSize + o) {
return 3; // 左下角
} else if(x >= area.right - area.angleSize - o && x <= area.right + o) {
return 4; // 右下角
}
}
return 0; // 无触摸到角
};
/**
* 重置数据
*/
function resetData() {
offset = { x: 0, y: 0 };
scale = 1;
minScale = img.minScale;
rotate = 0;
};
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
function rotateImage(e, o, r) {
rotate = (rotate + r) % 360;
if(img.minScale >= 1 && area.checkRange) {
// 因图片宽高可能不等,翻转后图片宽高需足够填满裁剪区域
minScale = 1;
if(img.width < area.height) {
minScale = area.height / img.oldWidth;
} else if(img.height < area.width) {
minScale = area.width / img.oldHeight;
}
if(minScale !== 1) {
scaleImage({
instance: o,
scale: minScale - scale,
x: sys.windowWidth / 2,
y: (sys.windowHeight - sys.offsetBottom) / 2
});
}
}
// 由于拖动画布后会导致图片位置偏移,翻转时的旋转中心点需是图片区域+偏移区域的中心点
// 翻转x轴中心点 = (超出裁剪区域右侧的图片宽度 - 超出裁剪区域左侧的图片宽度) / 2
// 翻转y轴中心点 = (超出裁剪区域下方的图片宽度 - 超出裁剪区域上方的图片宽度) / 2
var ox = ((offset.x + img.width - area.right) - (area.left - offset.x)) / 2;
var oy = ((offset.y + img.height - area.bottom) - (area.top - offset.y)) / 2;
changeImageRect({
instance: o,
check: true,
x: -ox - oy,
y: -oy + ox
});
};
module.exports = {
/**
* 初始化:观察数据变更
* @param {Object} newVal 新数据
* @param {Object} oldVal 旧数据
* @param {Object} o 组件实例对象
*/
initObserver: function(newVal, oldVal, o, i) {
if(newVal) {
img = newVal.img;
sys = newVal.sys;
area = newVal.area;
minScale = img.minScale;
resetData();
img.src && changeImageRect({
instance: o,
x: (sys.windowWidth - img.width) / 2,
y: (sys.windowHeight - sys.offsetBottom - img.height) / 2
});
changeAreaRect({
instance: o
});
// console.log('initRect', JSON.stringify(newVal))
}
},
/**
* 鼠标滚轮滚动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
mousewheel: function(e, o) {
if(!img.src) return;
scaleImage({
instance: o,
check: true,
// 鼠标向上滚动时deltaY 固定 -100鼠标向下滚动时deltaY 固定 100
scale: e.detail.deltaY > 0 ? -0.05 : 0.05,
x: e.touches[0].pageX,
y: e.touches[0].pageY
});
},
/**
* 触摸开始
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchstart: function(e, o) {
if(!img.src) return;
touches = e.touches;
activeAngle = area.showAngle ? getToucheAngle(touches[0].pageX, touches[0].pageY) : 0;
if(touches.length === 1 && activeAngle !== 0) {
touchType = 'stretch'; // 伸缩裁剪区域
} else {
touchType = '';
}
// console.log('touchstart', JSON.stringify(e), activeAngle)
},
/**
* 触摸移动
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchmove: function(e, o) {
if(!img.src) return;
// console.log('touchmove', JSON.stringify(e), JSON.stringify(o))
if(touchType === 'stretch') { // 触摸四个角进行拉伸
var point = e.touches[0];
var start = touches[0];
var x = point.pageX - start.pageX;
var y = point.pageY - start.pageY;
if(x !== 0 || y !== 0) {
var maxX = num(area.width * (1 - area.minScale));
var maxY = num(area.height * (1 - area.minScale));
// console.log(x, y, maxX, maxY, offset, area)
touches[0] = point;
var r = rotate / 90 % 2;
var m = r === 1 ? num((img.height - img.width) / 2) : 0; // 宽高差值一半
var xCompare = r === 1 ? lessThanByFault(img.height, area.width) : lessThanByFault(img.width, area.width);
var yCompare = r === 1 ? lessThanByFault(img.width, area.height) : lessThanByFault(img.height, area.height)
var isInclude = xCompare && yCompare;
var isIntersect = area.checkRange && (xCompare || yCompare); // 图片是否包含在裁剪区域内
var isReverse = !isInclude || num((offset.x - area.left) / area.width) <= num((offset.y - area.top) / area.height) || (area.width > area.height && img.width < img.height && r === 1);
switch(activeAngle) {
case 1: // 左上角
x = num(x + areaOffset.left);
y = num(y + areaOffset.top);
if(x >= 0 && y >= 0) { // 有效滑动
var t = num(offset.y + m - area.top);
var l = num(offset.x - m - area.left);
// && (offset.x + img.width < area.right || offset.y + img.height < area.bottom)
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.top = y;
}
break;
case 2: // 右上角
x = num(x + areaOffset.right);
y = num(y + areaOffset.top);
if(x <= 0 && y >= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var t = num(offset.y + m - area.top);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((t >= 0) || (l >= 0))
? minimum(t, l)
: false;
// var max = isInclude && ((offset.x > 0 && offset.x + img.width <= area.right) || (offset.y > 0 && offset.y >= area.top))
// ? minimum(offset.y - area.top, area.right - offset.x - img.width)
// : false;
// console.log(offset.x, offset.y, img.width, img.height, area.top, area.right, m, max)
// console.log(offset.y + m - area.top, area.right + m - offset.x - w)
if(-x > y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(y > maxY) y = maxY;
x = num(-y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.top = y;
}
break;
case 3: // 左下角
x += num(x + areaOffset.left);
y += num(y + areaOffset.bottom);
if(x >= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.width : img.height);
var t = num(area.bottom - m - offset.y - w);
var l = num(offset.x - m - area.left);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(x > maxX) x = maxX;
y = num(-x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(-y * area.width / area.height);
}
areaOffset.left = x;
areaOffset.bottom = y;
}
break;
case 4: // 右下角
x = num(x + areaOffset.right);
y = num(y + areaOffset.bottom);
if(x <= 0 && y <= 0) { // 有效滑动
var w = (r === 1 ? img.height : img.width);
var h = (r === 1 ? img.width : img.height);
var t = num(area.bottom - offset.y - h - m);
var l = num(area.right + m - offset.x - w);
var max = isIntersect && ((l >= 0) || (t >= 0))
? minimum(t, l)
: false;
if(-x > -y && isReverse) { // 以x轴滑动距离为缩放基准
maxX = validMax(maxX, max, isInclude, l, t, area.width / area.height);
if(-x > maxX) x = -maxX;
y = num(x * area.height / area.width);
} else { // 以y轴滑动距离为缩放基准
maxY = validMax(maxY, max, isInclude, t, l, area.height / area.width);
if(-y > maxY) y = -maxY;
x = num(y * area.width / area.height);
}
areaOffset.right = x;
areaOffset.bottom = y;
}
break;
}
// console.log(x, y, JSON.stringify(areaOffset))
changeAreaRect({
instance: o,
});
// this.draw();
}
} else if (e.touches.length == 2) { // 双点触摸缩放
var start = getDistanceByTouches(touches);
var end = getDistanceByTouches(e.touches);
scaleImage({
instance: o,
check: !area.bounce,
scale: (end.c - start.c) / 100,
x: end.x,
y: end.y
});
touchType = 'scale';
} else if(touchType === 'scale') {// 从双点触摸变成单点触摸 / 从缩放变成拖动
touchType = 'move';
} else {
changeImageRect({
instance: o,
check: !area.bounce,
x: e.touches[0].pageX - touches[0].pageX,
y: e.touches[0].pageY - touches[0].pageY
});
touchType = 'move';
}
touches = e.touches;
},
/**
* 触摸结束
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
touchend: function(e, o) {
if(!img.src) return;
if(touchType === 'stretch') { // 拉伸裁剪区域的四个角缩放
// 裁剪区域宽度被缩放到多少
var left = areaOffset.left;
var right = areaOffset.right;
var top = areaOffset.top;
var bottom = areaOffset.bottom;
var w = area.width + right - left;
var h = area.height + bottom - top;
// 图像放大倍数
var p = scale * (area.width / w) - scale;
// 复原裁剪区域
areaOffset = { left: 0, right: 0, top: 0, bottom: 0 };
changeAreaRect({
instance: o,
});
scaleImage({
instance: o,
scale: p,
x: area.left + left + (1 === activeAngle || 3 === activeAngle ? w : 0),
y: area.top + top + (1 === activeAngle || 2 === activeAngle ? h : 0)
});
} else if (area.bounce) { // 检查边界并矫正,实现拖动到边界时有回弹效果
changeImageRect({
instance: o,
check: true
});
}
},
/**
* 顺时针翻转图片90°
* @param {Object} e 事件对象
* @param {Object} o 组件实例对象
*/
rotateImage: function(e, o) {
rotateImage(e, o, 90);
},
rotateImage90: function(e, o) {
rotateImage(e, o, 90)
},
rotateImage270: function(e, o) {
rotateImage(e, o, 270)
},
// 此处只用于对齐其他平台端的样式参数,防止异常,无作用
imageStyles: '',
maskStylesList: ['', '', '', ''],
borderStyles: '',
gridStylesList: ['', '', '', ''],
angleStylesList: ['', '', '', ''],
circleBoxStyles: '',
circleStyles: '',
}

View File

@@ -0,0 +1,12 @@
import { Service } from '@/Service/Service';
/*****用户订单*****/
class vpOrderService {
private static GetUserOrderListPath : string = '/Order/GetUserOrderList';
/*****获取用户订单列表*****/
static GetUserOrderList(page:number) {
var result = Service.Request(this.GetUserOrderListPath, 'GET', {page});
return result;
}
}
export { Service, vpOrderService };

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

View File

@@ -0,0 +1,476 @@
<template>
<view class="page">
<!-- 商品图片展示 -->
<view class="product-image-section">
<image :src="Service.GetMateUrlByImg(goodsInfo.img)" mode="aspectFill" class="product-image">
</image>
</view>
<!-- 商品信息 -->
<view class="product-info-section">
<text class="product-name">{{goodsInfo.name}}</text>
<text class="product-price">¥{{goodsInfo.price}}</text>
<text class="product-description">{{goodsInfo.brief}}</text>
</view>
<!-- 商家信息卡片 -->
<view class="shop-card-section">
<view class="shop-header">
<text class="shop-name">{{merchInfo.name}}</text>
<view class="shop-tags">
<view class="tag" style="background-color: #FF9500;">{{merchInfo.code=='Discounts'?'积分可用':'积分不可用'}}
</view>
</view>
</view>
<view class="shop-info">
<view class="info-item">
<u-icon name="map" size="21" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{merchInfo.address}}</text>
</view>
<view class="info-item">
<u-icon name="clock" size="21" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{merchInfo.time}}</text>
</view>
<!-- <view class="info-item">
<u-icon name="home" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">朝阳美食社区</text>
</view> -->
</view>
</view>
<!-- 同店推荐 -->
<view v-if="recommendations.length>0" class="recommendations-section">
<text class="section-title">同店推荐</text>
<view class="recommendations-list">
<view @click="Service.GoPage('/pages/goods/goodsDetail')" class="recommendation-item"
v-for="(item, index) in recommendations" :key="index">
<image :src="Service.GetMateUrlByImg(item.img)" mode="aspectFill" class="recommendation-image">
</image>
<view class="recommendation-info">
<text class="recommendation-name">{{ item.name }}</text>
<text class="recommendation-price">{{ item.price }}</text>
</view>
<u-icon name="arrow-right" size="28rpx" color="#999999" class="arrow-icon"></u-icon>
</view>
</view>
</view>
<up-loadmore v-if="recommendations.length>0" :status="status" />
<view class="" style="width: 100%; height: 150rpx;">
</view>
<!-- 底部操作按钮 -->
<view class="action-buttons">
<u-button type="primary" @click="handleViewLocation()" :custom-style="locationButtonStyle">查看位置</u-button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from "@/Service/Service"
import { onLoad } from '@dcloudio/uni-app';
import { vpMerchService } from '@/Service/vp/vpMerchService'
// 商品信息
interface Goods {
brief : string
img : string
name : string
price : number
}
// 商家数据类型
interface shop {
showImg : string
price : number
name : string
score : number
tag : string
merchId : string
address : string
phone : string
sale : number
lat : string
lon : string
time : string
code:string
}
let merchInfo = ref<shop>({
showImg: '',
price: 0,
name: '',
score: 0,
tag: '',
merchId: '',
address: '',
phone: '',
sale: 0,
lat: '',
lon: '',
time: '',
code:''
})
// 营业时间处理
let weekList = ref<Array<number>>([])
let openTime = ref()
let closeTime = ref()
let timefunc = ref(-1)
// 推荐商品数据类型
interface Recommendation {
goodsId : string;
name : string;
price : string;
img : string;
}
let goodsId = ref()
let status = ref()
let page = ref(0)
let goodsInfo = ref<Goods>({
brief: '',
img: '',
name: '',
price: 0
})
// 同店推荐商品数据
const recommendations = ref<Recommendation[]>([]);
// 联系商家按钮样式
const contactButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginRight: '20rpx'
});
// 查看位置按钮样式
const locationButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginLeft: '20rpx'
});
onLoad((data : any) => {
goodsId.value = data.goodsId
getData()
})
const getData = () => {
status.value = 'loadmore'
page.value = 1
recommendations.value = []
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpMerchService.GetGoodsInfo(goodsId.value, page.value).then(res => {
goodsInfo.value = res.data.goodsInfo
merchInfo.value = res.data.merchInfo
merchInfo.value.time = !res.data.merchInfo.busTime ? '' : timeCancle(res.data.merchInfo.busTime)
recommendations.value = res.data.merchGoods
let index = recommendations.value.findIndex((item) => {
return item.goodsId == goodsId.value
})
recommendations.value.splice(index, 1)
status.value = res.data.merchGoods.length == 10 ? 'loadmore' : 'nomore'
page.value++
})
}
// 处理联系商家
const handleContactShop = () => {
uni.makePhoneCall({
phoneNumber: merchInfo.value.phone, // 要拨打的电话号码
success: function () {
console.log('拨打电话成功');
},
fail: function (err) {
console.error('拨打电话失败', err);
}
});
};
// 处理查看位置
const handleViewLocation = () => {
uni.openLocation({
latitude: Number(merchInfo.value.lat),
longitude: Number(merchInfo.value.lon),
name: merchInfo.value.name,
address: merchInfo.value.address,
success: function (e) {
console.log(e);
},
fail: function (e) {
console.log(e);
}
})
};
// 营业时间处理
const timeCancle = (busTime : string) => {
let data = busTime.split('_')
openTime.value = data[1].split('-')[0]
closeTime.value = data[1].split('-')[1]
let timeData = data[0].split('-')
for (let i = 0; i < timeData.length; i++) {
if (timeData[i] == '0') {
timeData[i] = '7'
}
}
let time = ''
timeData.sort((a : any, b : any) => {
return a - b
})
timeData.map((item : any) => {
weekList.value.push(item == '7' ? (Number(6)) : Number(item - 1))
})
let timeIndex = weekList.value[0] - 0
let judgment = weekList.value.findIndex((item, index) => {
return item - index !== timeIndex
})
// 1是至 /0是全显示
if (judgment == -1) {
timefunc.value = 1
} else {
timefunc.value = 0
}
if (timefunc.value == 0) {
weekList.value.map((item) => {
time = time + '周' + chinese(item) + ' '
})
} else {
time = '周' + chinese((weekList.value[0])) + '至' + '周' + chinese((weekList.value[weekList.value.length - 1]))
}
time = time + ' ' + data[1]
return time
}
const chinese = (item : number) => {
if (item + 1 == 1) {
return '一'
}
if (item + 1 == 2) {
return '二'
} if (item + 1 == 3) {
return '三'
} if (item + 1 == 4) {
return '四'
} if (item + 1 == 5) {
return '五'
} if (item + 1 == 6) {
return '六'
} if (item + 1 == 7) {
return '日'
}
}
</script>
<style scoped lang="scss">
/* 商品图片展示 */
.product-image-section {
width: 100%;
height: 500rpx;
margin-bottom: 30rpx;
}
.product-image {
width: 100%;
height: 100%;
}
/* 商品信息 */
.product-info-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.product-name {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-price {
font-size: 40rpx;
color: #FF6600;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-description {
font-size: 28rpx;
color: #666666;
line-height: 40rpx;
}
/* 商家信息卡片 */
.shop-card-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.shop-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.shop-name {
flex: 1;
font-size: 32rpx;
color: #333333;
font-weight: bold;
margin-right: 20rpx;
}
.shop-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 24rpx;
color: #FFFFFF;
padding: 4rpx 16rpx;
border-radius: 16rpx;
}
.tag.new-shop {
background-color: #4CD964;
}
.tag.popular {
background-color: #FF9500;
}
.shop-info {
gap: 20rpx;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-icon {
margin-right: 16rpx;
}
.info-text {
font-size: 28rpx;
margin-left: 15rpx;
color: #666666;
}
/* 同店推荐 */
.recommendations-section {
padding: 0 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 30rpx;
}
.recommendations-list {
gap: 30rpx;
}
.recommendation-item {
display: flex;
align-items: center;
padding: 30rpx;
margin: 20rpx 0;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.recommendation-item:last-child {
border-bottom: none;
}
.recommendation-image {
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
margin-right: 24rpx;
}
.recommendation-info {
flex: 1;
}
.recommendation-name {
font-size: 32rpx;
color: #333333;
display: block;
margin-bottom: 12rpx;
}
.recommendation-price {
font-size: 30rpx;
color: #FF6600;
}
.arrow-icon {
margin-left: 16rpx;
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,496 @@
<template>
<view class="orders-page">
<!-- 页面加载中 - 显示骨架屏 -->
<view v-if="pageLoading" class="page-loading-wrapper">
<!-- Tab 切换栏骨架屏 -->
<view class="tab-bar">
<view class="skeleton-tab-item"></view>
<view class="skeleton-tab-item"></view>
<view class="skeleton-tab-item"></view>
</view>
<!-- 订单列表骨架屏 -->
<view class="orders-content">
<SkeletonOrderCard v-for="i in 5" :key="i" />
</view>
</view>
<!-- 页面加载完成 - 显示实际内容 -->
<view v-else>
<!-- Tab 切换栏 -->
<view class="tab-bar">
<view v-for="(tab, index) in tabs" :key="index" class="tab-item"
:class="{ active: currentTab === index }" @click="switchTab(index)">
<text class="tab-text">{{ tab.label }}</text>
<view v-if="currentTab === index" class="tab-indicator"></view>
</view>
</view>
<!-- 订单列表 -->
<view class="orders-content">
<view v-if="orderList.length > 0" class="orders-list">
<view v-for="order in orderList" @click="Service.GoPage('/pages/goods/orderDetail?orderId='+order.orderId)" :key="order.orderId" class="order-card" >
<!-- 店铺信息 -->
<view class="order-header">
<image class="shop-avatar" :src="Service.GetMateUrlByImg(order.merchLogo)"
mode="aspectFill" />
<view class="shop-info">
<text class="shop-name">{{ order.merchName }}</text>
<text class="order-time">{{ Service.formatDate(order.addTime,1) }}</text>
</view>
<view class="order-status status-completed">
<text class="status-text">已完成</text>
</view>
</view>
<!-- 订单详情 -->
<view class="order-details">
<view class="detail-row">
<text class="detail-label">订单金额</text>
<text class="detail-value amount">¥{{ Number(order.amount).toFixed(2) }}</text>
</view>
<view v-if="order.useIntegral > 0" class="detail-row">
<text class="detail-label">使用积分</text>
<text class="detail-value used-points">-{{ order.useIntegral }}</text>
</view>
<view v-if="order.getIntegral > 0" class="detail-row">
<text class="detail-label">获得积分</text>
<text class="detail-value points-text">+{{ order.getIntegral }}</text>
</view>
<view v-if="order.discount" class="detail-row">
<text class="detail-label">优惠券</text>
<text class="detail-value coupon-text">{{ '满'+JSON.parse(order.discount).needMoney+'减'+JSON.parse(order.discount).deductMoney }}</text>
</view>
<view class="detail-row">
<text class="detail-label">付款金额</text>
<text class="detail-value amount">¥{{Number(order.payAmount).toFixed(2) }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="orderList.length === 0" class="empty-state">
<text class="ri-file-list-line empty-icon"></text>
<text class="empty-text">暂无订单</text>
</view>
<up-loadmore v-else :status="status" />
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {
ref,
onMounted
} from 'vue'
import SkeletonOrderCard from '../../components/skeleton/skeleton-order-card.vue'
import {
onLoad, onReachBottom
} from '@dcloudio/uni-app'
import { vpOrderService, Service } from '@/Service/vp/vpOrderService'
// 当前Tab
const currentTab = ref(0)
// 加载状态
const pageLoading = ref(true)
// Tab 配置
const tabs = [{
label: '全部订单',
value: 0
},
{
label: '待评价',
value: 1
},
{
label: '退款',
value: 2
}
]
let page = ref(1)
let status = ref('nomore')
let orderList = ref<Array<any>>([])
onLoad(() => {
getData()
})
onReachBottom(()=>{
getList()
})
const getData = () => {
page.value = 1
status.value = 'loadmore'
orderList.value = []
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpOrderService.GetUserOrderList(page.value).then(res => {
if (res.code == 0) {
pageLoading.value = false
orderList.value = [...orderList.value, ...res.data.list]
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
}
})
}
// 切换Tab
const switchTab = (index:any) => {
currentTab.value = index
if(index==0){
getData()
}else{
orderList.value=[]
}
}
</script>
<style lang="scss" scoped>
.orders-page {
min-height: 100vh;
background: #F5F5F5;
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.nav-placeholder {
width: 48rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
/* Tab 切换栏 */
.tab-bar {
background: #FFFFFF;
display: flex;
align-items: center;
padding: 0 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24rpx 0;
position: relative;
}
.tab-text {
font-size: 28rpx;
color: #666666;
font-weight: 500;
transition: all 0.3s ease;
}
.tab-item.active .tab-text {
color: #FF6B00;
font-weight: 600;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
border-radius: 2rpx;
}
/* 订单内容区域 */
.orders-content {
padding: 20rpx;
padding-top: 24rpx;
}
/* 订单列表 */
.orders-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.order-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
/* 订单头部 */
.order-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #F5F5F5;
}
.shop-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.shop-name {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
.order-time {
font-size: 22rpx;
color: #999999;
}
.order-status {
padding: 8rpx 20rpx;
border-radius: 24rpx;
font-size: 22rpx;
font-weight: 500;
}
.order-status.status-completed {
background: #E8F5E9;
color: #4CAF50;
}
.order-status.status-pending {
background: #FFF4E6;
color: #FF6B00;
}
.order-status.status-refunding {
background: #E3F2FD;
color: #2196F3;
}
.order-status.status-refunded {
background: #FFEBEE;
color: #F44336;
}
/* 订单详情 */
.order-details {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 24rpx;
color: #999999;
}
.detail-value {
font-size: 26rpx;
color: #222222;
font-weight: 500;
}
.detail-value.amount {
font-size: 32rpx;
color: #FF6B00;
font-weight: 700;
}
.detail-value.points-text {
color: #4CAF50;
font-weight: 600;
}
.detail-value.used-points {
color: #F44336;
font-weight: 600;
}
.detail-value.coupon-text {
color: #FF6B00;
}
.detail-value.order-no {
font-size: 22rpx;
color: #999999;
font-weight: 400;
}
/* 空状态 */
.empty-state {
background: #FFFFFF;
border-radius: 16rpx;
padding: 120rpx 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.empty-icon {
font-size: 96rpx;
color: #CCCCCC;
}
.empty-text {
font-size: 26rpx;
color: #999999;
}
/* 底部导航栏 - 精致简约风格 */
.tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-around;
padding: 12rpx 0 calc(12rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
z-index: 999;
height: calc(90rpx + env(safe-area-inset-bottom));
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0;
position: relative;
height: 100%;
}
.tabbar-icon {
font-size: 44rpx;
color: #CCCCCC;
margin-bottom: 8rpx;
transition: all 0.3s ease;
line-height: 1;
}
.tabbar-text {
font-size: 22rpx;
color: #CCCCCC;
font-weight: 400;
transition: all 0.3s ease;
line-height: 1.2;
}
/* 激活状态 */
.tabbar-item.active .tabbar-icon {
color: #FF6B00;
transform: scale(1.1);
}
.tabbar-item.active .tabbar-text {
color: #FF6B00;
font-weight: 500;
}
/* 订单页头部骨架屏样式 */
.page-loading-wrapper {
min-height: 100vh;
background: #F5F5F5;
}
.skeleton-nav-title {
flex: 1;
height: 36rpx;
margin: 0 24rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
}
.skeleton-tab-item {
flex: 1;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading-white {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

View File

@@ -0,0 +1,245 @@
<template>
<view>
<view :style="{'height':topHeight+'rpx'}"
style=" position: fixed; top: 0; z-index: 2; width: 100%; background-color: #fff; ">
<view class="" :style="{'margin-top':top+'rpx','height':height+'rpx','line-height':height+'rpx'}"
style=" margin-left: 40rpx; display: flex; align-items: center;">
<img :src="Service.GetIconImg('/static/index/index/location.png')" style="width: 40rpx; height: 40rpx;"
alt="" />
<text style="margin-left: 15rpx; font-size: 26rpx; font-weight: 600; ">许昌市魏都区</text>
</view>
<!-- 内容 -->
<view class="" style="margin: 40rpx 20rpx">
<up-swiper imgMode='heightFix' :list="swiperList" height='140' @change="e => current = e.current"
:autoplay="false">
<template #indicator>
<view class="indicator">
<view class="indicator__dot" v-for="(item, index) in swiperList" :key="index"
:class="[index === current && 'indicator__dot--active']">
</view>
</view>
</template>
</up-swiper>
</view>
<view class=""
style=" display: flex; align-items: center; justify-content: space-around; margin: 20rpx 0; background-color: #fff; padding: 20rpx">
<view v-for="(item, index) in tabList" @click="chooseTab(index)"
style="display: flex; flex-direction: column; align-items: center; justify-content: center;"
:key="index">
<view class="" :class="{tabimgActive:index==tabCurrent,tabimg: index!=tabCurrent }"
style=" border-radius: 50%; display: flex; align-items: center; justify-content: center; height: 80rpx; width: 80rpx; ">
<img :src="Service.GetIconImg( index==tabCurrent? item.imged:item.img)"
style="width: 45rpx; height: 45rpx; "></img>
</view>
<view :class="{tabActivefont:index==tabCurrent,tabfont:index!=tabCurrent}"
style="font-size: 26rpx; margin-top: 15rpx;" class="">
{{item.name}}
</view>
</view>
</view>
<view class=""
style="display: grid; grid-template-columns: repeat(2,1fr); gap: 20rpx; margin-top: 40rpx; background-color: #fff; padding: 20rpx; ">
<view class="">
<image :src="Service.GetMateUrlByImg('/static/dele/dele1.jpg')" mode="widthFix" style="width: 100%;" alt="" />
</view>
<view class="">
<image :src="Service.GetMateUrlByImg('/static/dele/dele2.jpg')" mode="widthFix" style="width: 100%;" alt="" />
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onShow, onLoad } from '@dcloudio/uni-app';
import { Service } from "@/Service/Service"
// 导航栏
let topHeight = ref()
let height = ref()
let top = ref()
let current = ref(0)
let swiperList = ref(
[
'/static/dele/dele1.jpg',
'/static/dele/dele2.jpg'
]
)
let tabCurrent = ref(0)
let tabList = ref(
[
{
name: '美食',
img: '/static/index/index/food.png',
imged: '/static/index/index/fooded.png'
},
{
name: '饮品',
img: '/static/index/index/cofe.png',
imged: '/static/index/index/cofed.png'
},
{
name: '超市',
img: '/static/index/index/shop.png',
imged: '/static/index/index/shoped.png'
},
{
name: '美妆',
img: '/static/index/index/good.png',
imged: '/static/index/index/gooded.png'
},
{
name: '医疗',
img: '/static/index/index/medical.png',
imged: '/static/index/index/medicaled.png'
}
]
)
onShow(() => {
})
onLoad(() => {
let res = wx.getMenuButtonBoundingClientRect()
topHeight.value = (res.top + res.height + 5) * 2
height.value = res.height * 2
top.value = res.top * 2
})
const chooseTab = (e) => {
tabCurrent.value = e
}
</script>
<style lang="scss">
page {
background-color: #F5F5F5;
}
.indicator {
@include flex(row);
justify-content: center;
&__dot {
height: 6px;
width: 6px;
border-radius: 100px;
background-color: rgba(255, 255, 255, 0.35);
margin: 0 5px;
transition: background-color 0.3s;
&--active {
background-color: #ffffff;
}
}
}
.tabimgActive {
background-color: var(--nav-mian);
}
.tabimg {
background-color: #F5F5F5;
}
.tabActivefont {
color: var(--nav-mian);
}
.tabfont {
color: #333333
}
.tag {
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
font-size: 24rpx;
margin-left: 10rpx;
}
// 瀑布流
.demo-warter {
border-radius: 8px;
margin: 5px;
background-color: #ffffff;
padding: 8px;
position: relative;
}
.u-close {
position: absolute;
top: 32rpx;
right: 32rpx;
}
.demo-image {
width: 100%;
border-radius: 4px;
}
.demo-title {
font-size: 30rpx;
margin-top: 5px;
}
.demo-tag {
display: flex;
margin-top: 5px;
}
.demo-tag-owner {
color: #FFFFFF;
display: flex;
align-items: center;
padding: 4rpx 14rpx;
border-radius: 50rpx;
font-size: 20rpx;
line-height: 1;
}
.demo-tag-text {
margin-left: 10px;
border-radius: 50rpx;
line-height: 1;
padding: 4rpx 14rpx;
display: flex;
align-items: center;
border-radius: 50rpx;
font-size: 20rpx;
}
.demo-price {
font-size: 30rpx;
margin-top: 5px;
}
.demo-shop {
font-size: 22rpx;
margin-top: 5px;
}
</style>

View File

@@ -0,0 +1,406 @@
<template>
<view class="page">
<!-- 商品图片展示 -->
<view class="product-image-section">
<image :src="Service.GetMateUrlByImg(merchInfo.showImg)" mode="aspectFill" class="product-image">
</image>
</view>
<!-- 商品信息 -->
<view class="product-info-section">
<view class="shop-header">
<view class="shop-name">{{merchInfo.name}}</view>
<view v-if="merchInfo.tag" v-for="(tagItem,tagIndex) in merchInfo.tag.split(',') " :key='tagIndex' class="shop-tags">
<text class="tag" :style="{ 'background-color':tagIndex==0?'#4CD964':(tagIndex==1?'#FF9500':'#fff')}" >{{tagItem}}</text>
</view>
</view>
<view class="" style="display: flex; align-items: center;">
<view class="func-detail">
<text style="color: #FF6B35;">{{merchInfo.score}}</text>分
</view>
<view class="func-detail">
月销<text style="color: #FF6B35;">{{merchInfo.sale}}</text>
</view>
<view class="func-detail">
人均<text style="color: #FF6B35;">¥{{merchInfo.price}}</text>
</view>
</view>
</view>
<!-- 商家信息卡片 -->
<view class="shop-card-section">
<view class="shop-info">
<view class="" @click="handleViewLocation()"
style="display: flex; align-items: center; justify-content: space-between;">
<view class="info-item">
<u-icon name="map" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{merchInfo.address}}</text>
</view>
<view class="">
<u-icon name="arrow-right" size="18" color="#999999" class="info-icon"></u-icon>
</view>
</view>
<view class="" @click="handleContactShop()"
style="display: flex; align-items: center; justify-content: space-between;">
<view class="info-item">
<u-icon name="phone" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{merchInfo.phone}}</text>
</view>
<view class="">
<u-icon name="arrow-right" size="18" color="#999999" class="info-icon"></u-icon>
</view>
</view>
<view v-if="communityInfo" class="info-item">
<u-icon name="home" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{communityInfo}}</text>
</view>
</view>
</view>
<!-- 同店推荐 -->
<view v-if="recommendations.length>0" class="recommendations-section">
<text class="section-title">店内商品</text>
<view class="recommendations-list">
<view @click="Service.GoPage('/pages/goods/goodsDetail?goodsId='+item.goodsId)" class="recommendation-item"
v-for="(item, index) in recommendations" :key="index">
<image :src="Service.GetMateUrlByImg(item.img)" mode="aspectFill" class="recommendation-image"></image>
<view class="recommendation-info">
<text class="recommendation-name">{{ item.name }}</text>
<text class="recommendation-price">{{ item.price }}</text>
</view>
<u-icon name="arrow-right" size="28rpx" color="#999999" class="arrow-icon"></u-icon>
</view>
</view>
</view>
<up-loadmore v-if="recommendations.length>0" :status="status" />
<view class="" style="width: 100%; height: 150rpx;">
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from "@/Service/Service"
import { onLoad } from '@dcloudio/uni-app';
import { vpHomeService } from '@/Service/vp/vpHomeService'
// 商家数据类型
interface shop{
showImg : string
price:number
name:string
score:number
tag:string
merchId:string
address:string
phone:string
sale:number
lat:string
lon:string
}
// 推荐商品数据类型
interface Recommendation {
goodsId: string;
name : string;
price : number;
img : string;
}
let merchId = ref()
let page = ref(0)
let status = ref('loadmore')
let merchInfo=ref<shop>({
showImg : '',
price:0,
name:'',
score: 0,
tag: '',
merchId: '',
address: '',
phone: '',
sale: 0,
lat: '',
lon: '',
})
let communityInfo=ref()
// 同店推荐商品数据
const recommendations = ref<Recommendation[]>();
// 联系商家按钮样式
const contactButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginRight: '20rpx'
});
// 查看位置按钮样式
const locationButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginLeft: '20rpx'
});
onLoad((data : any) => {
merchId.value = data.merchId
getData()
})
const getData = () => {
status.value = 'loadmore'
page.value = 1
recommendations.value=[]
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpHomeService.GetMerchInfo(merchId.value,page.value).then(res=>{
merchInfo.value=res.data.merchInfo
communityInfo.value=res.data.communityInfo
recommendations.value=res.data.merchGoods
status.value = res.data.merchGoods.length == 10 ? 'loadmore' : 'nomore'
page.value++
})
}
// 处理联系商家
const handleContactShop = () => {
uni.makePhoneCall({
phoneNumber: merchInfo.value.phone, // 要拨打的电话号码
success: function () {
console.log('拨打电话成功');
},
fail: function (err) {
console.error('拨打电话失败', err);
}
});
};
// 处理查看位置
const handleViewLocation = () => {
wx.openLocation({
latitude: Number(merchInfo.value.lat),
longitude: Number(merchInfo.value.lon),
name: merchInfo.value.name,
address: merchInfo.value.address,
success: function (e) {
console.log(e);
},
fail: function (e) {
console.log(e);
}
})
};
</script>
<style scoped lang="scss">
/* 商品图片展示 */
.product-image-section {
width: 100%;
height: 500rpx;
margin-bottom: 30rpx;
}
.product-image {
width: 100%;
height: 100%;
}
/* 商家信息 */
.func-detail {
margin-right: 30rpx;
font-size: 26rpx;
}
.product-info-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.product-name {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-price {
font-size: 40rpx;
color: #FF6600;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-description {
font-size: 28rpx;
color: #666666;
line-height: 40rpx;
}
/* 商家信息卡片 */
.shop-card-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.shop-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
justify-content: space-between;
}
.shop-name {
font-size: 38rpx;
color: #333333;
font-weight: bold;
margin-right: 20rpx;
}
.shop-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 24rpx;
color: #FFFFFF;
padding: 4rpx 16rpx;
border-radius: 16rpx;
}
.tag.new-shop {
background-color: #4CD964;
}
.tag.popular {
background-color: #FF9500;
}
.shop-info {
gap: 20rpx;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-icon {
margin-right: 16rpx;
}
.info-text {
font-size: 28rpx;
margin-left: 15rpx;
color: #666666;
}
/* 同店推荐 */
.recommendations-section {
padding: 0 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 30rpx;
}
.recommendations-list {
gap: 30rpx;
}
.recommendation-item {
display: flex;
align-items: center;
padding: 30rpx;
margin: 20rpx 0;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.recommendation-item:last-child {
border-bottom: none;
}
.recommendation-image {
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
margin-right: 24rpx;
}
.recommendation-info {
flex: 1;
}
.recommendation-name {
font-size: 32rpx;
color: #333333;
display: block;
margin-bottom: 12rpx;
}
.recommendation-price {
font-size: 30rpx;
color: #FF6600;
}
.arrow-icon {
margin-left: 16rpx;
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<view>
积分商城
</view>
</template>
<script setup lang="ts">
import { onShow,onLoad } from "@dcloudio/uni-app";
onLoad(() => {
});
onShow(() => {
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,312 @@
<template>
<view class="favorites-page">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">我的收藏</text>
<view class="nav-placeholder"></view>
</view>
<!-- 收藏列表 -->
<view class="content">
<view v-if="collectList.length > 0" class="favorites-list">
<view v-for="item in collectList" :key="item.merchId" class="shop-card" @click="Service.GoPage('/pages/community/merchantDetail?merchId='+item.merchId)">
<!-- 店铺图片 -->
<image class="shop-image" :src="Service.GetMateUrlByImg(item.logo)" mode="aspectFill" />
<!-- 店铺信息 -->
<view class="shop-info">
<text class="shop-name">{{ item.name }}</text>
<view class="shop-meta">
<text class="ri-star-fill rating-icon"></text>
<text class="rating-text">{{ item.score }}分</text>
<text class="sales-text">月售{{ item.sale }}</text>
</view>
<view class="shop-tags">
<view v-for="(coupon, idx) in item.tips" :key="idx" :class="getTagClass(coupon)" style="font-size: 24rpx;"
class="shop-tag">
{{ coupon }}
</view>
</view>
</view>
<!-- 取消收藏按钮 -->
<view class="cancel-btn" @click.stop="cancelFavorite(item)">
<text :class="{ 'ri-heart-fill': item.state==1,'ri-heart-line':item.state!==1 }"
class="action-pill-icon"></text>
</view>
</view>
<up-loadmore :status="status" />
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="ri-heart-line empty-icon"></text>
<text class="empty-text">暂无收藏店铺</text>
<text class="empty-desc">去首页逛逛,收藏喜欢的店铺吧</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {onShow,onLoad,onReachBottom} from "@dcloudio/uni-app";
import {Service} from "@/Service/Service"
import { ref, computed } from "vue";
import { vpDiscountService } from "@/Service/vp/vpDiscountService"
import { vpUserService } from "@/Service/vp/vpUserService"
let collectList = ref<Array<any>>([])
let status = ref<string>('loadmore')
let pageNo = ref<number>(1)
let longitude=ref(0)
let latitude=ref(0)
onLoad(()=>{
getLocation()
})
onReachBottom(()=>{
getList()
})
// 返回
const goBack = () => {
Service.GoPageBack()
}
const getLocation = () => {
uni.getLocation({
type: 'wgs84',
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
getData()
},
fail: function (e) {
console.log(e);
}
});
}
const getData=()=>{
pageNo.value=1
status.value='loadmore'
collectList.value=[]
getList()
}
const getList=()=>{
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpUserService.GetUserCollectList(longitude.value,latitude.value,pageNo.value).then(res=>{
if(res.code==0){
collectList.value=[...collectList.value,...res.data.collectList]
status.value=res.data.collectList.length==10?'loadmore':'nomore'
pageNo.value++
}
})
}
// 根据标签文本获取样式类
const getTagClass = (tagText : string) => {
const tagMap = {
'可用积分': 'tag-points-available',
'可用券': 'tag-coupon'
}
return tagMap[tagText] || 'tag-points'
}
// 取消收藏
const cancelFavorite = (item:any) => {
uni.showModal({
title: '取消收藏',
content: `确定取消收藏吗?`,
success: (res) => {
if (res.confirm) {
vpUserService.CollectMerch(item.merchId).then(res=>{
if(res.code==0){
getData()
}else{
Service.Msg(res.msg)
}
})
}
}
})
}
</script>
<style lang="scss" scoped>
.favorites-page {
min-height: 100vh;
background: #F5F5F5;
}
.action-pill-icon {
font-size: 28rpx;
color: #FF6B00;
line-height: 1;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 24rpx 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* 内容区域 */
.content {
padding: 20rpx;
}
/* 收藏列表 */
.favorites-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.shop-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 20rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
position: relative;
}
.shop-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.shop-name {
font-size: 32rpx;
font-weight: 600;
color: #222222;
}
.shop-meta {
display: flex;
align-items: center;
gap: 8rpx;
}
.rating-icon {
font-size: 24rpx;
color: #FFB800;
}
.rating-text {
font-size: 24rpx;
color: #FF6B00;
font-weight: 600;
}
.sales-text {
font-size: 22rpx;
color: #999999;
}
.shop-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 20rpx;
color: #666666;
background: #F5F5F5;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.cancel-btn {
width: 64rpx;
height: 64rpx;
background: #FFEBEE;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 16rpx;
flex-shrink: 0;
}
.cancel-icon {
font-size: 32rpx;
color: #F44336;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
gap: 16rpx;
}
.empty-icon {
font-size: 120rpx;
color: #CCCCCC;
}
.empty-text {
font-size: 28rpx;
color: #999999;
font-weight: 500;
}
.empty-desc {
font-size: 24rpx;
color: #CCCCCC;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,29 @@
import { Service } from '@/Service/Service';
/*****地址*****/
class vpMerchService {
private static GetMerchListPath: string = '/Merch/GetMerchList';
/*****商家列表*****/
static GetMerchList(assId: string,comId:string,lon:number,lat:number,page:number) {
var result = Service.Request(this.GetMerchListPath, 'GET', {assId,comId,lon,lat,page});
return result;
}
private static GetMerchInfoPath: string = '/Merch/GetMerchInfo';
/*****店铺详情*****/
static GetMerchInfo(merchId: string,page:number) {
var result = Service.Request(this.GetMerchInfoPath, 'GET', {merchId,page});
return result;
}
private static GetGoodsInfoPath: string = '/Merch/GetGoodsInfo';
/*****商品详情*****/
static GetGoodsInfo(goodsId: string,page:number) {
var result = Service.Request(this.GetGoodsInfoPath, 'GET', {goodsId,page});
return result;
}
}
export { Service, vpMerchService };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
<template>
<view style="padding: 20rpx; ">
<!-- 当前积分区域 -->
<view class="points-detail-section" style="margin-bottom: 20rpx;">
<view class="" style="display: flex; align-items: center; justify-content: space-between;">
<view class="" style="font-size: 30rpx; color: #999;">
积分余额
</view>
<view class="" style="color: red; font-weight: bold; font-size: 34rpx; ">
{{ integral }}
</view>
</view>
<view class="" style="margin-top: 20rpx;">
<u-button type="primary" @click="Service.GoPage('/pages/userFunc/withdrow')"
:custom-style="contactButtonStyle">立即提现</u-button>
</view>
</view>
<!-- 积分明细 -->
<view class="points-detail-section">
<text class="section-title">积分明细</text>
<view class="points-list">
<view class="points-item" v-for="(item, index) in pointsList" :key="index">
<text :class="['points-number', item.code=='收入' ? 'positive' : 'negative']">
{{ item.code=='收入' ? '+' : '-' }}{{ item.amount }}
</text>
<view class="" style="margin-left: 20rpx;">
<view class="points-type" style="font-size: 26rpx; font-weight: bold; ">{{ item.name }}</view>
<view class="points-date" style="margin-top: 4rpx;">{{ Service.formatDate(item.addTime,1) }}
</view>
</view>
</view>
</view>
<view v-if="pointsList.length==0" class="" style=" font-size: 28rpx; text-align: center;padding: 10rpx 0;">
暂无记录
</view>
<up-loadmore v-if="pointsList.length!==0" :status="status" />
</view>
<!-- <scroll-view scroll-y="true" class="scroll-Y" >
<text class="section-title">积分明细</text>
<view class="points-list">
<view class="points-item" v-for="(item, index) in pointsList" :key="index">
<text :class="['points-number', item.code=='收入' ? 'positive' : 'negative']">
{{ item.code=='收入' ? '+' : '-' }}{{ item.amount }}
</text>
<view class="" style="margin-left: 20rpx;">
<view class="points-type" style="font-size: 26rpx; font-weight: bold; ">{{ item.name }}</view>
<view class="points-date" style="margin-top: 4rpx;">{{ Service.formatDate(item.addTime,1) }}
</view>
</view>
</view>
</view>
<view v-if="pointsList.length==0" class="" style=" font-size: 28rpx; text-align: center;padding: 10rpx 0;">
暂无记录
</view>
<up-loadmore v-if="pointsList.length!==0" :status="status" />
</scroll-view> -->
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { vpUserService } from '@/Service/vp/vpUserService'
import { Service, vpMerchService } from '@/Service/vp/vpMerchService'
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
// 积分明细数据类型
interface PointsItem {
amount : number
code : string
name : string
addTime : string
}
// 按钮样式
const contactButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginRight: '20rpx'
});
let integral = ref(0)
let status = ref('loadmore')
let page = ref(1)
// 积分明细数据
const pointsList = ref<PointsItem[]>([
]);
onLoad(() => {
getUseraccInfo()
})
onReachBottom(() => {
console.log(1111);
getList()
})
const getUseraccInfo = () => {
status.value = 'loadmore'
page.value = 1
pointsList.value = []
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpMerchService.GetMerchAccInfo(page.value).then(res => {
integral.value = res.data.merchAcc.integral
pointsList.value = [...pointsList.value, ...res.data.merchAccLog]
status.value = res.data.merchAccLog.length == 10 ? 'loadmore' : 'nomore'
page.value++
})
}
</script>
<style lang="scss">
page {
background-color: #f6f6f6;
}
/* 积分明细 */
.points-detail-section {
background-color: #ffffff;
padding: 40rpx 30rpx;
border-radius: 20rpx;
}
.points-list {
gap: 36rpx;
}
.points-item {
display: flex;
align-items: center;
margin: 20rpx 0;
}
.points-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.points-type {
font-size: 32rpx;
color: #333333;
flex: 1;
}
.points-number {
font-size: 32rpx;
font-weight: bold;
margin-right: 24rpx;
min-width: 80rpx;
text-align: right;
}
.points-number.positive {
color: #4CD964;
}
.points-number.negative {
color: #FF4D4F;
}
.points-date {
font-size: 28rpx;
color: #999999;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -0,0 +1,735 @@
<template>
<view class="" style="padding: 30rpx; height: 100vh; overflow: auto; ">
<view class="" style="position: relative;">
<image v-if="!storeData.photo" :src="Service.GetIconImg('/static/userFunc/null.png')"
style="border-radius: 20rpx; width: 750rpx; height: 500rpx; " mode="aspectFit" alt="" />
<image v-else :src="Service.GetMateUrlByImg(storeData.photo)"
style="border-radius: 20rpx; width: 750rpx; height: 500rpx; " mode="aspectFill" alt="" />
<view @click="uploaduserImg(750,500,'MerchShow','showImg'),imgType='logo'"
style=" display: flex; align-items: center; justify-content: center; border-radius: 50%; width: 70rpx; height: 70rpx; position: absolute; bottom: 24rpx; right: 12rpx; background-color: #F97316; ">
<img :src="Service.GetIconImg('/static/userFunc/photo-store.png')"
style="width: 40rpx; height: 40rpx; " alt="" />
</view>
</view>
<view class=""
style=" margin-top: 20rpx; padding: 20rpx;border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #E2e2e2; ">
<up-form labelWidth='300rpx' :labelStyle="{'font-weight':600}" labelPosition="top" :model="storeData"
ref="form1">
<up-form-item label="店铺名称: " :borderBottom="true" ref="item1">
<up-input v-if="!upImgShow" v-model="storeData.name" placeholder="请输入店铺名称" border="none"></up-input>
</up-form-item>
<up-form-item @click="showStoreClass=true" label="店铺分类: " :borderBottom="true" ref="item1">
<view class="" >
<view v-if="!storeData.storeClass" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择店铺分类 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else class="">
{{storeData.storeClass}}
</view>
</view>
</up-form-item>
<up-form-item label="店铺地址: " @click="chooseAddress()" :borderBottom="true" ref="item1">
<view class="">
<view v-if="!storeData.province" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择地址 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else class="">
{{storeData.province+storeData.city+storeData.county}}
</view>
</view>
</up-form-item>
<up-form-item label="详细地址: " :borderBottom="true" ref="item1">
<up-input v-if="!upImgShow" v-model="storeData.address" placeholder="请输入您的详细地址"
border="none"></up-input>
</up-form-item>
<up-form-item label="平均消费金额: " :borderBottom="true" ref="item1">
<up-input v-if="!upImgShow" v-model="storeData.price" placeholder="请输入您的平均消费金额"
border="none"></up-input>
</up-form-item>
<up-form-item label="联系电话: " :borderBottom="true" ref="item1">
<up-input v-if="!upImgShow" v-model="storeData.phone" placeholder="请输入您的联系电话"
border="none"></up-input>
</up-form-item>
<up-form-item label="营业时间: " :borderBottom="true" ref="item1">
<view class="">
<view v-if="!storeData.time" @click="showDate=!showDate" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择营业时间 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showDate=!showDate" class="">
{{storeData.time}}
</view>
</view>
</up-form-item>
</up-form>
</view>
<!-- <view class=""
style="margin-top: 20rpx; padding: 20rpx;border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #E2e2e2; ">
<view style="display: flex; align-items: center; justify-content: space-between;" class="">
<view class="" style="font-weight: 600;">
店铺标签
</view>
<view class="">
<up-icon @click="showTagfunc()" name="plus" color="#000" size="16" :bold='true'></up-icon>
</view>
</view>
<view class="" style="display: grid; grid-template-columns: repeat(4,1fr); ">
<view class="tag" v-for="(item,index) in tagList" :key="index"
style=" position: relative; margin-top: 15rpx; padding: 10rpx 20rpx; border-radius: 8rpx; margin-right: 15rpx; background-color: var(--nav-mian); color: #fff; ">
{{item}}
<view class="" @click="deleTag(index)" style="position: absolute; right: -10rpx; top: -8rpx; " >
<up-icon size="12" :bold='true' color=" var(--nav-mian)" name="close"></up-icon>
</view>
</view>
</view>
</view> -->
<!-- <view class=""
style="margin-top: 20rpx; padding: 20rpx;border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #E2e2e2; ">
<view style="display: flex; align-items: center; justify-content: space-between;" class="">
<view class="" style="font-weight: 600;">
所属社区
</view>
<view class="">
</view>
</view>
<view class="" style="padding: 15rpx 0;">
<view v-if="!storeData.community" @click="showCommunity=!showCommunity" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择所属社区 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showCommunity=!showCommunity" class="">
{{storeData.community}}
</view>
</view>
</view> -->
<view class="section" style="margin-top: 20rpx;" >
<view style="font-size: 34rpx; font-weight: 600; margin-bottom: 20rpx;">商家展示图(最多三张)</view>
<view class="uploader-wrapper" style="">
<view v-if="storeData.imgs.length<3" @click="uploaduserImg(414,248,'StoreGoodsImg','轮播图'),imgType='img'"
class="upload-slot" >
<up-icon name="plus-circle-fill" size="30" color="#fa6400"></up-icon>
<text>点击上传图片</text>
</view>
<view v-if="storeData.imgs.length>0" v-for="(imgItem,imgIndex) in storeData.imgs" :key="imgIndex"
class="upload-slot"
style=" position: relative; overflow: hidden; margin-top: 15rpx; ">
<image :src="Service.GetMateUrlByImg(imgItem)" style="width: 100%; height: 100%; "
mode=""></image>
<view class="" @click="deleImg(imgIndex)"
style="position: absolute; top: 20rpx; right: 20rpx; ">
<up-icon name="close" bold="true" color="var(--nav-mian)" size="20"></up-icon>
</view>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 200rpx; ">
</view>
</view>
<view class="" @click="save()"
style=" z-index: 1000; width: 100%; background-color: #fff; position: fixed; bottom: 0; left: 0; padding: 20rpx; ">
<up-button shape='circle' color='#FF6B35' text="保存修改"></up-button>
</view>
<!-- 弹窗 -->
<up-popup :show="showDate">
<view style="width: 100%; padding: 30rpx; ">
<view class="" style="display: flex; align-items: center; justify-content: space-between;">
<view class="">
</view>
<up-icon @click="showDate=!showDate" name="close" color="#000" size="18"></up-icon>
</view>
<up-form labelWidth='180rpx' :labelStyle="{'font-weight':600}" labelPosition="top" :model="storeData"
ref="form1">
<up-form-item label="营业时间: ">
<view class="" style=" width: 100%; display: grid; grid-template-columns: repeat(4,1fr);">
<view v-for="(item,index) in 7" @click="chooseWeek(index)"
style=" margin-top: 15rpx; font-size: 32rpx; padding: 10rpx 30rpx;border-radius: 14rpx;"
:class="{ tab:weekList.findIndex(item => item == index)===-1,tabActive: weekList.findIndex(item => item == index)!=-1 }"
:key="index" class="tag">
周{{chinese(index)}}
</view>
</view>
</up-form-item>
<up-form-item label="开业时间: " labelPosition="left">
<view v-if="!openTime" @click="showOpenTime=!showOpenTime" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择开业时间 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showOpenTime=!showOpenTime" class="">
{{openTime}}
</view>
</up-form-item>
<up-form-item label="结业时间: " labelPosition="left">
<view v-if="!closeTime" @click="showCloseTime=!showCloseTime" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择开业时间 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showCloseTime=!showCloseTime" class="">
{{closeTime}}
</view>
</up-form-item>
</up-form>
<view class="" style="margin-top: 20rpx;">
<up-button @click="dateconfiom" text="确认"></up-button>
</view>
<up-datetime-picker @confirm='showOpenTime=!showOpenTime' @cancel="showOpenTime=!showOpenTime"
:show="showOpenTime" v-model="openTime" mode="time"></up-datetime-picker>
<up-datetime-picker @confirm='showCloseTime=!showCloseTime' @cancel="showCloseTime=!showCloseTime"
:show="showCloseTime" v-model="closeTime" mode="time"></up-datetime-picker>
</view>
</up-popup>
<up-popup :show="showTag">
<view style="width: 100%; padding: 30rpx; ">
<view class="" style="display: flex; align-items: center; justify-content: space-between;">
<view class="">
</view>
<up-icon @click="showTag=!showTag" name="close" color="#000" size="18"></up-icon>
</view>
<view class="">
<view style="display: flex; align-items: center; justify-content: space-between;" class="">
<view class="">
店铺标签
</view>
<view class="">
</view>
</view>
<view class="" style="margin: 20rpx 0;">
<up-input placeholder="请输入标签" border="bottom" v-model="tag"></up-input>
</view>
<view class="" style="margin-top: 20rpx;">
<up-button @click="addTag()" text="确认"></up-button>
</view>
</view>
</view>
</up-popup>
<!-- 社区 -->
<up-picker :show="showCommunity" @confirm="communityFunc" @cancel='showCommunity=!showCommunity' :columns="columns"
keyName="name" valueName="name"></up-picker>
<up-picker :show="showStoreClass" @confirm="storeClassFunc" @cancel='showStoreClass=!showStoreClass' :columns="storeColumns"
keyName="name" valueName="assortId"></up-picker>
<ImageCropperFunc style="z-index: 200;" :show="upImgShow" :url="upImgUrl" :imgName="imgName" :width="upImgwidth"
:height="upImgheight" :upType="upImgType" :retFun="upImgData">
</ImageCropperFunc>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
import { vpCommunityService } from '@/Service/vp/vpCommunityService'
import ImageCropperFunc from "@/components/ImageCropper"
import { vpMerchService } from '@/Service/vp/vpMerchService'
interface data {
photo : string,
name : string,
address : string,
phone : string,
time : string,
community : string
comId : string
province : string
city : string
county : string
latitude : number
longitude : number
price:any
assortId:any
storeClass:any
imgs:Array<any>
}
let storeData = ref<data>({
assortId:'',
storeClass:'',
price:null,
photo: '',
name: '',
address: '',
phone: '',
time: '',
community: '',
comId: '',
province: '',
city: '',
county: '',
latitude: 0,
longitude: 0,
imgs:[]
})
// 时间
let showDate = ref(false)
let weekList = ref<Array<number>>([])
let openTime = ref()
let showOpenTime = ref(false)
let closeTime = ref()
let showCloseTime = ref(false)
// 时间方式
let timefunc = ref(-1)
// 照片类型logo,展示图)
let imgType=ref('')
// 标签
let showTag = ref(false)
let tag = ref()
let tagList = ref<Array<string>>([])
// 社区
let showCommunity = ref(false)
const columns = ref([]);
// 店铺分类
let showStoreClass = ref(false)
const storeColumns = ref<Array<any>>([]);
// 裁剪图片参数
let upImgShow = ref<boolean>(false)
let upImgUrl = ref<string>('')
let upImgwidth = ref<number>(0)
let upImgheight = ref<number>(0)
let upImgType = ref<string>('')
let imgName = ref<string>('')
onLoad(() => {
getStoreClass()
});
onShow(() => {
});
const getData=()=>{
vpMerchService.GetMyMerchInfo().then(res=>{
storeData.value.address=res.data.merchInfo.address
storeData.value.city=res.data.merchInfo.city
// storeData.value.comId=res.data.merchInfo.communityId
// columns.value[0].map((item)=>{
// if(item.communityId==res.data.merchInfo.communityId){
// storeData.value.community=item.name
// }
// })
storeData.value.assortId=res.data.merchInfo.assId
storeColumns.value[0].map(item=>{
if(item.assortId==res.data.merchInfo.assId){
storeData.value.storeClass= item.name
}
})
storeData.value.county=res.data.merchInfo.county
storeData.value.latitude=res.data.merchInfo.lat
storeData.value.longitude=res.data.merchInfo.lon
storeData.value.name=res.data.merchInfo.name
storeData.value.phone=res.data.merchInfo.phone
storeData.value.photo=res.data.merchInfo.logo
storeData.value.imgs=JSON.parse(res.data.merchInfo.showImg)
storeData.value.price=res.data.merchInfo.price
storeData.value.province=res.data.merchInfo.province
storeData.value.time=!res.data.merchInfo.busTime?'': timeCancle(res.data.merchInfo.busTime)
tagList.value=JSON.parse(res.data.merchInfo.tag)==null?new Array():JSON.parse(res.data.merchInfo.tag)
})
}
const getStoreClass=()=>{
vpMerchService.GetMerchAssort().then(res=>{
storeColumns.value[0]=res.data.assortList
getData()
})
}
// 商家分类
const storeClassFunc = (e : any) => {
showStoreClass.value=false
storeData.value.storeClass = e.value[0].name
storeData.value.assortId = e.value[0].assortId
}
const getCommunityList = () => {
vpCommunityService.GetCommunityList().then(res => {
columns.value[0] = res.data.comList
getData()
})
}
const deleImg=(index:any)=>{
storeData.value.imgs.splice(index,1)
}
// 社区
const communityFunc = (e : any) => {
showCommunity.value = !showCommunity.value
storeData.value.community = e.value[0].name
storeData.value.comId = e.value[0].communityId
}
// 标签
const showTagfunc = () => {
showTag.value = !showTag.value
tag.value = ''
}
const addTag = () => {
if (!tag.value) {
Service.Msg('请输入标签内容')
return
}
tagList.value.push(tag.value)
showTag.value = !showTag.value
}
const deleTag=(index:number)=>{
tagList.value.splice(index,1)
}
// 选择照片
const uploaduserImg = (width : any, height : any, Type : any, Name : any) => {
uni.chooseImage({
count: 1, // 最多选择1张图片
sizeType: ['original', 'compressed'], // 支持原图和压缩图
sourceType: ['album', 'camera'], // 可从相册选择或使用相机拍照
success: function (res) {
let path = res.tempFiles[0].path
upImgUrl.value = path
upImgwidth.value = width
upImgheight.value = height
upImgType.value = Type
imgName.value = Name
upImgShow.value = true
},
fail: function (err) {
console.error('选择失败:', err.errMsg);
}
})
}
const upImgData = (e : any) => {
console.log(e);
upImgShow.value = false
if(imgType.value=='logo'){
storeData.value.photo = e.url
}else{
storeData.value.imgs.push(Service.GetMateUrlByImg(e.url))
console.log(storeData.value.imgs);
}
}
// 地址
const chooseAddress = () => {
wx.chooseLocation({
success: res => {
storeData.value.province = res.address.split('省')[0] + '省'
storeData.value.city = res.address.split('省')[1].split('市')[0] + '市'
storeData.value.county = res.address.split('省')[1].split('市')[1].split('区')[0] + '区'
storeData.value.address = res.address.split('省')[1].split('市')[1].split('区')[1]
storeData.value.longitude = res.longitude
storeData.value.latitude = res.latitude
}
})
}
const chooseWeek = (index : number) => {
if (weekList.value.findIndex(item => item == index) != -1) {
let i = weekList.value.findIndex(item => item == index)
weekList.value.splice(i, 1)
return
}
weekList.value.push(index)
}
const chinese = (item : number) => {
if (item + 1 == 1) {
return '一'
}
if (item + 1 == 2) {
return '二'
} if (item + 1 == 3) {
return '三'
} if (item + 1 == 4) {
return '四'
} if (item + 1 == 5) {
return '五'
} if (item + 1 == 6) {
return '六'
} if (item + 1 == 7) {
return '日'
}
}
const dateconfiom = () => {
storeData.value.time = ''
if (weekList.value.length == 0) {
Service.Msg('请选择营业时间')
return
}
if (!openTime.value) {
Service.Msg('请选择开业时间')
return
}
if (!closeTime.value) {
Service.Msg('请选择结业时间')
return
}
if (openTime.value > closeTime.value) {
Service.Msg('开业时间不得小于结业时间')
return
}
weekList.value.sort((a, b) => {
return a - b
})
let timeIndex = weekList.value[0] - 0
let judgment = weekList.value.findIndex((item, index) => {
return item - index !== timeIndex
})
// 1是至 /0是全显示
if (judgment == -1) {
timefunc.value = 1
} else {
timefunc.value = 0
}
if (timefunc.value == 0) {
weekList.value.map((item) => {
storeData.value.time = storeData.value.time + '周' + chinese(item) + ' '
})
} else {
storeData.value.time = '周' + chinese((weekList.value[0])) + '至' + '周' + chinese((weekList.value[weekList.value.length - 1]))
}
storeData.value.time = storeData.value.time + ' ' + openTime.value + '-' + closeTime.value
showDate.value = !showDate.value
}
const save = () => {
if(!storeData.value.photo){
Service.Msg('请上传图片!')
return
}
if(!storeData.value.name){
Service.Msg('请输入店铺名称!')
return
}
if(!storeData.value.storeClass){
Service.Msg('请选择店铺吗分类!')
return
}
if(!storeData.value.province && !storeData.value.city && storeData.value.county ){
Service.Msg('请选择地址!')
return
}
if(!storeData.value.address){
Service.Msg('请输入详细地址!')
return
}
if(!storeData.value.price){
Service.Msg('请输入平均消费金额!')
return
}
if(!storeData.value.phone){
Service.Msg('请输入联系电话!')
return
}
if(!storeData.value.time){
Service.Msg('请输入营业时间!')
return
}
if(storeData.value.imgs.length==0){
Service.Msg('请上传店铺展示图!')
return
}
// if(tagList.value.length==0){
// Service.Msg('请添加店铺标签!')
// return
// }
// if(!storeData.value.comId){
// Service.Msg('请选择所属社区!')
// return
// }
// start时间处理
let time = ''
let newWeekList = new Array()
weekList.value.forEach((item, index) => {
let date = item + 1
if (date == 7) {
date = 0
}
newWeekList.push(date)
})
newWeekList.sort((a, b) => {
return a - b
})
newWeekList.forEach((item, index) => {
time = time + (time == '' ? '' : '-') + item
})
time = time + '_' + storeData.value.time.split(' ')[storeData.value.time.split(' ').length - 1]
// end时间处理
let tag = JSON.stringify(tagList.value)
let imgs=JSON.stringify(storeData.value.imgs)
vpMerchService.UpdateMerch( storeData.value.assortId,storeData.value.photo , '',storeData.value.name,imgs,storeData.value.phone,storeData.value.price,storeData.value.province,storeData.value.city,storeData.value.county,storeData.value.address,storeData.value.longitude,storeData.value.latitude,time,'').then(res=>{
if(res.code==0){
Service.Msg('保存成功!')
setTimeout(()=>{
Service.GoPageBack()
},2500)
}
})
}
const timeCancle=(busTime:string)=>{
let data= busTime.split('_')
openTime.value=data[1].split('-')[0]
closeTime.value=data[1].split('-')[1]
let timeData=data[0].split('-')
for(let i=0;i<timeData.length;i++){
if(timeData[i]=='0'){
timeData[i]='7'
}
}
let time=''
timeData.sort((a:any,b:any)=>{
return a-b
})
weekList.value=[]
timeData.map((item:any)=>{
weekList.value.push(item=='7'?(Number(6)):Number(item-1))
})
let timeIndex = weekList.value[0] - 0
let judgment = weekList.value.findIndex((item, index) => {
return item - index !== timeIndex
})
// 1是至 /0是全显示
if (judgment == -1) {
timefunc.value = 1
} else {
timefunc.value = 0
}
if (timefunc.value == 0) {
weekList.value.map((item) => {
time = time + '周' + chinese(item) + ' '
})
} else {
time = '周' + chinese((weekList.value[0])) + '至' + '周' + chinese((weekList.value[weekList.value.length - 1]))
}
time=time+data[1]
return time
}
</script>
<style lang="scss">
.section {
margin-bottom: 24rpx;
.section-title {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
display: block;
}
}
.uploader-wrapper {
.upload-slot {
width: 100%;
height: 300rpx;
background-color: #fff8f5;
border: 1rpx dashed #e2e2e2;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
font-size: 26rpx;
color: #fa6400;
}
}
.tag {
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
font-size: 24rpx;
}
.tabActive {
background-color: var(--nav-mian);
color: #fff;
}
.tab {
background-color: #F5F5F5;
color: #333333
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<view style="padding: 20rpx;">
<up-search placeholder="搜索相关内容..." :showAction='false' v-model="search"></up-search>
<view class="" style=" margin: 25rpx 0; display: flex; align-items: center; justify-content: space-between;" >
<view @click="changetab(index)" v-for="(item,index) in tag" :key="index" :class="{active:index!=currentIndex,actived:index==currentIndex}" class="tag" style=" padding: 14rpx 36rpx; border-radius: 46rpx; " >
{{item}}
</view>
</view>
</view>
<view class="" style="padding: 30rpx 20rpx; border-bottom: 1rpx solid #e2e2e2; border-top: 1rpx solid #e2e2e2; " >
<view class="" style="font-weight: 600; font-size: 32rpx;" >
精品推荐
</view>
<view class="" @click="Service.GoPage('/pages/article/news')" style="margin-top: 20rpx; position: relative; ">
<image :src="Service.GetMateUrlByImg('/static/dele/dele1.jpg')" mode="aspectFill" style=" border-radius: 20rpx; width: 100%; height: 350rpx;" alt="" />
<view class="" style="position: absolute; left: 20rpx; bottom: 50rpx; color: #fff; " >
<view class="" style="font-size: 34rpx;" >
城市漫步:寻找完美咖啡馆
</view>
<view class="" style=" margin: 15rpx 0; font-size: 26rpx;" >
探索城市中隐藏的咖啡珍宝,感受不同的咖啡文化
</view>
<view class="" style="font-size: 22rpx; display: flex; align-items: center;" >
<img :src="Service.GetIconImg('/static/article/time.png')" style="width: 24rpx; height: 24rpx;" alt="" />
<text style="margin-left: 10rpx;" >2小时前</text>
</view>
</view>
</view>
</view>
<view class="" style="padding: 20rpx; border-bottom: 1rpx solid #e2e2e2;" >
<view class="" @click="Service.GoPage('/pages/article/news')" style="display: flex; margin-top: 20rpx; border-radius: 20rpx; " >
<image :src="Service.GetMateUrlByImg('/static/dele/dele1.jpg')" mode="aspectFill" style=" border-radius: 20rpx; width: 190rpx; height: 150rpx;" alt="" />
<view class="" style=" flex: 1; margin-left: 20rpx; display: flex; flex-direction: column; justify-content: space-between; " >
<view class="" style=" " >
<view class="" style="font-weight: 700; font-size: 32rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;" >
老北京炸酱面
</view>
</view>
<view class="" style=" margin: 8rpx 0; color: #666666; font-size: 26rpx; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; " >
老北京炸酱面是北京市传统小吃,属北京菜系,在京津冀地区广为流传。该菜品以面条为主料,搭配炸酱与时令菜码拌制而成,核心酱料选用肥瘦相间的五花肉丁,配以干黄酱和甜面酱混合炒制,经慢火熬煮形成深褐色酱料
</view>
<view class="" style=" display: flex; align-items: center; " >
<img :src="Service.GetIconImg('/static/article/date.png')" style="width: 24rpx; height: 24rpx;" alt="" />
<text style="color: #666666; margin-left: 10rpx; font-size: 26rpx;" >2026-10-15</text>
</view>
</view>
</view>
</view>
<up-loadmore :status="status" />
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from "@/Service/Service"
let search = ref()
let status=ref('nomore')
let currentIndex=ref(0)
let tag=ref([
'全部',
'热门',
'推荐',
'最近'
])
onLoad(() => {
});
onShow(() => {
});
const changetab=(e:number)=>{
currentIndex.value=e
}
</script>
<style lang="scss">
.active{
background-color: #F3F4F6;
color: #4B5563;
}
.actived{
background-color: #FF6B35;
color: #fff;
}
.tag{
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
font-size: 28rpx;
margin-left: 10rpx;
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<view style=" padding: 60rpx 40rpx 20rpx; ">
<view class="" style="font-size: 26rpx;">
提现金额
</view>
<view class="" style="margin-bottom: 20rpx;">
<up-input v-model="account" type="digit" :customStyle="{'padding':'12rpx 0','height':'100rpx'}"
fontSize='18' prefixIconStyle="font-size: 28px;color: #000;font-weight:bold" placeholder="请输入金额"
border="bottom" prefixIcon="rmb"></up-input>
</view>
<view class="" style="font-size: 26rpx; display: flex; align-items: center; ">
<view class="" style="color: #999; ">
当前剩余 {{ Number(userData.integral).toFixed(2) }} 元
</view>
</view>
</view>
<view class="" style="margin: 40rpx 20rpx; ">
<u-button type="primary" @click="save" :custom-style="contactButtonStyle">立即提现</u-button>
</view>
<view class="" style="margin: 20rpx; padding: 20rpx 30rpx; background-color: #f6f6f6; border-radius: 20rpx; ">
<view class="" style="font-weight: bold; font-size: 30rpx;">
提示
</view>
<view class="" style="font-size: 28rpx;">
<view class="" style="text-indent: 1em; margin-top: 10rpx; ">
1.单笔最小提现1元
</view>
<view class="" style="text-indent: 1em; margin-top: 10rpx; ">
2.单笔最多可提现200元
</view>
<view class="" style="text-indent: 1em; margin-top: 10rpx; ">
3.每次最多一次申请提现
</view>
</view>
</view>
<view class="" v-if="withLog!==null"
style="margin: 20rpx; padding: 20rpx 30rpx 30rpx; background-color: #f6f6f6; border-radius: 10rpx; ">
<view class="" style="font-weight: bold; font-size: 30rpx; display: flex; gap: 6rpx; ">
<img :src="Service.GetIconImg('/static/index/pay/light.png')" style="width: 40rpx; height: 40rpx;" alt="" />
待收款提醒
</view>
<view class="" style="font-size: 28rpx; margin: 20rpx 0; text-indent: 2em; ">
金额: {{ Number(withLog.amount).toFixed(2) }}
</view>
<view class="" style="font-size: 28rpx; margin: 20rpx 0; text-indent: 2em; ">
时间: {{ withLog.addTime }}
</view>
<view class=""
style=" display: flex; justify-content: flex-end; width: 100%; ">
<button @click="withdrow(withdrowData)" class="button-withdrow" style=" ">确认收款</button>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service, vpMerchService } from '@/Service/vp/vpMerchService'
import { vpLoginService } from '@/Service/vp/vpLoginService'
import { vpUserService } from '@/Service/vp/vpUserService'
let name = ref('')
let account = ref<number>()
let userData = ref({
integral: 0,
merchId: ''
})
// 按钮样式
const contactButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '90rpx',
borderRadius: '45rpx',
marginRight: '20rpx'
});
let withdrowData = ref({
data:{
appId: '',
mchId: '',
packInfo: '',
withId:''
}
})
let withLog = ref<any>(null)
onLoad(() => {
getData()
});
onShow(() => {
});
const getData = () => {
vpMerchService.GetMerchAccInfo('', 1).then(res => {
if (res.code == 0) {
userData.value = res.data.merchAcc
withLog.value = res.data.withLog
withdrowData.value.data.mchId = res.data.mchId
withdrowData.value.data.appId = res.data.appId
withdrowData.value.data.packInfo = res.data.withLog.packInfo
withdrowData.value.data.withId=res.data.withLog.withId
} else {
Service.Msg(res.msg)
}
})
}
const save = () => {
if (account.value === 0 || !account.value) {
Service.Msg('请输入金额')
return
}
if (account.value < 1) {
Service.Msg('单笔最小提现1元')
return
}
if (account.value > 200) {
Service.Msg('单笔最多可提现200元')
return
}
Service.LoadIng('提现中')
uni.getProvider({
service: 'oauth',
success: function (res : any) {
uni.login({
onlyAuthorize: true,
provider: res.provider,
success: function (loginRes) {
vpLoginService.GetOpenIdByWeixin(loginRes.code, res.provider == 'weixin' ? 1 : 3).then(content => {
if (content.code == 0) {
vpUserService.WxWith(content.data, account.value, '').then(wxres => {
Service.LoadClose()
if (wxres.code == 0) {
withdrow(wxres)
} else {
Service.Msg(wxres.msg)
}
})
} else {
Service.Msg(content.msg)
}
})
}
})
}
});
}
const withdrow = (wxres) => {
if (wx.canIUse('requestMerchantTransfer')) {
wx.requestMerchantTransfer({
mchId: wxres.data.mchId,
appId: wxres.data.appId,
package: wxres.data.packInfo,
success: () => {
vpUserService.UpdateWith(wxres.data.withId).then(withdrow => {
})
Service.Msg('提现成功!')
setTimeout(() => {
Service.GoPageBack()
}, 1000)
},
fail: (res) => {
console.log('fail:', res);
},
});
} else {
wx.showModal({
content: '你的微信版本过低,请更新至最新版本。',
showCancel: false,
});
}
}
</script>
<style lang="scss">
page {
background-color: #fff;
}
.action-buttons {
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
.button-withdrow {
width: 36%;
color: #fff;
background-color: var(--nav-mian);
border-color: #f6f6f6;
border-radius: 16rpx;
font-size: 24rpx;
margin: 0;
}
.button-withdrow::after {
border: none;
}
</style>

View File

@@ -0,0 +1,961 @@
# Painter 画板 测试版
> uniapp 海报画板,更优雅的海报生成方案
> [查看更多](https://limeui.qcoon.cn/#/painter)
## 平台兼容
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
| √ | √ | √ | 未测 | √ | √ | √ |
## 安装
在市场导入**[海报画板](https://ext.dcloud.net.cn/plugin?id=2389)uni_modules**版本的即可,无需`import`
## 代码演示
### 插件demo
- lime-painter 为 demo
- 位于 uni_modules/lime-painter/components/lime-painter
- 导入插件后直接使用可查看demo
```vue
<lime-painter />
```
### 基本用法
- 插件提供 JSON 及 Template 的方式绘制海报
- 参考 css 块状流布局模拟 css schema。
- 另外flex布局还不是成完善请谨慎使用普通的流布局我觉得已经够用了。
#### 方式一 Template
- 提供`l-painter-view`、`l-painter-text`、`l-painter-image`、`l-painter-qrcode`四种类型组件
- 通过 `css` 属性绘制样式,与 style 使用方式保持一致。
```html
<l-painter>
//如果使用Template出现顺序错乱可使用`template` 等所有变量完成再显示
<template v-if="show">
<l-painter-view
css="background: #07c160; height: 120rpx; width: 120rpx; display: inline-block"
></l-painter-view>
<l-painter-view
css="background: #1989fa; height: 120rpx; width: 120rpx; border-top-right-radius: 60rpx; border-bottom-left-radius: 60rpx; display: inline-block; margin: 0 30rpx;"
></l-painter-view>
<l-painter-view
css="background: #ff9d00; height: 120rpx; width: 120rpx; border-radius: 50%; display: inline-block"
></l-painter-view>
<template>
</l-painter>
```
#### 方式二 JSON
- 在 json 里四种类型组件的`type`为`view`、`text`、`image`、`qrcode`
- 通过 `board` 设置海报所需的 JSON 数据进行绘制或`ref`获取组件实例调用组件内的`render(json)`
- 所有类型的 schema 都具有`css`字段css 的 key 值使用**驼峰**如:`lineHeight`
```html
<l-painter :board="poster"/>
```
```js
data() {
return {
poster: {
css: {
// 根节点若无尺寸,自动获取父级节点
width: '750rpx'
},
views: [
{
css: {
background: "#07c160",
height: "120rpx",
width: "120rpx",
display: "inline-block"
},
type: "view"
},
{
css: {
background: "#1989fa",
height: "120rpx",
width: "120rpx",
borderTopRightRadius: "60rpx",
borderBottomLeftRadius: "60rpx",
display: "inline-block",
margin: "0 30rpx"
},
views: [],
type: "view"
},
{
css: {
background: "#ff9d00",
height: "120rpx",
width: "120rpx",
borderRadius: "50%",
display: "inline-block"
},
views: [],
type: "view"
},
]
}
}
}
```
### View 容器
- 类似于 `div` 可以嵌套承载更多的 view、text、imageqrcode 共同构建一颗完整的节点树
- 在 JSON 里具有 `views` 的数组字段,用于嵌套承载节点。
#### 方式一 Template
```html
<l-painter>
<l-painter-view css="background: #f0f0f0; padding-top: 100rpx;">
<l-painter-view
css="background: #d9d9d9; width: 33.33%; height: 100rpx; display: inline-block"
></l-painter-view>
<l-painter-view
css="background: #bfbfbf; width: 66.66%; height: 100rpx; display: inline-block"
></l-painter-view>
</l-painter-view>
</l-painter>
```
#### 方式二 JSON
```js
{
css: {},
views: [
{
type: 'view',
css: {
background: '#f0f0f0',
paddingTop: '100rpx'
},
views: [
{
type: 'view',
css: {
background: '#d9d9d9',
width: '33.33%',
height: '100rpx',
display: 'inline-block'
}
},
{
type: 'view',
css: {
background: '#bfbfbf',
width: '66.66%',
height: '100rpx',
display: 'inline-block'
}
}
],
}
]
}
```
### Text 文本
- 通过 `text` 属性填写文本内容。
- 支持`\n`换行符
- 支持省略号,使用 css 的`line-clamp`设置行数,当文字内容超过会显示省略号。
- 支持`text-decoration`
#### 方式一 Template
```html
<l-painter>
<l-painter-view css="background: #e0e2db; padding: 30rpx; color: #222a29">
<l-painter-text
text="登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼"
/>
<l-painter-text
text="登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼"
css="text-align:center; padding-top: 20rpx; text-decoration: line-through "
/>
<l-painter-text
text="登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼"
css="text-align:right; padding-top: 20rpx"
/>
<l-painter-text
text="水调歌头\n明月几时有把酒问青天。不知天上宫阙今夕是何年。我欲乘风归去又恐琼楼玉宇高处不胜寒。起舞弄清影何似在人间。"
css="line-clamp: 3; padding-top: 20rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%); background-clip: text"
/>
</l-painter-view>
</l-painter>
```
#### 方式二 JSON
```js
// 基础用法
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
css: {
// 设置居中对齐
textAlign: 'center',
// 设置中划线
textDecoration: 'line-through'
}
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
css: {
// 设置右对齐
textAlign: 'right',
}
},
{
type: 'text',
text: '登鹳雀楼\n白日依山尽黄河入海流\n欲穷千里目更上一层楼',
css: {
// 设置行数,超出显示省略号
lineClamp: 3,
// 渐变文字
background: 'linear-gradient(,#ff971b 0%, #1989fa 100%)',
backgroundClip: 'text'
}
}
```
### Image 图片
- 通过 `src` 属性填写图片路径。
- 图片路径支持:网络图片,本地 static 里的图片路径,缓存路径,**字节的static目录是写相对路径**
- 通过 `css` 的 `object-fit`属性可以设置图片的填充方式,可选值见下方 CSS 表格。
- 通过 `css` 的 `object-position`配合 `object-fit` 可以设置图片的对齐方式,类似于`background-position`,详情见下方 CSS 表格。
- 使用网络图片时:小程序需要去公众平台配置 [downloadFile](https://mp.weixin.qq.com/) 域名
- 使用网络图片时:**H5 和 Nvue 需要决跨域问题**
#### 方式一 Template
```html
<l-painter>
<!-- 基础用法 -->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx"
/>
<!-- 填充方式 -->
<!-- css object-fit 设置 填充方式 见下方表格-->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx; object-fit: contain; background: #eee"
/>
<!-- css object-position 设置 图片的对齐方式-->
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="width: 200rpx; height: 200rpx; object-fit: contain; object-position: 50% 50%; background: #eee"
/>
</l-painter>
```
#### 方式二 JSON
```js
// 基础用法
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx'
}
},
// 填充方式
// css objectFit 设置 填充方式 见下方表格
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx',
objectFit: 'contain'
}
},
// css objectPosition 设置 图片的对齐方式
{
type: 'image',
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
css: {
width: '200rpx',
height: '200rpx',
objectFit: 'contain',
objectPosition: '50% 50%'
}
}
```
### Qrcode 二维码
- 通过`text`属性填写需要生成二维码的文本。
- 通过 `css` 里的 `color` 可设置生成码点的颜色。
- 通过 `css` 里的 `background`可设置背景色。
- 通过 `css `里的 `width`、`height`设置尺寸。
#### 方式一 Template
```html
<l-painter>
<l-painter-qrcode
text="limeui.qcoon.cn"
css="width: 200rpx; height: 200rpx"
/>
</l-painter>
```
#### 方式二 JSON
```js
{
type: 'qrcode',
text: 'limeui.qcoon.cn',
css: {
width: '200rpx',
height: '200rpx',
}
}
```
### 富文本
- 这是一个有限支持的测试能力只能通过JSON方式不要抱太大希望!
- 首先需要把富文本转成JSON,这需要引入`parser`这个包,如果你不使用是不会进入主包
```html
<l-painter ref="painter"/>
```
```js
import parseHtml from '@/uni_modules/lime-painter/parser'
const json = parseHtml(`<p><span>测试测试</span><img src="/static/logo.png"/></p>`)
this.$refs.painter.render(json)
```
### 生成图片
- 方式1、通过设置`isCanvasToTempFilePath`自动生成图片并在 `@success` 事件里接收海报临时路径
- 方式2、通过调用内部方法生成图片
```html
<l-painter ref="painter">...code</l-painter>
```
```js
this.$refs.painter.canvasToTempFilePathSync({
fileType: "jpg",
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum需要设置 pathType为url
pathType: 'url',
quality: 1,
success: (res) => {
console.log(res.tempFilePath);
// 非H5 保存到相册
// H5 提示用户长按图另存
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: function () {
console.log('save success');
}
});
},
});
```
### 主动调用方式
- 通过获取组件实例内部的`render`函数 传递`JSON`即可
```html
<l-painter ref="painter" />
```
```js
// 渲染
this.$refs.painter.render(jsonSchema);
// 生成图片
this.$refs.painter.canvasToTempFilePathSync({
fileType: "jpg",
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum需要设置 pathType为url
pathType: 'url',
quality: 1,
success: (res) => {
console.log(res.tempFilePath);
// 非H5 保存到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: function () {
console.log('save success');
}
});
},
});
```
### H5跨域
- 一般是需要后端或管理OSS资源的大佬处理
- 一般OSS的处理方式:
1、设置来源
```cmd
*
```
2、允许Methods
```html
GET
```
3、允许Headers
```html
access-control-allow-origin:*
```
4、最后如果还是不行,可试下给插件设置`useCORS`
```html
<l-painter useCORS>
```
### 海报示例
- 提供一份示例,只把插件当成生成图片的工具,非必要不要在弹窗里使用。
- 通过设置`isCanvasToTempFilePath`主动生成图片,再由 `@success` 事件接收海报临时路径
- 设置`hidden`隐藏画板。
请注意,示例用到了图片,海报的渲染是包括下载图片的时间,也许在某天图片会失效或访问超级慢,请更换为你的图片再查看,另外如果你是小程序请在使用示例时把**不校验合法域名**勾上!!!!!不然不显示还以为是插件的锅,求求了大佬们!
#### 方式一 Template
```html
<image :src="path" mode="widthFix"></image>
<l-painter
isCanvasToTempFilePath
@success="path = $event"
hidden
css="width: 750rpx; padding-bottom: 40rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%)"
>
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="margin-left: 40rpx; margin-top: 40rpx; width: 84rpx; height: 84rpx; border-radius: 50%;"
/>
<l-painter-view
css="margin-top: 40rpx; padding-left: 20rpx; display: inline-block"
>
<l-painter-text
text="隔壁老王"
css="display: block; padding-bottom: 10rpx; color: #fff; font-size: 32rpx; fontWeight: bold"
/>
<l-painter-text
text="为您挑选了一个好物"
css="color: rgba(255,255,255,.7); font-size: 24rpx"
/>
</l-painter-view>
<l-painter-view
css="margin-left: 40rpx; margin-top: 30rpx; padding: 32rpx; box-sizing: border-box; background: #fff; border-radius: 16rpx; width: 670rpx; box-shadow: 0 20rpx 58rpx rgba(0,0,0,.15)"
>
<l-painter-image
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
css="object-fit: cover; object-position: 50% 50%; width: 606rpx; height: 606rpx; border-radius: 12rpx;"
/>
<l-painter-view
css="margin-top: 32rpx; color: #FF0000; font-weight: bold; font-size: 28rpx; line-height: 1em;"
>
<l-painter-text text="¥" css="vertical-align: bottom" />
<l-painter-text
text="39"
css="vertical-align: bottom; font-size: 58rpx"
/>
<l-painter-text text=".39" css="vertical-align: bottom" />
<l-painter-text
text="¥59.99"
css="vertical-align: bottom; padding-left: 10rpx; font-weight: normal; text-decoration: line-through; color: #999999"
/>
</l-painter-view>
<l-painter-view css="margin-top: 32rpx; font-size: 26rpx; color: #8c5400">
<l-painter-text text="自营" css="color: #212121; background: #ffb400;" />
<l-painter-text
text="30天最低价"
css="margin-left: 16rpx; background: #fff4d9; text-decoration: line-through;"
/>
<l-painter-text
text="满减优惠"
css="margin-left: 16rpx; background: #fff4d9"
/>
<l-painter-text
text="超高好评"
css="margin-left: 16rpx; background: #fff4d9"
/>
</l-painter-view>
<l-painter-view css="margin-top: 30rpx">
<l-painter-text
css="line-clamp: 2; color: #333333; line-height: 1.8em; font-size: 36rpx; width: 478rpx; padding-right:32rpx; box-sizing: border-box"
text="360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝"
></l-painter-text>
<l-painter-qrcode
css="width: 128rpx; height: 128rpx;"
text="limeui.qcoon.cn"
></l-painter-qrcode>
</l-painter-view>
</l-painter-view>
</l-painter>
```
```js
data() {
return {
path: ''
}
}
```
#### 方式二 JSON
```html
<image :src="path" mode="widthFix"></image>
<l-painter
:board="poster"
isCanvasToTempFilePath
@success="path = $event"
hidden
/>
```
```js
data() {
return {
path: '',
poster: {
css: {
width: "750rpx",
paddingBottom: "40rpx",
background: "linear-gradient(,#000 0%, #ff5000 100%)"
},
views: [
{
src: "https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg",
type: "image",
css: {
background: "#fff",
objectFit: "cover",
marginLeft: "40rpx",
marginTop: "40rpx",
width: "84rpx",
border: "2rpx solid #fff",
boxSizing: "border-box",
height: "84rpx",
borderRadius: "50%"
}
},
{
type: "view",
css: {
marginTop: "40rpx",
paddingLeft: "20rpx",
display: "inline-block"
},
views: [
{
text: "隔壁老王",
type: "text",
css: {
display: "block",
paddingBottom: "10rpx",
color: "#fff",
fontSize: "32rpx",
fontWeight: "bold"
}
},
{
text: "为您挑选了一个好物",
type: "text",
css: {
color: "rgba(255,255,255,.7)",
fontSize: "24rpx"
},
}
],
},
{
css: {
marginLeft: "40rpx",
marginTop: "30rpx",
padding: "32rpx",
boxSizing: "border-box",
background: "#fff",
borderRadius: "16rpx",
width: "670rpx",
boxShadow: "0 20rpx 58rpx rgba(0,0,0,.15)"
},
views: [
{
src: "https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg",
type: "image",
css: {
objectFit: "cover",
objectPosition: "50% 50%",
width: "606rpx",
height: "606rpx"
},
}, {
css: {
marginTop: "32rpx",
color: "#FF0000",
fontWeight: "bold",
fontSize: "28rpx",
lineHeight: "1em"
},
views: [{
text: "¥",
type: "text",
css: {
verticalAlign: "bottom"
},
}, {
text: "39",
type: "text",
css: {
verticalAlign: "bottom",
fontSize: "58rpx"
},
}, {
text: ".39",
type: "text",
css: {
verticalAlign: "bottom"
},
}, {
text: "¥59.99",
type: "text",
css: {
verticalAlign: "bottom",
paddingLeft: "10rpx",
fontWeight: "normal",
textDecoration: "line-through",
color: "#999999"
}
}],
type: "view"
}, {
css: {
marginTop: "32rpx",
fontSize: "26rpx",
color: "#8c5400"
},
views: [{
text: "自营",
type: "text",
css: {
color: "#212121",
background: "#ffb400"
},
}, {
text: "30天最低价",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9",
textDecoration: "line-through"
},
}, {
text: "满减优惠",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9"
},
}, {
text: "超高好评",
type: "text",
css: {
marginLeft: "16rpx",
background: "#fff4d9"
},
}],
type: "view"
}, {
css: {
marginTop: "30rpx"
},
views: [
{
text: "360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝",
type: "text",
css: {
paddingRight: "32rpx",
boxSizing: "border-box",
lineClamp: 2,
color: "#333333",
lineHeight: "1.8em",
fontSize: "36rpx",
width: "478rpx"
},
}, {
text: "limeui.qcoon.cn",
type: "qrcode",
css: {
width: "128rpx",
height: "128rpx",
},
}],
type: "view"
}],
type: "view"
}
]
}
}
}
```
### 自定义字体
- 需要平台的支持,已知微信小程序支持,其它的没试过,如果可行请告之
```
// 需要在app.vue中下载字体
uni.loadFontFace({
global:true,
scopes: ['native'],
family: '自定义字体名称',
source: 'url("https://sungd.github.io/Pacifico.ttf")',
success() {
console.log('success')
}
})
// 然后就可以在插件的css中写font-family: '自定义字体名称'
```
### Nvue
- 必须为HBX 3.4.11及以上
### 原生小程序
- 插件里的`painter.js`支持在原生小程序中使用
- new Painter 之后在`source`里传入 JSON
- 再调用`render`绘制海报
- 如需生成图片,请查看微信小程序 cavnas 的[canvasToTempFilePath](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html)
```html
<canvas type="2d" id="painter" style="width: 100%"></canvas>
```
```js
import { Painter } from "./painter";
page({
data: {
poster: {
css: {
width: "750rpx",
},
views: [
{
type: "view",
css: {
background: "#d2d4c8",
paddingTop: "100rpx",
},
views: [
{
type: "view",
css: {
background: "#5f7470",
width: "33.33%",
height: "100rpx",
display: "inline-block",
},
},
{
type: "view",
css: {
background: "#889696",
width: "33.33%",
height: "100rpx",
display: "inline-block",
},
},
{
type: "view",
css: {
background: "#b8bdb5",
width: "33.33%",
height: "100rpx",
display: "inline-block",
},
},
],
},
],
},
},
async onLoad() {
const res = await this.getCentext();
const painter = new Painter(res);
// 返回计算布局后的整个内容尺寸
const { width, height } = await painter.source(this.data.poster);
// 得到计算后的尺寸后 可给canvas尺寸赋值达到动态响应效果
// 渲染
await painter.render();
},
// 获取canvas 2d
// 非2d 需要传一个 createImage 方法用于获取图片信息 即把 getImageInfo 的 success 通过 promise resolve 返回
getCentext() {
return new Promise((resolve) => {
wx.createSelectorQuery()
.select(`#painter`)
.node()
.exec((res) => {
let { node: canvas } = res[0];
resolve({
canvas,
context: canvas.getContext("2d"),
width: canvas.width,
height: canvas.height,
// createImage: getImageInfo()
pixelRatio: 2,
});
});
});
},
});
```
### 旧版(1.6.x)更新
- 由于 1.8.x 版放弃了以定位的方式,所以 1.6.x 版更新之后要每个样式都加上`position: absolute`
- 旧版的 `image` mode 模式被放弃,使用`object-fit`
- 旧版的 `isRenderImage` 改成 `is-canvas-to-temp-file-path`
- 旧版的 `maxLines` 改成 `line-clamp`
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| -------------------------- | ------------------------------------------------------------ | ---------------- | ------------ |
| board | JSON 方式的海报元素对象集 | <em>object</em> | - |
| css | 海报内容最外层的样式,可以理解为`body` | <em>object</em> | 参数请向下看 |
| custom-style | canvas 元素的样式 | <em>string</em> | |
| hidden | 隐藏画板 | <em>boolean</em> | `false` |
| is-canvas-to-temp-file-path | 是否生成图片,在`@success`事件接收图片地址 | <em>boolean</em> | `false` |
| after-delay | 生成图片错乱,可延时生成图片 | <em>number</em> | `100` |
| type | canvas 类型,对微信头条支付宝小程序可有效,可选值:`2d``''` | <em>string</em> | `2d` |
| file-type | 生成图片的后缀类型, 可选值:`png`、`jpg` | <em>string</em> | `png` |
| path-type | 生成图片路径类型,可选值`url`、`base64` | <em>string</em> | `-` |
| pixel-ratio | 生成图片的像素密度,默认为对应手机的像素密度,`nvue`无效 | <em>number</em> | `-` |
| hidpi | H5和APP是否使用高清处理 | <em>boolean</em> | `true` |
| width | **废弃** 画板的宽度,一般只用于通过内部方法时加上 | <em>number</em> | `` |
| height | **废弃** 画板的高度 ,同上 | <em>number</em> | `` |
### css
| 属性名 | 支持的值或类型 | 默认值 |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| (min\max)width | 支持`%`、`rpx`、`px` | - |
| height | 同上 | - |
| color | `string` | - |
| position | 定位,可选值:`absolute`、`fixed` | - |
| ↳ left、top、right、bottom | 配合`position`才生效,支持`%`、`rpx`、`px` | - |
| margin | 可简写或各方向分别写,如:`margin-top`,支持`auto`、`rpx`、`px` | - |
| padding | 可简写或各方向分别写,支持`rpx`、`px` | - |
| border | 可简写或各个值分开写:`border-width`、`border-style` 、`border-color`,简写请按顺序写 | - |
| line-clamp | `number`,超过行数显示省略号 | - |
| vertical-align | 文字垂直对齐,可选值:`bottom`、`top`、`middle` | `middle` |
| line-height | 文字行高,支持`rpx`、`px`、`em` | `1.4em` |
| font-weight | 文字粗细,可选值:`normal`、`bold` | `normal` |
| font-size | 文字大小,`string`,支持`rpx`、`px` | `14px` |
| text-decoration | 文本修饰,可选值:`underline` 、`line-through`、`overline` | - |
| text-stroke | 文字描边,可简写或各个值分开写,如:`text-stroke-color`, `text-stroke-width` | - |
| text-align | 文本水平对齐,可选值:`right` 、`center` | `left` |
| display | 框类型,可选值:`block`、`inline-block`、`flex`、`none`,当为`none`时是不渲染该段, `flex`功能简陋。 | - |
| flex | 配合 display: flex; 属性定义了在分配多余空间,目前只用为数值如: flex: 1 | - |
| align-self | 配合 display: flex; 单个项目垂直轴对齐方式: `flex-start` `flex-end` `center` | `flex-start` |
| justify-content | 配合 display: flex; 水平轴对齐方式: `flex-start` `flex-end` `center` | `flex-start` |
| align-items | 配合 display: flex; 垂直轴对齐方式: `flex-start` `flex-end` `center` | `flex-start` |
| border-radius | 圆角边框,支持`%`、`rpx`、`px` | - |
| box-sizing | 可选值:`border-box` | - |
| box-shadow | 投影 | - |
| background(color) | 支持渐变,但必须写百分比!如:`linear-gradient(,#ff971b 0%, #ff5000 100%)`、`radial-gradient(#0ff 15%, #f0f 60%)`,目前 radial-gradient 渐变的圆心为元素中点,半径为最长边,不支持设置 | - |
| background-clip | 文字渐变,配合`background`背景渐变,设置`background-clip: text` 达到文字渐变效果 | - |
| background-image | view 元素背景:`url(src)`,若只是设置背景图,请不要设置`background-repeat` | - |
| background-repeat | 设置是否及如何重复背景纹理,可选值:`repeat`、`repeat-x`、`repeat-y`、`no-repeat` | `repeat` |
| [object-fit](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit/) | 图片元素适应容器方式,类似于`mode`,可选值:`cover`、 `contain`、 `fill`、 `none` | - |
| [object-position](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-position) | 图片的对齐方式,配合`object-fit`使用 | - |
### 图片填充模式 object-fit
| 名称 | 含义 |
| ------- | ------------------------------------------------------ |
| contain | 保持宽高缩放图片,使图片的长边能完全显示出来 |
| cover | 保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边 |
| fill | 拉伸图片,使图片填满元素 |
| none | 保持图片原有尺寸 |
### 事件 Events
| 事件名 | 说明 | 返回值 |
| -------- | ---------------------------------------------------------------- | ------ |
| success | 生成图片成功,若使用`is-canvas-to-temp-filePath` 可以接收图片地址 | path |
| fail | 生成图片失败 | error |
| done | 绘制成功 | |
| progress | 绘制进度 | number |
### 暴露函数 Expose
| 事件名 | 说明 | 返回值 |
| -------- | ---------------------------------------------------------------- | ------ |
| render(object) | 渲染器传入JSON 绘制海报 | promise |
| [canvasToTempFilePath](https://uniapp.dcloud.io/api/canvas/canvasToTempFilePath.html#canvastotempfilepath)(object) | 把当前画布指定区域的内容导出生成指定大小的图片,并返回文件临时路径。 | |
| canvasToTempFilePathSync(object) | 同步接口,同上 | |
## 常见问题
- 1、H5 端使用网络图片需要解决跨域问题。
- 2、小程序使用网络图片需要去公众平台增加下载白名单二级域名也需要配
- 3、H5 端生成图片是 base64有时显示只有一半可以使用原生标签`<IMG/>`
- 4、发生保存图片倾斜变形或提示 native buffer exceed size limit 时,使用 pixel-ratio="2"参数,降分辨率。
- 5、h5 保存图片不需要调接口,提示用户长按图片保存。
- 6、画板不能隐藏包括`v-if``v-show`、`display:none`、`opacity:0`,另外也不要把画板放在弹窗里。如果需要隐藏画板请设置 `custom-style="position: fixed; left: 200%"`
- 7、微信小程序真机调试请使用 **真机调试2.0**不支持1.0。
- 8、微信小程序打开调试时可以生但并闭无法生成时这种情况一般是没有在公众号配置download域名
- 9、HBX 3.4.5之前的版本不支持vue3
- 10、在微信开发工具上 canvas 层级最高无法zindex并不影响真机
- 11、请不要导入非uni_modules插件
- 12、关于QQ小程序 报 Propertyor method"toJSON"is not defined 请把基础库调到 1.50.3
- 13、支付宝小程序 IDE 不支持 生成图片 请以真机调试结果为准
- 14、返回值为字符串 `data:,` 大概是尺寸超过限制,设置 pixel-ratio="2"
- 华为手机 APP 上无法生成图片,请使用 HBX2.9.11++(已过时,忽略这条)
- IOS APP 请勿使用 HBX2.9.3.20201014 的版本!这个版本无法生成图片。(已过时,忽略这条)
- 苹果微信 7.0.20 存在闪退和图片无法 onload 为微信 bug已过时忽略这条
- 微信小程序 IOS 旧接口 如父级设置圆角子级也设会导致子级的失效为旧接口BUG。
- 微信小程序 安卓 旧接口 如使用图片必须加背景色为旧接口BUG。
- 微信小程序 安卓端 [图片可能在首次可以加载成功,再次加载会不触发任何事件](https://developers.weixin.qq.com/community/develop/doc/000ee2b8dacf4009337f51f4556800?highLine=canvas%25202d%2520createImage),临时解决方法是给图片加个时间戳
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

View File

@@ -0,0 +1,48 @@
<template>
<view style="margin: 20rpx ;">
<view class=""
style=" display: flex; align-items: center; justify-content: center; padding: 60rpx 20rpx; border-radius: 20rpx; background-color: #fff; ">
<image :src="path" style="width: 100%;" mode="widthFix" alt="" />
</view>
<!-- <view class=""
style=" gap: 20rpx; margin-top: 20rpx; display: flex; align-items: center; background-color: #fff; border-radius: 15rpx;padding: 20rpx; ">
<up-icon size='20' name="info-circle"></up-icon>
<view class="" style="flex: 1; font-size: 28rpx; color: #666; ">
绑定用户,获取积分
</view>
</view> -->
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { vpUserService, Service } from '@/Service/vp/vpUserService'
import { ref } from "vue";
let path = ref()
onLoad(() => {
getData()
});
onShow(() => {
});
const getData = () => {
vpUserService.GetShareEwm().then(res => {
if (res.code == 0) {
path.value = 'https://vp.xypays.cn'+ res.data.path
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss">
page {
background-color: #f6f6f6;
}
</style>

View File

@@ -0,0 +1,369 @@
{
"easycom": {
// 注意一定要放在custom里否则无效https://ask.dcloud.net.cn/question/131175
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
}
},
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#36394D",
"navigationStyle": "custom",
"backgroundColor": "#F8F8F8"
}
},
{
"path": "pages/index/community",
"style": {
"navigationBarTitleText": "社区",
"navigationStyle": "custom"
}
},
{
"path": "pages/index/user",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path" : "pages/index/shop",
"style" :
{
"navigationBarTitleText" : "积分商城"
}
},
{
"path" : "pages/index/order",
"style" :
{
"navigationBarTitleText" : "我的订单"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "v派商家",
"navigationBarBackgroundColor": "#fff",
"backgroundColor": "#F8F8F8"
},
"subPackages": [{
"root": "pages/community",
"pages": [{
"path": "noticeList",
"style": {
"navigationBarTitleText": "社区公告"
}
},
{
"path": "merchantCom",
"style": {
"navigationBarTitleText": "社区商家"
}
},
{
"path" : "merchantDetail",
"style" :
{
"navigationBarTitleText" : "美食小店"
}
}
]
},
{
"root": "pages/goods",
"pages": [
{
"path": "integralGoods",
"style": {
"navigationBarTitleText": "积分商品"
}
},
{
"path": "merchant",
"style": {
"navigationBarTitleText": "热门商家"
}
},
{
"path" : "goodsDetail",
"style" :
{
"navigationBarTitleText" : "商品详情"
}
},
{
"path" : "goodsContro",
"style" :
{
"navigationBarTitleText" : "商品管理"
}
},
{
"path" : "addGoods",
"style" :
{
"navigationBarTitleText" : "添加商品"
}
},
{
"path" : "Pay",
"style" :
{
"navigationBarTitleText" : "付款"
}
},
{
"path": "goodsPay",
"style": {
"navigationBarTitleText": "积分订单"
}
},
{
"path" : "search",
"style" :
{
"navigationBarTitleText" : "搜索",
"navigationStyle": "custom"
}
},
{
"path" : "goodsClass",
"style" :
{
"navigationBarTitleText" : "分类",
"navigationStyle": "custom"
}
}
]
},
{
"root": "pages/article",
"pages": [{
"path": "articleCom",
"style": {
"navigationBarTitleText": "社区公告"
}
},
{
"path": "newsList",
"style": {
"navigationBarTitleText": "新闻公告"
}
},
{
"path": "news",
"style": {
"navigationBarTitleText": "新闻详情"
}
}
]
}, {
"root": "pages/userFunc",
"pages": [{
"path" : "setData",
"style" :
{
"navigationBarTitleText" : "编辑资料"
}
},
{
"path" : "integration",
"style" :
{
"navigationBarTitleText" : "积分明细"
}
},
{
"path" : "trade",
"style" :
{
"navigationBarTitleText" : "交易记录"
}
},
{
"path" : "editStore",
"style" :
{
"navigationBarTitleText" : "编辑店铺"
}
},
{
"path" : "statistics",
"style" :
{
"navigationBarTitleText" : "数据统计"
}
},
{
"path" : "set",
"style" :
{
"navigationBarTitleText" : "设置"
}
},
{
"path" : "bind",
"style" :
{
"navigationBarTitleText" : "绑定手机号"
}
},
{
"path" : "password",
"style" :
{
"navigationBarTitleText" : "修改支付密码"
}
},
{
"path" : "storeInter",
"style" :
{
"navigationBarTitleText" : "积分明细"
}
},
{
"path" : "withdrow",
"style" :
{
"navigationBarTitleText" : "提现"
}
},
{
"path" : "vip",
"style" :
{
"navigationBarTitleText" : "会员码",
"navigationStyle": "custom"
}
},
{
"path" : "promoteCode",
"style" :
{
"navigationBarTitleText" : "推广码"
}
},
{
"path" : "addressList",
"style" :
{
"navigationBarTitleText" : "收货地址"
}
},
{
"path" : "addAddress",
"style" :
{
"navigationBarTitleText" : "添加地址"
}
},
{
"path": "alliance-card",
"style": {
"navigationBarTitleText": "我的联盟卡",
"navigationStyle": "custom"
}
},
{
"path": "member-benefits",
"style": {
"navigationBarTitleText": "会员权益",
"navigationStyle": "custom"
}
},
{
"path": "promotion",
"style": {
"navigationBarTitleText": "我的推广",
"navigationStyle": "custom"
}
},
{
"path": "invite",
"style": {
"navigationBarTitleText": "邀请好友",
"navigationStyle": "custom"
}
},
{
"path": "feedback",
"style": {
"navigationBarTitleText": "意见反馈",
"navigationStyle": "custom"
}
},
{
"path": "about-us",
"style": {
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
},
{
"path": "coupon",
"style": {
"navigationBarTitleText": "优惠券",
"navigationStyle": "custom"
}
},
{
"path": "favorites",
"style": {
"navigationBarTitleText": "我的收藏",
"navigationStyle": "custom"
}
}
]
}
],
"tabBar": {
"color": "#8a8a8a",
"selectedColor": "#EC754B",
"backgroundColor": "#ffffff",
"list": [{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "/static/tabBar/home.png",
"selectedIconPath": "/static/tabBar/homed.png"
},
// {
// "text": "社区",
// "pagePath": "pages/index/community",
// "iconPath": "/static/tabBar/community.png",
// "selectedIconPath": "/static/tabBar/communityed.png"
// }
// ,
// {
// "text": "积分商城",
// "pagePath": "pages/index/shop",
// "iconPath": "/static/tabBar/shop.png",
// "selectedIconPath": "/static/tabBar/shoped.png"
// },
{
"text": "我的订单",
"pagePath": "pages/index/order",
"iconPath": "/static/tabBar/shop.png",
"selectedIconPath": "/static/tabBar/shoped.png"
},
{
"text": "个人中心",
"pagePath": "pages/index/user",
"iconPath": "/static/tabBar/user.png",
"selectedIconPath": "/static/tabBar/usered.png"
}]
}
}

View File

@@ -0,0 +1,22 @@
{
"uni-datetime-picker.selectDate": "选择日期",
"uni-datetime-picker.selectTime": "选择时间",
"uni-datetime-picker.selectDateTime": "选择日期时间",
"uni-datetime-picker.startDate": "开始日期",
"uni-datetime-picker.endDate": "结束日期",
"uni-datetime-picker.startTime": "开始时间",
"uni-datetime-picker.endTime": "结束时间",
"uni-datetime-picker.ok": "确定",
"uni-datetime-picker.clear": "清除",
"uni-datetime-picker.cancel": "取消",
"uni-datetime-picker.year": "年",
"uni-datetime-picker.month": "月",
"uni-calender.SUN": "日",
"uni-calender.MON": "一",
"uni-calender.TUE": "二",
"uni-calender.WED": "三",
"uni-calender.THU": "四",
"uni-calender.FRI": "五",
"uni-calender.SAT": "六",
"uni-calender.confirm": "确认"
}

View File

@@ -0,0 +1,8 @@
export class BaseConfig {
// protected static servesUrl: string = "http://192.168.0.142:5002";//线下
protected static servesUrl: string = "https://vp.xypays.cn";
protected static imgUrl : string = "https://vp.clouds.xypays.cn";
protected static mediaUrl: string = "http://byc1.xypays.cn/";
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
protected static payuploadUrl: string = "https://vp.xypays.cn";
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
<template>
<view>
<view class="" v-if="props.show">
<!-- <qf-image-cropper :src="props.url" :width="props.width" :height="props.height" :radius="20"
@crop="handleCrop"></qf-image-cropper> -->
<Tcropper :imageUrl="props.url" :width="props.width" @cancel="cancel" :height="props.height" :radius="0" @confirm='handleCrop' ></Tcropper>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { onBeforeUnmount, onMounted, reactive } from 'vue';
import Tcropper from "../uni_modules/t-cropper/components/t-cropper/t-cropper.vue"
import { Service } from '@/Service/Service'
// const props = defineProps({
// show : string,
// // url : string,
// // width : number,
// // height:number,
// // upType:string,
// // retFun:Function
// });
let props = defineProps(["show","url",'width','height','upType',"retFun","imgName"]);
onMounted(() => {
});
const handleCrop = (e) => {
let arr = e.tempFilePath.split('.')
let name = arr[arr.length - 1]
Service.uploadH5(e.tempFilePath, name ,(data:any)=>{
props.retFun( {name:props.imgName,url:data})
})
}
const cancel=()=>{
console.log(11111);
uni.$emit('cancle')
}
</script>
<style>
</style>

View File

@@ -0,0 +1,466 @@
<template>
<view class="member-code-page">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="Service.GoPageBack()" mode="aspectFit" />
<text class="nav-title">会员码</text>
<view class="nav-placeholder"></view>
</view>
<!-- 主要内容区域 -->
<view class="content">
<!-- 会员卡片 -->
<view class="member-card">
<!-- 用户信息 -->
<view class="user-section">
<image class="user-avatar" :src="Service.GetMateUrlByImg(userData.headImg)" mode="aspectFill" />
<view class="user-info">
<text class="user-name">{{ userData.nick }}</text>
<!-- ID和会员等级标签在一排 -->
<view class="tags-row">
<view class="user-id-tag" @click="copyMemberId">
<text class="ri-vip-crown-2-fill id-icon"></text>
<text class="id-text">ID: {{ userData.userNo }}</text>
<text class="ri-file-copy-line id-copy"></text>
</view>
<!-- <view class="user-member-tag">
<text class="ri-vip-crown-fill member-icon"></text>
<text class="member-text">黄金会员</text>
</view> -->
</view>
</view>
</view>
<!-- 分割线 -->
<view class="divider"></view>
<!-- 条形码区域 -->
<view class="barcode-section">
<u-barcode value="1234567890" :displayValue='false' :width="300"
:height="100" />
<text class="barcode-number">{{ user.memberCode }}</text>
</view>
<!-- 二维码区域 -->
<view class="qrcode-section">
<view class="qrcode-wrapper" @click="previewQrcode">
<view class="qrcode-placeholder">
<text class="ri-qr-code-line qrcode-icon"></text>
</view>
</view>
<text class="code-tip">向商家出示会员码,扫码享受积分优惠</text>
</view>
<!-- 刷新提示 -->
<view class="refresh-section">
<text class="ri-time-line refresh-icon"></text>
<text class="refresh-text">{{ refreshCountdown }}秒后自动刷新</text>
<view class="refresh-btn" @click="refreshCode">
<text class="ri-refresh-line btn-icon"></text>
<text class="btn-text">刷新</text>
</view>
</view>
</view>
<!-- 温馨提示 -->
<view class="tips-section">
<view class="tips-title">
<text class="ri-lightbulb-line tips-icon"></text>
<text class="title-text">温馨提示</text>
</view>
<view class="tips-list">
<text class="tips-item">• 会员码每分钟自动更新,确保使用安全</text>
<text class="tips-item">• 结账时请向商家出示此码</text>
<text class="tips-item">• 消费可获得积分,积分可抵扣现金</text>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { vpUserService, Service } from '@/Service/vp/vpUserService'
import { onLoad } from '@dcloudio/uni-app'
// 用户数据
const user = ref({
nickname: '美食达人',
avatar: 'https://picsum.photos/200/200?random=100',
points: 2580,
memberLevel: 'gold',
memberId: '8888888',
memberCode: 'VIP8888888888888'
})
let userData = ref<any>({})
// 刷新倒计时(秒)
const refreshCountdown = ref(60)
let refreshTimer = null
let countdownTimer = null
// 会员等级文本
const memberLevelText = computed(() => {
const levelMap = {
'gold': '黄金会员',
'platinum': '铂金会员',
'diamond': '钻石会员'
}
return levelMap[user.value.memberLevel] || '普通会员'
})
onLoad(() => {
fetchUserInfo()
})
// 获取用户信息
const fetchUserInfo = () => {
vpUserService.GetUserInfo().then(res => {
if (res.code == 0) {
userData.value = res.data.userInfo
console.log(userData.value);
}
})
}
// 刷新会员码
const refreshCode = () => {
// 生成新的会员码
const newCode = 'VIP' + Date.now().toString().slice(-12)
user.value.memberCode = newCode
// 重置倒计时
refreshCountdown.value = 60
uni.showToast({
title: '会员码已刷新',
icon: 'success',
duration: 1500
})
}
// 预览条形码
const previewBarcode = () => {
uni.showToast({
title: '条形码预览功能开发中',
icon: 'none'
})
}
// 预览二维码
const previewQrcode = () => {
uni.showToast({
title: '二维码预览功能开发中',
icon: 'none'
})
}
// 复制会员ID
const copyMemberId = () => {
uni.setClipboardData({
data: user.value.memberId,
success: () => {
uni.showToast({
title: '会员ID已复制',
icon: 'success',
duration: 2000
})
}
})
}
// 开始自动刷新
const startAutoRefresh = () => {
// 清除之前的定时器
if (refreshTimer) {
clearInterval(refreshTimer)
}
if (countdownTimer) {
clearInterval(countdownTimer)
}
// 倒计时定时器(每秒更新)
countdownTimer = setInterval(() => {
refreshCountdown.value--
if (refreshCountdown.value <= 0) {
refreshCountdown.value = 60
// 自动刷新会员码
const newCode = 'VIP' + Date.now().toString().slice(-12)
user.value.memberCode = newCode
}
}, 1000)
}
// 清理定时器
onUnmounted(() => {
if (refreshTimer) {
clearInterval(refreshTimer)
}
if (countdownTimer) {
clearInterval(countdownTimer)
}
})
</script>
<style lang="scss" scoped>
/* 引入全局标签样式 */
@import '@/styles/member-tags.scss';
/* 现代化会员码页面 */
.member-code-page {
min-height: 100vh;
background: linear-gradient(180deg, #FFF4E6 0%, #F5F5F5 100%);
display: flex;
flex-direction: column;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 40rpx;
}
/* 内容区域 */
.content {
flex: 1;
padding: 32rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
}
/* 会员卡片 */
.member-card {
width: 100%;
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(255, 107, 0, 0.12);
margin-bottom: 24rpx;
}
/* 用户信息区域 */
.user-section {
display: flex;
align-items: center;
margin-bottom: 32rpx;
}
.user-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 48rpx;
margin-right: 20rpx;
border: 3rpx solid #FF6B00;
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #222222;
margin-bottom: 10rpx;
}
/* 分割线 */
.divider {
height: 1rpx;
background: linear-gradient(90deg, transparent, #E0E0E0, transparent);
margin-bottom: 32rpx;
}
/* 条形码区域 */
.barcode-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
}
.barcode-wrapper {
width: 480rpx;
background: #F8F8F8;
border-radius: 12rpx;
padding: 20rpx 32rpx;
margin-bottom: 16rpx;
}
.barcode-placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.barcode-lines {
font-size: 40rpx;
color: #000000;
letter-spacing: 2rpx;
font-weight: 900;
line-height: 1;
}
.barcode-number {
font-size: 32rpx;
font-weight: 600;
color: #222222;
letter-spacing: 4rpx;
}
/* 二维码区域 */
.qrcode-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
}
.qrcode-wrapper {
width: 360rpx;
height: 360rpx;
background: #F8F8F8;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.qrcode-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #000000;
border-radius: 8rpx;
}
.qrcode-icon {
font-size: 200rpx;
color: #FFFFFF;
}
.code-tip {
font-size: 22rpx;
color: #999999;
text-align: center;
}
/* 刷新区域 */
.refresh-section {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.refresh-icon {
font-size: 24rpx;
color: #FF6B00;
}
.refresh-text {
font-size: 24rpx;
color: #666666;
text-align: center;
}
.refresh-btn {
display: flex;
align-items: center;
gap: 4rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
padding: 10rpx 20rpx;
border-radius: 24rpx;
}
.btn-icon {
font-size: 20rpx;
color: #FFFFFF;
}
.btn-text {
font-size: 24rpx;
color: #FFFFFF;
font-weight: 500;
}
/* 温馨提示 */
.tips-section {
width: 100%;
background: #FFFFFF;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.tips-title {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 16rpx;
}
.tips-icon {
font-size: 28rpx;
color: #FFA500;
}
.title-text {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.tips-item {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
}
</style>

View File

@@ -0,0 +1 @@
@import './styles/index.scss';

View File

@@ -0,0 +1,898 @@
<template>
<view class="profile-page">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 个人中心骨架屏 -->
<SkeletonProfile v-if="pageLoading" />
<!-- 个人中心内容 -->
<view v-else>
<!-- 顶部用户信息区 - 浅色清新设计 -->
<view class="user-header">
<view class="user-info">
<image class="user-avatar" :src="Service.GetMateUrlByImg(user.headImg)" mode="aspectFill" />
<view class="user-details">
<text class="user-name">{{ user.nick }}</text>
<!-- ID和会员等级标签在一排 -->
<view class="tags-row">
<view class="user-id-tag" @click="copyMemberId">
<text class="ri-vip-crown-2-fill id-icon"></text>
<text class="id-text">ID: {{user.userNo}}</text>
<text class="ri-file-copy-line id-copy"></text>
</view>
<!-- <view class="user-member-tag" v-if="user.memberLevel">
<text class="ri-vip-crown-fill member-icon"></text>
<text class="member-text">{{ memberLevelText }}</text>
</view> -->
</view>
</view>
</view>
</view>
<!-- 数据统计卡片 - 白色背景独立区域 -->
<view class="stats-card-wrapper">
<view class="stats-card">
<view class="stats-item" @click="Service.GoPage('/pages/userFunc/integration')">
<view class="stats-icon-box">
<text class="ri-coin-line stats-icon"></text>
</view>
<view class="stats-content">
<text class="stats-value">{{ accInfo.integral }}</text>
<text class="stats-label">积分</text>
</view>
</view>
<view class="stats-divider"></view>
<view class="stats-item" @click="Service.GoPage('/pages/userFunc/coupon')">
<view class="stats-icon-box">
<text class="ri-coupon-3-line stats-icon"></text>
</view>
<view class="stats-content">
<text class="stats-value">{{ discount }}</text>
<text class="stats-label">优惠券</text>
</view>
</view>
</view>
</view>
<!-- 会员码入口 -->
<view class="member-code-section" @click="Service.GoPage('/pages/userFunc/vip')">
<view class="member-code-card">
<view class="code-left">
<view class="code-title">
<text class="ri-qr-code-line code-icon"></text>
<text class="title-text">会员码</text>
</view>
<text class="code-desc">向商家出示会员码,享受积分优惠</text>
</view>
<view class="code-preview">
<text class="ri-qr-code-line preview-icon"></text>
</view>
</view>
</view>
<!-- 功能入口 - 网格布局 -->
<view class="function-section">
<view class="function-grid">
<view class="function-item" @click="Service.GoPage('/pages/userFunc/vip')">
<view class="function-icon-box" style="background: #FFF8E1;">
<text class="ri-file-list-3-line function-icon" style="color: #FF9800;"></text>
</view>
<text class="function-name">我的订单</text>
</view>
<view class="function-item" @click="Service.GoPage('/pages/userFunc/favorites')">
<view class="function-icon-box" style="background: #FFEBEE;">
<text class="ri-heart-line function-icon" style="color: #F44336;"></text>
</view>
<text class="function-name">我的收藏</text>
</view>
<!-- <view class="function-item" @click="goToAddress">
<view class="function-icon-box" style="background: #E3F2FD;">
<text class="ri-map-pin-line function-icon" style="color: #2196F3;"></text>
</view>
<text class="function-name">收货地址</text>
</view> -->
<view class="function-item" @click="serviceFunc">
<view class="function-icon-box" style="background: #F3E5F5;">
<text class="ri-customer-service-2-line function-icon" style="color: #9C27B0;"></text>
</view>
<text class="function-name">联系客服</text>
</view>
<view class="function-item" @click="showPhoneAuthModal = true">
<view class="function-icon-box" style="background: #e7e7f5;">
<text class="ri-smartphone-line function-icon" style="color: #8a77f5;"></text>
</view>
<text class="function-name">绑定手机号</text>
</view>
</view>
</view>
<!-- 功能列表 -->
<view class="menu-section">
<!-- 联盟卡功能块 -->
<view class="scan-section" @click="Service.GoPage('/pages/userFunc/alliance-card')">
<view class="scan-card">
<view class="scan-icon-box">
<text class="ri-bank-card-line scan-icon"></text>
</view>
<view class="scan-content">
<text class="scan-title">我的联盟卡</text>
<text class="scan-desc">绑定联盟卡,享受专属权益</text>
</view>
<text class="ri-arrow-right-s-line scan-arrow"></text>
</view>
</view>
<view class="menu-group">
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/member-benefits')">
<view class="item-left">
<text class="ri-gift-line item-icon" style="color: #FF6B00;"></text>
<text class="item-name">会员权益</text>
</view>
<view class="item-right">
<text class="item-desc">积分抵扣 · 专享优惠</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/promotion')">
<view class="item-left">
<text class="ri-team-line item-icon" style="color: #9C27B0;"></text>
<text class="item-name">我的推广</text>
</view>
<view class="item-right">
<text class="item-desc">{{ promotionCount }}人</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/invite')">
<view class="item-left">
<text class="ri-user-add-line item-icon" style="color: #10B981;"></text>
<text class="item-name">邀请好友</text>
</view>
<view class="item-right">
<text class="item-tag">赚积分</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
</view>
<view class="menu-group">
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/feedback')">
<view class="item-left">
<text class="ri-feedback-line item-icon" style="color: #3B82F6;"></text>
<text class="item-name">意见反馈</text>
</view>
<view class="item-right">
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/about-us')">
<view class="item-left">
<text class="ri-information-line item-icon" style="color: #64748B;"></text>
<text class="item-name">关于我们</text>
</view>
<view class="item-right">
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="menu-item" @click="goToSettings">
<view class="item-left">
<text class="ri-settings-4-line item-icon" style="color: #64748B;"></text>
<text class="item-name">设置</text>
</view>
<view class="item-right">
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
</view>
</view>
<!-- 手机号授权弹窗 -->
<view v-if="showPhoneAuthModal" class="phone-auth-overlay" @click="closePhoneAuthModal">
<view class="phone-auth-modal" @click.stop>
<!-- 关闭按钮 -->
<view class="modal-close" @click="closePhoneAuthModal">
<text class="ri-close-line close-icon"></text>
</view>
<!-- 顶部图标 -->
<view class="modal-header">
<view class="header-icon-box">
<text class="ri-wechat-fill header-icon"></text>
</view>
<text class="header-title">微信授权</text>
<text class="header-desc">使用微信授权快速获取手机号</text>
</view>
<!-- 微信授权按钮 -->
<button class="auth-btn wechat" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
<view class="btn-content">
<text class="ri-wechat-fill btn-icon"></text>
<text class="btn-text">微信授权手机号</text>
</view>
</button>
<!-- 温馨提示 -->
<view class="auth-tips">
<view class="tips-item">
<text class="ri-checkbox-circle-fill tips-icon"></text>
<text class="tips-text">授权后可快速联系在线客服</text>
</view>
<view class="tips-item">
<text class="ri-shield-check-fill tips-icon"></text>
<text class="tips-text">您的信息将严格保密</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
import { vpUserService } from '@/Service/vp/vpUserService'
import SkeletonProfile from '@/components/skeleton/skeleton-profile.vue'
let user = ref<any>({
headImg: ''
})
let accInfo=ref<any>({})
let pageLoading = ref<boolean>(true)
let showPhoneAuthModal = ref<boolean>(false)
let discount=ref(0)
onLoad(() => {
});
onShow(() => {
getUserinfo()
});
const gotopage = (item : any) => {
if (item.path) {
Service.GoPage(item.path)
}
}
const serviceFunc = (item : any, index : any) => {
if (index == 0) {
wx.openCustomerServiceChat({
extInfo: { url: 'https://work.weixin.qq.com/kfid/kfc959c128ce7801256' },
corpId: 'wwb1123fbb286554ab',
success(res) { },
fail(err) {
console.log(err, '失败')
// 失败回调
}
})
} else {
Service.GoPage(item.path)
}
}
const getUserinfo = () => {
vpUserService.GetUserInfo().then(res => {
pageLoading.value = false
user.value = res.data.userInfo
discount.value=res.data.discount
accInfo.value=res.data.accInfo
})
}
// 复制会员ID
const copyMemberId = () => {
console.log('000')
uni.setClipboardData({
data: user.value.userNo.toString(),
success: () => {
Service.Msg('会员ID已复制')
}
})
}
// 关闭手机号授权弹窗
const closePhoneAuthModal = () => {
showPhoneAuthModal.value = false
}
</script>
<style lang="scss" scoped>
/* 引入全局标签样式 */
@import '@/styles/member-tags.scss';
.profile-page {
min-height: 100vh;
background: #F5F5F5;
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FFF4E6, #FFE0B2);
height: var(--status-bar-height);
width: 100%;
}
/* 顶部用户信息区 */
.user-header {
background: linear-gradient(135deg, #FFF4E6, #FFE0B2);
padding: 20rpx 20rpx 50rpx;
}
.user-info {
display: flex;
align-items: center;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.5);
margin-right: 20rpx;
}
.user-details {
flex: 1;
}
.user-name {
display: block;
font-size: 36rpx;
font-weight: 600;
color: #222222;
margin-bottom: 10rpx;
}
/* 数据统计卡片 - 白色背景独立区域 */
.stats-card-wrapper {
background: #FFFFFF;
margin: -20rpx 0 20rpx;
border-radius: 0;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
position: relative;
z-index: 10;
}
.stats-card {
background: transparent;
padding: 24rpx 24rpx;
display: flex;
justify-content: space-around;
}
.stats-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
/* 统计图标盒子 */
.stats-icon-box {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: linear-gradient(135deg, #FFF4E6, #FFE0B2);
}
.stats-icon {
font-size: 32rpx;
color: #FF6B00;
}
/* 统计内容 */
.stats-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.stats-value {
font-size: 36rpx;
font-weight: 700;
color: #222222;
line-height: 1;
}
.stats-label {
font-size: 24rpx;
color: #999999;
}
.stats-divider {
width: 1rpx;
background: rgba(0, 0, 0, 0.08);
height: 64rpx;
align-self: center;
}
/* 会员码入口 */
.member-code-section {
padding: 20rpx;
}
.member-code-card {
background: linear-gradient(135deg, #FFF4E6, #FFE0B2);
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.15);
}
.code-left {
flex: 1;
}
.code-title {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.code-icon {
font-size: 32rpx;
color: #FF9800;
margin-right: 8rpx;
}
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #222222;
}
.code-desc {
font-size: 24rpx;
color: #666666;
margin-left: 40rpx;
}
.code-preview {
width: 80rpx;
height: 80rpx;
background: #FFFFFF;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
}
.preview-icon {
font-size: 48rpx;
color: #FF9800;
}
/* 功能入口 */
.function-section {
background: #FFFFFF;
margin: 0 20rpx 20rpx;
border-radius: 16rpx;
padding: 32rpx 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.function-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
}
.function-item {
display: flex;
flex-direction: column;
align-items: center;
}
.function-icon-box {
width: 88rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
margin-bottom: 12rpx;
}
.function-icon {
font-size: 44rpx;
}
.function-name {
font-size: 24rpx;
color: #222222;
text-align: center;
}
/* 菜单列表 */
.menu-section {
padding: 0 20rpx;
}
/* 扫一扫功能块 */
.scan-section {
margin-bottom: 20rpx;
}
.scan-card {
background: linear-gradient(135deg, #FF6B00, #FF9500);
border-radius: 16rpx;
padding: 28rpx 24rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 0, 0.25);
}
.scan-icon-box {
width: 72rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
}
.scan-icon {
font-size: 40rpx;
color: #FFFFFF;
}
.scan-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.scan-title {
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
}
.scan-desc {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.85);
}
.scan-arrow {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.7);
}
.menu-group {
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #F5F5F5;
}
.menu-item:last-child {
border-bottom: none;
}
.item-left {
display: flex;
align-items: center;
flex: 1;
}
.item-icon {
font-size: 40rpx;
margin-right: 16rpx;
}
.item-name {
font-size: 28rpx;
color: #222222;
font-weight: 500;
}
.item-right {
display: flex;
align-items: center;
}
.item-desc {
font-size: 24rpx;
color: #999999;
margin-right: 8rpx;
}
.item-tag {
background: linear-gradient(135deg, #FF6B00, #FF9800);
color: #FFFFFF;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
margin-right: 8rpx;
font-weight: 500;
}
.item-arrow {
font-size: 28rpx;
color: #CCCCCC;
}
/* 底部导航栏 - 精致简约风格 */
.tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-around;
padding: 12rpx 0 calc(12rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
z-index: 999;
height: calc(90rpx + env(safe-area-inset-bottom));
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0;
position: relative;
height: 100%;
}
.tabbar-icon {
font-size: 44rpx;
color: #CCCCCC;
margin-bottom: 8rpx;
transition: all 0.3s ease;
line-height: 1;
}
.tabbar-text {
font-size: 22rpx;
color: #CCCCCC;
font-weight: 400;
transition: all 0.3s ease;
line-height: 1.2;
}
/* 激活状态 */
.tabbar-item.active .tabbar-icon {
color: #FF6B00;
transform: scale(1.1);
}
.tabbar-item.active .tabbar-text {
color: #FF6B00;
font-weight: 500;
}
/* 手机号授权弹窗 */
.phone-auth-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: flex-end;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.phone-auth-modal {
width: 100%;
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
padding: 48rpx 32rpx 40rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
animation: slideUp 0.3s ease;
position: relative;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.modal-close {
position: absolute;
top: 24rpx;
right: 24rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
background: #F5F5F5;
border-radius: 30rpx;
z-index: 10;
}
.close-icon {
font-size: 32rpx;
color: #666666;
}
.modal-header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 48rpx;
}
.header-icon-box {
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
border-radius: 60rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.header-icon {
font-size: 56rpx;
color: #07C160;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #222222;
margin-bottom: 12rpx;
}
.header-desc {
font-size: 24rpx;
color: #999999;
text-align: center;
}
.auth-btn {
width: 100%;
height: 96rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
margin-bottom: 20rpx;
overflow: visible;
padding: 0;
}
.auth-btn.wechat {
background: linear-gradient(135deg, #07C160, #06AD56);
}
.auth-btn.wechat::after {
border: none;
}
.auth-btn.manual {
background: #F5F5F5;
}
.btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
}
.btn-icon {
font-size: 36rpx;
}
.auth-btn.wechat .btn-icon {
color: #FFFFFF;
}
.auth-btn.manual .btn-icon {
color: #666666;
}
.btn-text {
font-size: 30rpx;
font-weight: 600;
}
.auth-btn.wechat .btn-text {
color: #FFFFFF;
}
.auth-btn.manual .btn-text {
color: #222222;
}
.auth-tips {
margin-top: 32rpx;
padding: 24rpx;
background: #FFF9F0;
border-radius: 16rpx;
}
.tips-item {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.tips-item:last-child {
margin-bottom: 0;
}
.tips-icon {
font-size: 28rpx;
color: #FF9800;
flex-shrink: 0;
}
.tips-text {
font-size: 22rpx;
color: #666666;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,97 @@
# qf-image-cropper
## 图片裁剪插件
uniapp微信小程序图片裁剪插件支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。
### 平台支持:
1. 支持微信小程序移动端、PC端、开发者工具
2. 支持H5平台2.1.0版本起)
3. 支持APP平台2.1.5版本起Android、IOS
4. 其他平台暂未测试兼容性未知
### 支持功能:
1. 自定义裁剪尺寸
2. 定点等比例缩放移动端以双指触摸中心点为缩放中心点PC端以鼠标所在点为缩放中心点
3. 自由拖动:支持限制滑出边界,也支持回弹效果(滑动时可滑出边界,释放时回弹到边界)
4. 图片翻转:在裁剪尺寸非 1:1 的情况下,翻转时宽高无法铺满裁剪区域时,图片会自动放大到合适尺寸
5. 裁剪生成新图片
6. 本地选择图片
7. 可定制样式:可自由选择是否渲染裁剪边框、可伸缩裁剪顶角、参考线
8. 裁剪圆角图片:圆形、圆角矩形
### 属性说明
| 属性名 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| src | String | | 图片资源地址 |
| width | Number | 300 | 裁剪宽度 |
| height | Number | 300 | 裁剪高度 |
| showBorder | Boolean | true | 是否绘制裁剪区域边框 |
| showGrid | Boolean | true | 是否绘制裁剪区域网格参考线 |
| showAngle | Boolean | true | 是否展示四个支持伸缩的角 |
| areaScale | Number | 0.3 | 裁剪区域最小缩放倍数 |
| minScale | Number | 1 | 图片最小缩放倍数 |
| maxScale | Number | 5 | 图片最大缩放倍数 |
| checkRange | Boolean | true | 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 |
| backgroundColor | String | | 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性则生成图片存在一定的透明块 |
| bounce | Boolean | true | 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 |
| rotatable | Boolean | true | 是否支持翻转 |
| reverseRotatable | Boolean | false | 是否支持逆向翻转 |
| choosable | Boolean | true | 是否支持从本地选择素材 |
| gpu | Boolean | false | 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 |
| angleSize | Number | 20 | 四个角尺寸单位px |
| angleBorderWidth | Number | 2 | 四个角边框宽度单位px |
| zIndex | Number/String | | 调整组件层级 |
| radius | Number | | 裁剪图片圆角半径单位px |
| fileType | String | png | 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' |
| delay | Number | 1000 | 图片从绘制到生成所需时间单位ms<br>微信小程序平台使用 `Canvas 2D` 绘制时有效<br>如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间 |
| navigation | Boolean | true | 页面是否是原生标题栏:<br>H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 `"navigationStyle": "custom"` 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。<br>注因H5平台的窗口高度是包含标题栏的而屏幕触摸点的坐标是不包含的 |
| @crop | EventHandle | | 剪裁完成后触发event = { tempFilePath }。在H5平台下tempFilePath 为 base64 |
### 基本用法
```
<template>
<div>
<qf-image-cropper :width="500" :height="500" :radius="30" @crop="handleCrop"></qf-image-cropper>
</div>
</template>
<script>
import QfImageCropper from '@/components/qf-image-cropper/qf-image-cropper.vue';
export default {
components: {
QfImageCropper
},
methods: {
handleCrop(e) {
uni.previewImage({
urls: [e.tempFilePath],
current: 0
});
}
}
}
</script>
```
通过ref组件实例可在进入页面后直接打开相册选择图片
```
mounted() {
this.$refs.qfImageCropper.chooseImage({ sourceType: ['album'] });
}
```
### 使用说明
1.建议在`pages.json`中将引用插件的页面添加一下配置禁止下拉刷新和禁止页面滑动,防止出现性能或页面抖动等问题。
```
{
"enablePullDownRefresh": false,
"disableScroll": true
}
```
2.建议使用本插件不要设置过大宽高的目标图片尺寸建议1365x1365以内否则可能会导致如下问题
```
1.界面卡顿,内存占用过高
2.生成图片失真(模糊)
3.确定裁剪后一直显示 `裁剪中...`,该问题是由 `uni.canvasToTempFilePath` 无法回调导致,不同平台不同设备限制可能有所不同。
```
3.如裁剪后的图片存在偏移的问题,请检查是否受自己项目中父组件或全局样式影响。
4.src属性设置网络图片时图片资源必须是能触发 `getImageInfo` API 的 success 回调才可用于插件裁剪。因此小程序平台获取网络图片信息需先配置download域名白名单才能生效。
5.如果组件无法正常渲染且使用了 `v-if` 时,可尝试将 `v-if` 替换为 `v-show`
6.如果App端导入组件后无法正常渲染请尝试重新运行

View File

@@ -0,0 +1,791 @@
<template>
<view class="index-page">
<!-- 页面整体加载状态 -->
<view v-if="loding" class="page-loading">
<!-- 固定顶部区域 -->
<view class="top-fixed">
<view class="status-bar-placeholder"></view>
<view class="top-section-fixed">
<view class="location-box-skeleton">
<view class="skeleton-location-icon"></view>
<view class="skeleton-location-text"></view>
<view class="skeleton-arrow-icon"></view>
</view>
<view class="search-wrapper">
<view class="search-box-skeleton"></view>
</view>
</view>
</view>
<!-- 状态栏占位 - 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 占位区域 - 抵消固定顶部的高度 -->
<view class="top-placeholder"></view>
<!-- 分类骨架屏 -->
<view class="category-section">
<SkeletonCategory />
</view>
<!-- 商家列表骨架屏 -->
<view class="shop-list">
<view class="shop-waterfall">
<view class="waterfall-column">
<SkeletonShopCard v-for="i in 3" :key="'left-' + i" />
</view>
<view class="waterfall-column">
<SkeletonShopCard v-for="i in 3" :key="'right-' + i" />
</view>
</view>
</view>
</view>
<!-- 实际内容 -->
<view v-else>
<!-- 固定顶部区域 -->
<view class="top-fixed">
<view class="status-bar-placeholder"></view>
<view class="top-section-fixed">
<view class="location-box" @click.stop="choooseLocation()">
<up-icon name="map" color="#fff" size="12"></up-icon>
<text class="location-text" style="margin: 0 4rpx;" >{{ location }}</text>
<up-icon name="arrow-right" color="#fff" size="10"></up-icon>
</view>
<view class="search-wrapper">
<view class="search-box">
<input type="text" v-model="searchKey"
@confirm="Service.GoPage('/pages/goods/search?search='+searchKey)" class="search-input"
placeholder="请输入关键字" placeholder-class="search-placeholder" />
<view @click="Service.GoPage('/pages/goods/search?search='+searchKey)" class="search-btn">搜索
</view>
</view>
</view>
</view>
</view>
<!-- 状态栏占位 - 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 占位区域 - 抵消固定顶部的高度 -->
<view class="top-placeholder"></view>
<!-- 分类导航 - 三行五列 -->
<view class="category-section">
<view class="category-grid">
<view v-for="(category, index) in tabList" :key="index" class="category-item"
@click="Service.GoPage('/pages/goods/goodsClass?type='+category.assortId+'&name='+category.name )">
<view class="category-icon-box" :style="{ background: category.bgColor }">
<image :src="Service.GetMateUrlByImg(category.icon)" mode=""
style="width: 100%; height: 100%;"></image>
</view>
<text class="category-name">{{ category.name }}</text>
</view>
</view>
</view>
<!-- 商家列表 - 瀑布流布局 -->
<view class="shop-list">
<view class="shop-waterfall">
<!-- 左列 -->
<view class="waterfall-column">
<view v-for="shop in leftGoodsList" :key="shop.merchId" class="shop-card"
@click="Service.GoPage('/pages/community/merchantDetail?merchId='+shop.merchId)">
<view class="shop-cover-wrapper">
<image class="shop-cover" :src="Service.GetMateUrlByImg(shop.logo)" mode="aspectFill" />
<!-- 浮动标签 - 左下角距离 -->
<view class="float-tag float-distance">
<up-icon name="map" color="#fff" size="12"></up-icon>
<text class="tag-text">{{formatDistance(shop.distance)}}</text>
</view>
<!-- 右上角商家标签 -->
<view class="shop-badges">
<text v-if="shop.codeName=='联盟商家'" class="badge-partner">{{shop.codeName }}</text>
<text v-else class="badge-coop">{{shop.codeName }}</text>
</view>
</view>
<view class="shop-info">
<view class="shop-header">
<text class="shop-name">{{ shop.name }}</text>
</view>
<view class="shop-meta">
<text class="shop-sales">月售:{{ shop.sale }}</text>
<text class="shop-avg-price">人均{{ shop.price }}</text>
</view>
<view class="shop-footer">
<view class="shop-tags">
<view v-for="(coupon, idx) in shop.tips" :key="idx" :class="getTagClass(coupon)"
style="font-size: 22rpx; margin-left: 4rpx;" class="shop-tag">
{{ coupon }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 右列 -->
<view class="waterfall-column">
<view v-for="shop in rightGoodsList" :key="shop.merchId" class="shop-card"
@click="Service.GoPage('/pages/community/merchantDetail?merchId='+shop.merchId)">
<view class="shop-cover-wrapper">
<image class="shop-cover" :src="Service.GetMateUrlByImg(shop.logo)" mode="aspectFill" />
<!-- 浮动标签 - 左下角距离 -->
<view class="float-tag float-distance">
<up-icon name="map" color="#fff" size="12"></up-icon>
<text class="tag-text">{{formatDistance(shop.distance)}}</text>
</view>
<!-- 右上角商家标签 -->
<view class="shop-badges">
<text v-if="shop.code=='Discounts'" class="badge-partner">联盟商家</text>
<text v-else class="badge-coop">合作商家</text>
</view>
</view>
<view class="shop-info">
<view class="shop-header">
<text class="shop-name">{{ shop.name }}</text>
</view>
<view class="shop-meta">
<text class="shop-sales">月售:{{ shop.sale }}</text>
<text class="shop-avg-price">人均{{ shop.price }}</text>
</view>
<view class="shop-footer">
<view class="shop-tags">
<view v-for="(coupon, idx) in shop.tips" :key="idx" :class="getTagClass(coupon)"
style="font-size: 22rpx;" class="shop-tag">
{{ coupon }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<up-loadmore :status="status" />
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {
onLoad, onReachBottom, onShareAppMessage, onShareTimeline
} from '@dcloudio/uni-app'
import {
ref,
onMounted,
computed
} from 'vue'
import SkeletonCategory from '../../components/skeleton/skeleton-category.vue'
import SkeletonShopCard from '../../components/skeleton/skeleton-shop-card.vue'
import { vpLoginService, Service } from '@/Service/vp/vpLoginService'
import { vpMerchService } from '@/Service/vp/vpMerchService'
import { vpUserService } from '@/Service/vp/vpUserService'
// 数据
let location = ref('')
const categories = ref([])
const shops = ref([])
const currentCategory = ref('all')
let status = ref('nomore')
let loding = ref(true)
let longitude = ref(0)
let latitude = ref(0)
let searchKey = ref('')
let leftGoodsList = ref<Array<any>>([])
let rightGoodsList = ref<Array<any>>([])
let tabList = ref<Array<any>>([])
let page = ref(1)
let scene=ref('')
onLoad((data:any) => {
if(data.scene){
scene.value=data.scene
}
if (data.q) {
scene.value = decodeURIComponent(data.q).split('?')[1].split('=')[1]
}
getLocation()
})
// 分享
onShareAppMessage((res) => {
return {
path: '/pages/index/index'
}
});
onShareTimeline(() => {
return {
path: '/pages/index/index'
}
})
onReachBottom(()=>{
getList()
})
const getLocation = () => {
uni.getLocation({
type: 'wgs84',
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
getAddress()
if (!Service.GetUserToken()) {
login()
return
}
getdata()
},
fail: function (e) {
console.log(e);
}
});
}
const login = () => {
uni.getProvider({
service: 'oauth',
success: function (res : any) {
uni.login({
onlyAuthorize: true,
provider: res.provider,
success: function (loginRes) {
vpLoginService.WxLogin(loginRes.code, res.provider == 'weixin' ? 1 : 3, longitude.value, latitude.value, scene.value).then(content => {
if (content.code == 0) {
Service.SetUserToken(content.data.accToken)
getdata()
} else {
Service.Msg(content.msg)
}
})
}
})
}
});
}
const getdata = () => {
leftGoodsList.value = []
rightGoodsList.value = []
status.value = 'loadmore'
page.value = 1
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpMerchService.GetMerchList('', '', longitude.value, latitude.value,0,0, page.value).then(res => {
loding.value = false
res.data.merchList.forEach((goodsItem : any, GoodsIndex : number) => {
if (GoodsIndex % 2 == 0) {
leftGoodsList.value.push(goodsItem)
} else {
rightGoodsList.value.push(goodsItem)
}
})
tabList.value = res.data.assortList
status.value = res.data.merchList.length == 10 ? 'loadmore' : 'nomore'
page.value++
})
}
const getAddress = () => {
vpUserService.GetAddressInfo(longitude.value, latitude.value).then(res => {
if (res.code == 0) {
location.value = res.data.addrName
} else {
Service.Msg(res.msg)
}
})
}
const choooseLocation = () => {
uni.chooseLocation({
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
location.value = res.address
getdata()
}
});
}
// 格式化距离
const formatDistance = (distance : any) => {
if (distance < 1) {
return `${Number(distance*1000).toFixed(1)}m`
}
return `${(distance).toFixed(1)}km`
}
// 根据标签文本获取样式类
const getTagClass = (tagText : string) => {
const tagMap = {
'可用积分': 'tag-points-available',
'可用券': 'tag-coupon'
}
return tagMap[tagText] || 'tag-points'
}
</script>
<style lang="scss" scoped>
.index-page {
min-height: 100vh;
background: #F5F5F5;
padding-bottom: 20rpx;
}
/* 状态栏占位 - 沉浸式 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 固定顶部区域 - 始终固定 */
.top-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
}
.status-bar-placeholder {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
.top-section-fixed {
background: linear-gradient(135deg, #FF6B00, #FF9500);
padding: 65rpx 20rpx 28rpx;
}
/* 占位区域 - 抵消固定顶部的高度 */
.top-placeholder {
height: calc(var(--status-bar-height) + 170rpx);
width: 100%;
}
.location-box {
display: flex;
align-items: center;
margin-bottom: 28rpx;
}
.location-icon-small {
font-size: 28rpx;
color: #FFFFFF;
margin-right: 8rpx;
}
.location-text {
font-size: 30rpx;
color: #FFFFFF;
font-weight: 500;
margin-right: 8rpx;
}
.arrow-icon {
font-size: 24rpx;
color: #FFFFFF;
}
/* 搜索区域 */
.search-wrapper {
display: flex;
align-items: center;
margin-top: 20rpx;
}
/* 搜索框 */
.search-box {
flex: 1;
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 32rpx;
padding: 8rpx 28rpx;
gap: 8rpx;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #222222;
height: 48rpx;
line-height: 48rpx;
}
.search-placeholder {
color: #999999;
}
.search-btn {
background: #FF6B00;
color: #FFFFFF;
font-size: 24rpx;
padding: 6rpx 18rpx;
border-radius: 20rpx;
font-weight: 500;
flex-shrink: 0;
}
/* 分类导航 - 三行五列 */
.category-section {
background: #FFFFFF;
padding: 32rpx 20rpx;
margin-bottom: 20rpx;
}
.category-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 32rpx 20rpx;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
}
.category-icon-box {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
margin-bottom: 12rpx;
}
.category-icon-box text {
font-size: 44rpx;
color: #FFFFFF;
}
.category-name {
font-size: 24rpx;
color: #222222;
white-space: nowrap;
}
/* 商家列表 - 瀑布流布局 */
.shop-list {
padding: 0 20rpx;
}
.shop-waterfall {
display: flex;
gap: 16rpx;
}
.waterfall-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.shop-card {
background: #FFFFFF;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
break-inside: avoid;
}
.shop-cover-wrapper {
position: relative;
width: 100%;
height: 200rpx;
}
.shop-cover {
width: 100%;
height: 100%;
}
/* 浮动标签 */
.float-tag {
position: absolute;
padding: 2rpx 6rpx;
border-radius: 3rpx;
font-size: 16rpx;
backdrop-filter: blur(10rpx);
-webkit-backdrop-filter: blur(10rpx);
display: flex;
align-items: center;
gap: 2rpx;
}
.float-distance {
left: 6rpx;
bottom: 6rpx;
background: rgba(0, 0, 0, 0.65);
color: #FFFFFF;
}
.float-distance .tag-icon {
font-size: 14rpx;
color: #FFFFFF;
}
.float-distance .tag-text {
font-size: 16rpx;
color: #FFFFFF;
}
.shop-info {
padding: 16rpx;
}
// 右上角
.shop-badges {
position: absolute;
top: 16rpx;
right: 16rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
align-items: flex-end;
}
/* 商家信息 */
.shop-header {
margin-bottom: 12rpx;
}
.shop-name {
font-size: 28rpx;
font-weight: 500;
color: #222222;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
}
.shop-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.shop-sales {
font-size: 22rpx;
color: #999999;
}
.shop-avg-price {
font-size: 26rpx;
color: #FF6B00;
font-weight: 700;
}
/* 商家标签 */
.shop-tags {
display: flex;
flex-wrap: wrap;
gap: 4rpx;
}
/* 标签 */
.tag-badge {
display: inline-block;
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-size: 20rpx;
font-weight: 500;
margin-right: 12rpx;
flex-shrink: 0;
}
.tag-coupon {
background: #FFF8E1;
color: #FF6B00;
}
.shop-price {
font-size: 28rpx;
font-weight: 600;
color: #FF4D4F;
margin-right: 8rpx;
}
.shop-original {
font-size: 22rpx;
color: #999999;
text-decoration: line-through;
}
/* 空状态 */
.empty {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.empty-icon {
font-size: 120rpx;
color: #CCCCCC;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
/* 底部导航栏 - 小巧精致风格 */
.tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-around;
padding: 12rpx 0 calc(12rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
z-index: 999;
height: calc(90rpx + env(safe-area-inset-bottom));
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0;
position: relative;
height: 100%;
}
.tabbar-icon {
font-size: 44rpx;
color: #CCCCCC;
margin-bottom: 8rpx;
transition: all 0.3s ease;
line-height: 1;
}
.tabbar-text {
font-size: 22rpx;
color: #CCCCCC;
font-weight: 400;
transition: all 0.3s ease;
line-height: 1.2;
}
/* 激活状态 */
.tabbar-item.active .tabbar-icon {
color: #FF6B00;
transform: scale(1.1);
}
.tabbar-item.active .tabbar-text {
color: #FF6B00;
font-weight: 500;
}
/* 首页头部骨架屏样式 */
.location-box-skeleton {
display: flex;
align-items: center;
margin-bottom: 28rpx;
}
.skeleton-location-icon {
width: 28rpx;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
margin-right: 8rpx;
}
.skeleton-location-text {
width: 200rpx;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
margin-right: 8rpx;
}
.skeleton-arrow-icon {
width: 24rpx;
height: 24rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
}
.search-box-skeleton {
width: 100%;
height: 60rpx;
border-radius: 32rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
}
@keyframes loading-white {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.page-loading {
min-height: 100vh;
background: #F5F5F5;
}
</style>

View File

@@ -0,0 +1,186 @@
# t-cropper
> **t-cropper 一款高性能移动端图片裁剪工具**
## 平台兼容
| App | H5 | 微信小程序 | 支付宝小程序 |
| :---: | :---: | :----------: | :-----------: |
| √ | √ | √ | √ |
### 属性说明
|属性 |类型 |默认 |备注 |
| :--------: | :-----: | :----: | :----: |
| mode |String | "ratio" | 裁剪模式|
| imageUrl |String | " " | 需要裁剪的图片路径|
| width |Number | 200 | 图片裁剪后的宽度,固定大小时有效|
| height |Number | 200 | 图片裁剪后的高度,固定大小时有效|
| maxWidth |Number | 1024 | 图片裁剪后的最大宽度 |
| maxHeight |Number | 1024 | 图片裁剪后的最大高度 |
| scaleRatio |Number | 0.7 | 裁剪比列缩放,建议不超过0.95 |
| minRatio |Number | 1 | 最小缩放 |
| maxRatio |Number | 3 | 最大缩放 |
| radius |Number | 0 | 裁剪图片圆角半径单位px |
| delay |Number | 250 | 确定按钮快速重复点击时间 |
| isRotateBtn |Boolean | true | 是否显示旋转按钮 |
### mode有效值
| 模式 |值 |说明 |
| :-----: | :-----: | :----: |
| 固定模式 |fixed | 裁剪出指定大小的图片,一般用于头像上传 |
| 等比缩放 |ratio | 限定宽高比,裁剪大小不固定 |
| 自由模式 |free | 不限定宽高比,裁剪大小不固定 |
### 事件说明
|事件名称 |说明 |返回 |
| :--------: | :-----: | :----: |
| confirm |点击确定按钮 | object |
| cancel |点击取消按钮 | - |
### 示例
```html
<template>
<view>
<t-cropper
mode="ratio"
:imageUrl="imageUrl"
:width="500"
:height="500"
:radius="100"
:delay="150"
@cancel="onCancel"
@confirm="onConfirm"
></t-cropper>
<view class="preview">
<image
v-for="(item, index) in resultUrl"
:key="item.id"
class="images"
@click="prviewImgae(index, item.url)"
:src="item.url"
/>
</view>
<button class="button" type="primary" @click="selectFile">选择图片</button>
</view>
</template>
<script setup>
import { reactive, toRefs } from "vue";
const model = reactive({
imageUrl: "",
resultUrl: [],
});
const { resultUrl, imageUrl } = toRefs(model);
// 使用uni.compressImage压缩图片
const compressImage = () => {
uni.chooseImage({
count: 1,
sizeType: "original",
success: (res) => {
console.log("original", res);
// 示例防止大图片文件的Image无法直接临时路径显示图片
uni.showLoading({
title: "处理中...",
mask: true,
});
// 使用uni.compressImage压缩图片
uni.compressImage({
src: res.tempFilePaths[0],
quality: 80, // 压缩质量
success: (compressRes) => {
model.imageUrl = compressRes.tempFilePath;
},
fail: (err) => {
console.error("图片压缩失败:", err);
},
complete: () => {
uni.hideLoading(); // 关闭loading
},
});
},
});
};
// 使用默认压缩方式
const defaultCompressImage = () => {
uni.chooseImage({
count: 1,
sizeType: "compressed",
success: (res) => {
model.imageUrl = res.tempFilePaths[0];
},
});
};
/**
*** 特别声明:在使用 uni.chooseImage 选择的大图片文件无法直接在 Image 组件中显示,通常涉及到以下可能的问题和限制。
*** 图片大小和尺寸限制:移动设备和浏览器对于能够加载和处理的图片大小有限制,如果选择的图片文件尺寸过大,可能无法正常加载和显示。
*** 性能问题:大图片文件可能会导致页面加载缓慢或者卡顿,尤其是在移动设备上。
*** 内存问题:加载大图片可能会消耗大量的内存资源,特别是在移动设备上,可能导致内存不足或者页面崩溃的问题。
*** 解决参考方案如下:
*** 防止选择大文件图片后无法在Image中直接临时路径显示图片导致无法在裁剪插件中显示
*** 根据项目需要对大尺寸图片进行压缩、对图片质量要求高的需要提前上传至oss进行采用网络图片进行裁剪。
*/
const selectFile = () => {
// 推荐使用其他压缩方式,这里只是简单大图片压缩示例-经供思路参考切勿使用
// 示例一uni.compressImage压缩图片
// compressImage();
// 示例二:使用自带压缩图
defaultCompressImage();
};
// 关闭裁剪
const onCancel = () => {
model.imageUrl = "";
};
// 裁剪确认
const onConfirm = (res) => {
// 设置url的值显示控件
const params = {
id: new Date().getTime(),
url: res.tempFilePath,
};
model.resultUrl.push(params);
model.imageUrl = "";
};
// 预览图片
const prviewImgae = (index, url) => {
uni.previewImage({
current: index, // 当前资源下标
urls: [url],
});
};
</script>
<style lang="scss" scoped>
.preview {
padding: 32rpx;
.images {
margin: 10rpx;
width: 200rpx;
height: 200rpx;
}
}
.button {
margin: 0 20rpx;
}
</style>
```
### 注意
1.uni-app版本不断更新插件有时无法适应新版本感谢大家及时提交bug但希望大家手下留情不要轻易给差评

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"uni-datetime-picker.selectDate": "select date",
"uni-datetime-picker.selectTime": "select time",
"uni-datetime-picker.selectDateTime": "select date and time",
"uni-datetime-picker.startDate": "start date",
"uni-datetime-picker.endDate": "end date",
"uni-datetime-picker.startTime": "start time",
"uni-datetime-picker.endTime": "end time",
"uni-datetime-picker.ok": "ok",
"uni-datetime-picker.clear": "clear",
"uni-datetime-picker.cancel": "cancel",
"uni-datetime-picker.year": "-",
"uni-datetime-picker.month": "",
"uni-calender.MON": "MON",
"uni-calender.TUE": "TUE",
"uni-calender.WED": "WED",
"uni-calender.THU": "THU",
"uni-calender.FRI": "FRI",
"uni-calender.SAT": "SAT",
"uni-calender.SUN": "SUN",
"uni-calender.confirm": "confirm"
}

View File

@@ -0,0 +1,684 @@
<template>
<view class="search-page">
<!-- 顶部导航栏 - 橙色背景 -->
<view class="nav-bar">
<view class="nav-content">
<view class="nav-back" @click="Service.GoPageBack()">
<image class="back-icon" src="/static/icons/back.svg" mode="aspectFit" />
</view>
<view class="search-input-wrapper" >
<image class="search-icon" src="https://img.icons8.com/ios-glyphs/30/999999/search--v1.png"
mode="aspectFit" />
<input class="search-input" v-model="searchKeyword" placeholder="搜索商家或商品"
placeholder-class="search-placeholder" @input="onSearchInput" @confirm="onSearch" focus />
<!-- <image v-if="searchKeyword" class="clear-icon"
src="https://img.icons8.com/ios-glyphs/30/999999/close-window--v1.png" mode="aspectFit"
@click="clearSearch" /> -->
</view>
<!-- <text class="search-btn" @click="onSearch">搜索</text> -->
</view>
</view>
<!-- 搜索内容区域 -->
<view class="content-wrapper">
<!-- 搜索历史 - 无搜索内容时显示 -->
<view v-if="hasSearched" class="search-history-section">
<!-- 搜索历史 -->
<view v-if="searchHistory.length > 0" class="history-section">
<view class="section-header">
<text class="section-title">搜索历史</text>
<image class="delete-icon" src="https://img.icons8.com/ios-glyphs/30/999999/delete--v1.png"
mode="aspectFit" @click="clearHistory" />
</view>
<view class="history-tags">
<view v-for="(item, index) in searchHistory" :key="index" class="history-tag"
@click="searchFromHistory(item)">
<text class="history-tag-text">{{ item }}</text>
</view>
</view>
</view>
<!-- 热门搜索 -->
<view class="hot-section">
<view class="section-header">
<text class="section-title">热门搜索</text>
</view>
<view class="hot-tags">
<view v-for="(item, index) in hotSearches" :key="index" class="hot-tag"
@click="searchFromHistory(item)">
<text class="hot-tag-text" :class="{ 'top-tag': index < 3 }">{{ item }}</text>
</view>
</view>
</view>
</view>
<!-- 搜索结果 - 有搜索内容时显示 -->
<view class="search-results-section">
<!-- 搜索建议 - 输入时显示 -->
<!-- <view class="suggestions-section">
<view v-for="(item, index) in searchSuggestions" :key="index" class="suggestion-item"
@click="searchFromHistory(item)">
<image class="suggestion-icon" src="https://img.icons8.com/ios-glyphs/30/999999/search--v1.png"
mode="aspectFit" />
<text class="suggestion-text">{{ item }}</text>
</view>
</view> -->
<!-- 搜索结果列表 -->
<view class="results-list">
<!-- 商家列表 -->
<view class="shop-list">
<view v-for="shop in goodsList" :key="shop.merchId" class="shop-card" @click="Service.GoPage('/pages/goods/goodsDetail?merchId='+shop.merchId)">
<!-- 左侧图片 -->
<image class="shop-image" :src="Service.GetMateUrlByImg(shop.logo)" mode="aspectFill" />
<!-- 右侧信息 -->
<view class="shop-info">
<!-- 店名 -->
<view class="shop-header">
<text class="shop-name">{{ shop.name }}</text>
</view>
<!-- 评分和销量 -->
<view class="shop-info-left">
<view class="shop-rating">
<text class="rating-score">{{ shop.score }}</text>
<text class="rating-unit">分</text>
</view>
<text class="shop-type">堂食店</text>
<text class="shop-sales">月售{{ shop.sale }}</text>
</view>
<!-- 距离、用时、人均价格 - 同一行 -->
<view class="shop-info-row">
<!-- 距离和用时 - 左侧 -->
<view class="shop-info-left">
<view class="distance-wrapper">
<text class="ri-map-pin-2-line location-icon"></text>
<text class="distance-text">{{ formatDistance(shop.distance ) }}</text>
</view>
</view>
<!-- 人均价格 - 右侧 -->
<view class="shop-avg-price-inline">
<text class="avg-price-small">¥</text>
<text class="avg-price-value-small">{{ shop.price }}</text>
<text class="avg-price-text-small">人均</text>
</view>
</view>
<!-- 底部标签 -->
<view class="shop-bottom-tags">
<text v-for="(tag, index) in shop.tips" :key="index" class="shop-tag" style="font-size: 24rpx;"
:class="getTagClass(tag)">
{{ tag }}
</text>
</view>
<!-- 右上角标签 -->
<view class="shop-badges" style="top: 24rpx;" >
<text v-if="shop.code=='Discounts'" class="badge-partner" style="font-size: 24rpx;">联盟商家</text>
<text v-else class="badge-coop" style="font-size: 24rpx;" >合作商家</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<up-loadmore :status="status" />
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {
ref
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import { Service } from "@/Service/Service"
import { vpMerchService } from '@/Service/vp/vpMerchService'
// 搜索关键词
const searchKeyword = ref('')
// 是否已搜索
const hasSearched = ref(false)
// 是否显示搜索结果
const showResults = ref(false)
// 搜索结果
const searchResults = ref([])
// 搜索历史
const searchHistory = ref(['汉堡', '奶茶', '炸鸡', '披萨'])
// 热门搜索
const hotSearches = ref(['汉堡王', '肯德基', '麦当劳', '星巴克', '必胜客', '喜茶', '奈雪的茶', '瑞幸咖啡'])
// 模拟商家数据
const mockShops = [{
id: 1,
name: '河南省许昌市汉堡王(宏安店)',
cover: 'https://img.icons8.com/color/96/000000/burger.png',
rating: '4.8',
sales: '2000+',
distance: '500m',
time: '25分钟',
avgPrice: '25.0',
coupons: ['12%积分', '可用券'],
badgeType: 'partner' // 只使用一个类型标签
}
]
let longitude=ref(0)
let latitude=ref(0)
let status=ref('nomore')
let goodsList=ref<Array<any>>([])
let page=ref(1)
// 页面加载
onLoad((data : any) => {
searchKeyword.value = data.search
getLocation()
})
// 搜索建议
const searchSuggestions = ref([])
const getLocation = () => {
uni.getLocation({
type: 'wgs84',
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
getdata()
},
fail: function (e) {
console.log(e);
}
});
}
const getdata = () => {
goodsList.value=[]
status.value = 'loadmore'
page.value = 1
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpMerchService.GetMerchList('', searchKeyword.value, longitude.value, latitude.value, page.value).then(res => {
goodsList.value=[...goodsList.value,...res.data.merchList]
status.value = res.data.merchList.length == 10 ? 'loadmore' : 'nomore'
page.value++
})
}
// 输入搜索关键词
const onSearchInput = () => {
if (searchKeyword.value.trim()) {
// 模拟搜索建议
searchSuggestions.value = hotSearches.value.filter(item =>
item.toLowerCase().includes(searchKeyword.value.toLowerCase())
).slice(0, 5)
} else {
searchSuggestions.value = []
}
}
// 执行搜索
const onSearch = () => {
const keyword = searchKeyword.value.trim()
if (!keyword) {
uni.showToast({
title: '请输入搜索关键词',
icon: 'none'
})
return
}
// 保存到搜索历史
if (!searchHistory.value.includes(keyword)) {
searchHistory.value.unshift(keyword)
if (searchHistory.value.length > 10) {
searchHistory.value = searchHistory.value.slice(0, 10)
}
}
// 执行搜索 - 使用模拟数据
// hasSearched.value = true
showResults.value = true
// 过滤模拟数据
searchResults.value = mockShops.filter(shop =>
shop.name.toLowerCase().includes(keyword.toLowerCase())
)
// 如果没有匹配结果,显示所有数据(方便演示)
if (searchResults.value.length === 0) {
searchResults.value = mockShops
}
}
// 从历史/热门搜索
const searchFromHistory = (keyword) => {
searchKeyword.value = keyword
onSearch()
}
// 清除搜索
const clearSearch = () => {
searchKeyword.value = ''
searchSuggestions.value = []
showResults.value = false
}
// 清除搜索历史
const clearHistory = () => {
uni.showModal({
title: '提示',
content: '确定清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
searchHistory.value = []
}
}
})
}
// 获取标签样式类
const getTagClass = (tagText) => {
const tagMap = {
'12%积分': 'tag-points',
'15%积分': 'tag-points',
'可用积分': 'tag-points-available',
'可用券': 'tag-coupon'
}
return tagMap[tagText] || 'tag-default'
}
// 格式化距离
const formatDistance = (distance : any) => {
if (distance < 1000) {
return `${distance}m`
}
return `${(distance / 1000).toFixed(1)}km`
}
</script>
<style lang="scss" scoped>
.search-page {
min-height: 100vh;
background: #F5F5F5;
}
/* 顶部导航栏 */
.nav-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: calc(var(--status-bar-height) + 146rpx);
position: relative;
z-index: 1;
}
.nav-content {
position: absolute;
left: 0;
right: 0;
top: 90rpx;
height: 88rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
}
.nav-back {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.search-input-wrapper {
width: 60%;
background: #FFFFFF;
border-radius: 20px;
height: 36px;
display: flex;
align-items: center;
padding: 0 12px;
margin: 0 8px;
}
.search-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
.search-input {
flex: 1;
font-size: 14px;
color: #222222;
height: 36px;
}
.search-placeholder {
color: #999999;
}
.clear-icon {
width: 16px;
height: 16px;
margin-left: 8px;
}
.search-btn {
font-size: 15px;
color: #FFFFFF;
font-weight: 500;
flex-shrink: 0;
padding: 0 8px;
}
/* 搜索内容区域 */
.content-wrapper {
position: relative;
z-index: 2;
background: #FAFAFA;
min-height: calc(100vh - var(--status-bar-height) - 60px);
}
/* 搜索历史和热门搜索 */
.search-history-section {
padding: 16px;
}
.history-section,
.hot-section {
margin-bottom: 24px;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333333;
}
.delete-icon {
width: 20px;
height: 20px;
padding: 4px;
}
.history-tags,
.hot-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.history-tag,
.hot-tag {
background: #FFFFFF;
border: 1px solid #E0E0E0;
border-radius: 4px;
padding: 6px 12px;
}
.history-tag-text,
.hot-tag-text {
font-size: 14px;
color: #333333;
}
.hot-tag .top-tag {
color: #FF6B00;
font-weight: 500;
}
/* 搜索建议 */
.suggestions-section {
background: #FFFFFF;
margin: 16px;
border-radius: 8px;
overflow: hidden;
}
.suggestion-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #F0F0F0;
}
.suggestion-item:last-child {
border-bottom: none;
}
.suggestion-icon {
width: 16px;
height: 16px;
margin-right: 12px;
opacity: 0.5;
}
.suggestion-text {
font-size: 14px;
color: #333333;
}
/* 搜索结果 */
.search-results-section {
padding: 16px 0;
}
.results-list {
background: transparent;
}
/* 商家列表 */
.shop-list {
padding: 0 10px;
}
.shop-card {
background: #FFFFFF;
border: 1px solid #E0E0E0;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
display: flex;
gap: 12px;
position: relative;
}
/* 商家图片 */
.shop-image {
width: 100px;
height: 100px;
border-radius: 6px;
flex-shrink: 0;
background: #F5F5F5;
}
/* 商家信息 */
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* 店名 */
.shop-header {
margin-bottom: 6px;
padding-right: 60px;
}
.shop-name {
font-size: 16px;
font-weight: bold;
color: #333333;
word-wrap: break-word;
word-break: break-all;
line-height: 1.4;
}
/* 左侧信息:评分、类型、销量 */
.shop-info-left {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.shop-rating {
display: flex;
align-items: baseline;
}
.rating-score {
font-size: 14px;
font-weight: 600;
color: #FF9800;
}
.rating-unit {
font-size: 10px;
color: #FF9800;
margin-left: 2px;
}
.shop-info-left .shop-type {
font-size: 12px;
color: #4CAF50;
font-weight: 500;
}
.shop-info-left .shop-sales {
font-size: 12px;
color: #666666;
}
/* 距离、用时、人均价格 - 同一行 */
.shop-info-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
.distance-wrapper {
display: flex;
align-items: center;
gap: 4px;
}
.location-icon {
font-size: 12px;
color: #999999;
}
.distance-text {
font-size: 12px;
color: #666666;
}
.time-text {
font-size: 12px;
color: #666666;
margin-left: 8px;
}
/* 人均价格 - 小号内联样式 */
.shop-avg-price-inline {
display: inline-flex;
align-items: baseline;
}
.avg-price-small {
font-size: 11px;
color: #FF6B00;
font-weight: 600;
}
.avg-price-value-small {
font-size: 15px;
color: #FF6B00;
font-weight: 700;
line-height: 1;
}
.avg-price-text-small {
font-size: 11px;
color: #FF6B00;
margin-left: 2px;
}
/* 底部标签 */
.shop-bottom-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* 无搜索结果 */
.no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
}
.no-results-icon {
width: 80px;
height: 80px;
margin-bottom: 16px;
opacity: 0.5;
}
.no-results-text {
font-size: 16px;
color: #333333;
margin-bottom: 8px;
}
.no-results-hint {
font-size: 14px;
color: #999999;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
<template>
<view style="margin: 20rpx 30rpx;">
<view class="" style="border-radius: 20rpx; background-color: #fff; padding: 20rpx; ">
<view class="" style="font-weight: 600; font-size: 30rpx;">
商品封面
</view>
<view class=""
style=" margin-top: 20rpx; width: 100%; height: 320rpx; border: 4rpx dashed #e2e2e2; border-radius: 20rpx; overflow: hidden; ">
<view class=""
style="width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; ">
<img :src="Service.GetIconImg('/static/goods/photo.png')" style="width: 80rpx; height: 80rpx;"
alt="" />
<view class="" style=" color: #666666; font-size: 28rpx;">
上传图片
</view>
</view>
<!-- <image :src="Service.GetMateUrlByImg('/static/dele/dele4.jpg')" mode="aspectFill" style="width: 100%; height: 100%; " alt="" /> -->
</view>
</view>
<view class="" style=" margin-top: 20rpx; border-radius: 20rpx; background-color: #fff; padding: 20rpx; ">
<up-form labelWidth='90' :labelStyle="{'font-weight': 600 }" labelPosition="top" :model="goodsInfo"
ref="form1">
<up-form-item label="商品名称" prop="goodsInfo.name" :borderBottom="false" ref="item1">
<up-input :customStyle="{'border-radius': '15rpx' }" placeholder="请输入商品名称" border='surround'
v-model="goodsInfo.name"></up-input>
</up-form-item>
<up-form-item label="价格" prop="goodsInfo.sex" :borderBottom="false">
<up-input prefixIcon='rmb' :prefixIconStyle="{ 'color':'#000','font-weight': 600 }"
:customStyle="{'border-radius': '15rpx' }" placeholder="请输入价格" v-model="goodsInfo.prince"
border="surround"></up-input>
</up-form-item>
<up-form-item label="描述" prop="goodsInfo.sex" :borderBottom="false">
<up-textarea v-model="goodsInfo.des" placeholder="请输入商品描述"></up-textarea>
</up-form-item>
<up-form-item label="分类" prop="goodsInfo.sex" :borderBottom="false">
<view class=""
style="padding: 12rpx 18rpx; border-radius: 7px; border: 1rpx solid #e2e2e2; width: 100%; ">
<view class="" @click="showClass=true"
style="display: flex; height: 48rpx; align-items: center; justify-content: space-between;">
<view class="" :style="{ 'color':goodsInfo.class? '#000':'#c0c4cc' } ">
{{ goodsInfo.class?goodsInfo.class:'请选择分类'}}
</view>
<view class="">
<up-icon name="arrow-right" size="14" color='#666666' :bold='true'></up-icon>
</view>
</view>
</view>
</up-form-item>
<up-form-item label="状态" prop="goodsInfo.sex" :borderBottom="false">
<up-radio-group v-model="goodsInfo.status" placement="row">
<up-radio :customStyle="{marginBottom: '8px'}" v-for="(item, index) in radiolist" :key="index"
:label="item.name" :name="item.value" @change="radioChange">
</up-radio>
</up-radio-group>
</up-form-item>
<up-form-item label="标签" :borderBottom="false">
<view v-for="(item, index) in tabsList" @click="clickTab(index)" :key="index"
:class="{active:!goodsInfo.tabs.includes(index),actived:goodsInfo.tabs.includes(index)}">
{{item}}
</view>
</up-form-item>
</up-form>
</view>
<view class="" style="width: 100%; height: 200rpx;">
</view>
<!-- 底部操作按钮 -->
<view class="action-buttons">
<u-button type="primary" @click="Service.GoPageBack()" :custom-style="ButtonStyle">取消</u-button>
<u-button type="primary" @click="save()" :custom-style="ButtonStyle">保存</u-button>
</view>
<up-picker :show="showClass" @cancel="showClass=!showClass" @confirm="confirmClass"
:columns="columns"></up-picker>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
let goodsInfo = ref({
name: "",
prince: '',
des: "",
class: '',
status: '',
tabs: []
})
let showClass = ref(false)
const columns = ref([
['中国', '美国', '日本']
]);
const radiolist = ref([
{
name: '上架中',
value: '0'
},
{
name: '已下架',
value: '1'
}
]);
let tabsList = ref([
'新品',
'热销',
'推荐',
'折扣',
'特惠'
])
const ButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginLeft: '20rpx'
});
onLoad(() => {
});
onShow(() => {
});
const confirmClass = (e) => {
goodsInfo.value.class = e.value[0]
showClass.value = !showClass.value
}
const radioChange = (e) => {
goodsInfo.value.status = e
}
const clickTab = (e : any) => {
if (goodsInfo.value.tabs.includes(e)) {
let a = goodsInfo.value.tabs.findIndex((x) => {
return x == e
})
goodsInfo.value.tabs.splice(a, 1)
return
}
goodsInfo.value.tabs.push(e)
}
const save=()=>{
Service.GoPageBack()
}
</script>
<style lang="scss">
page {
background-color: #f6f6f6;
}
.active {
color: #b5b5b5;
border: 1rpx solid #b5b5b5;
margin-right: 15rpx;
border-radius: 23rpx;
padding: 5rpx 16rpx;
width: fit-content;
}
.actived {
color: var(--nav-mian);
border: 1rpx solid var(--nav-mian);
margin-right: 15rpx;
border-radius: 23rpx;
padding: 5rpx 16rpx;
width: fit-content;
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,797 @@
<template>
<view class="index-page">
<!-- 页面整体加载状态 -->
<view v-if="loding" class="page-loading">
<!-- 固定顶部区域 -->
<view class="top-fixed">
<view class="status-bar-placeholder"></view>
<view class="top-section-fixed">
<view class="location-box-skeleton">
<view class="skeleton-location-icon"></view>
<view class="skeleton-location-text"></view>
<view class="skeleton-arrow-icon"></view>
</view>
<view class="search-wrapper">
<view class="search-box-skeleton"></view>
</view>
</view>
</view>
<!-- 状态栏占位 - 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 占位区域 - 抵消固定顶部的高度 -->
<view class="top-placeholder"></view>
<!-- 分类骨架屏 -->
<view class="category-section">
<SkeletonCategory />
</view>
<!-- 商家列表骨架屏 -->
<view class="shop-list">
<view class="shop-waterfall">
<view class="waterfall-column">
<SkeletonShopCard v-for="i in 3" :key="'left-' + i" />
</view>
<view class="waterfall-column">
<SkeletonShopCard v-for="i in 3" :key="'right-' + i" />
</view>
</view>
</view>
</view>
<!-- 实际内容 -->
<view v-else>
<!-- 固定顶部区域 -->
<view class="top-fixed">
<view class="status-bar-placeholder"></view>
<view class="top-section-fixed">
<view class="location-box" @click.stop="choooseLocation()">
<up-icon name="map" color="#fff" size="12"></up-icon>
<text class="location-text" style="margin: 0 4rpx;" >{{ location }}</text>
<up-icon name="arrow-right" color="#fff" size="10"></up-icon>
</view>
<view class="search-wrapper">
<view class="search-box">
<input type="text" v-model="searchKey"
@confirm="Service.GoPage('/pages/goods/search?search='+searchKey)" class="search-input"
placeholder="请输入关键字" placeholder-class="search-placeholder" />
<view @click="Service.GoPage('/pages/goods/search?search='+searchKey)" class="search-btn">搜索
</view>
</view>
</view>
</view>
</view>
<!-- 状态栏占位 - 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 占位区域 - 抵消固定顶部的高度 -->
<view class="top-placeholder"></view>
<!-- 分类导航 - 三行五列 -->
<view class="category-section">
<view class="category-grid">
<view v-for="(category, index) in tabList" :key="index" class="category-item"
@click="Service.GoPage('/pages/goods/goodsClass?type='+category.assortId+'&name='+category.name )">
<view class="category-icon-box" :style="{ background: category.bgColor }">
<image :src="Service.GetMateUrlByImg(category.icon)"
style="width: 100%; height: 100%;"></image>
</view>
<text class="category-name">{{ category.name }}</text>
</view>
</view>
</view>
<!-- 商家列表 - 瀑布流布局 -->
<view class="shop-list">
<view class="shop-waterfall">
<!-- 左列 -->
<view class="waterfall-column">
<view v-for="shop in leftGoodsList" :key="shop.merchId" class="shop-card"
@click="Service.GoPage('/pages/community/merchantDetail?merchId='+shop.merchId)">
<view class="shop-cover-wrapper">
<image class="shop-cover" :src="Service.GetMateUrlByImg(shop.logo)" mode="aspectFill" />
<!-- 浮动标签 - 左下角距离 -->
<view class="float-tag float-distance">
<up-icon name="map" color="#fff" size="12"></up-icon>
<text class="tag-text">{{formatDistance(shop.distance)}}</text>
</view>
<!-- 右上角商家标签 -->
<view class="shop-badges">
<text v-if="shop.codeName=='联盟商家'" class="badge-partner">{{shop.codeName }}</text>
<text v-else class="badge-coop">{{shop.codeName }}</text>
</view>
</view>
<view class="shop-info">
<view class="shop-header">
<text class="shop-name">{{ shop.name }}</text>
</view>
<view class="shop-meta">
<text class="shop-sales">月售:{{ shop.sale }}</text>
<text class="shop-avg-price">人均{{ shop.price }}</text>
</view>
<view class="shop-footer">
<view class="shop-tags">
<view v-for="(coupon, idx) in shop.tips" :key="idx" :class="getTagClass(coupon)"
style="font-size: 22rpx; margin-left: 4rpx;" class="shop-tag">
{{ coupon }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 右列 -->
<view class="waterfall-column">
<view v-for="shop in rightGoodsList" :key="shop.merchId" class="shop-card"
@click="Service.GoPage('/pages/community/merchantDetail?merchId='+shop.merchId)">
<view class="shop-cover-wrapper">
<image class="shop-cover" :src="Service.GetMateUrlByImg(shop.logo)" mode="aspectFill" />
<!-- 浮动标签 - 左下角距离 -->
<view class="float-tag float-distance">
<up-icon name="map" color="#fff" size="12"></up-icon>
<text class="tag-text">{{formatDistance(shop.distance)}}</text>
</view>
<!-- 右上角商家标签 -->
<view class="shop-badges">
<text v-if="shop.code=='Discounts'" class="badge-partner">联盟商家</text>
<text v-else class="badge-coop">合作商家</text>
</view>
</view>
<view class="shop-info">
<view class="shop-header">
<text class="shop-name">{{ shop.name }}</text>
</view>
<view class="shop-meta">
<text class="shop-sales">月售:{{ shop.sale }}</text>
<text class="shop-avg-price">人均{{ shop.price }}</text>
</view>
<view class="shop-footer">
<view class="shop-tags">
<view v-for="(coupon, idx) in shop.tips" :key="idx" :class="getTagClass(coupon)"
style="font-size: 22rpx;" class="shop-tag">
{{ coupon }}
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<up-loadmore :status="status" />
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {
onLoad, onReachBottom, onShareAppMessage, onShareTimeline, onShow
} from '@dcloudio/uni-app'
import {
ref,
onMounted,
computed
} from 'vue'
import SkeletonCategory from '../../components/skeleton/skeleton-category.vue'
import SkeletonShopCard from '../../components/skeleton/skeleton-shop-card.vue'
import { vpLoginService, Service } from '@/Service/vp/vpLoginService'
import { vpMerchService } from '@/Service/vp/vpMerchService'
import { vpUserService } from '@/Service/vp/vpUserService'
// 数据
let location = ref('')
const categories = ref([])
const shops = ref([])
const currentCategory = ref('all')
let status = ref('nomore')
let loding = ref(true)
let longitude = ref(0)
let latitude = ref(0)
let searchKey = ref('')
let leftGoodsList = ref<Array<any>>([])
let rightGoodsList = ref<Array<any>>([])
let tabList = ref<Array<any>>([])
let page = ref(1)
let scene=ref('')
onLoad((data:any) => {
if(data.scene){
scene.value=data.scene
}
if (data.q) {
scene.value = decodeURIComponent(data.q).split('?')[1].split('=')[1]
}
getLocation()
})
onShow(()=>{
})
// 分享
onShareAppMessage((res) => {
return {
path: '/pages/index/index'
}
});
onShareTimeline(() => {
return {
path: '/pages/index/index'
}
})
onReachBottom(()=>{
getList()
})
const getLocation = () => {
uni.getLocation({
isHighAccuracy:true,
type: 'gcj02',
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
getAddress()
if (!Service.GetUserToken()) {
login()
return
}
getdata()
},
fail: function (e) {
console.log(e);
}
});
}
const login = () => {
uni.getProvider({
service: 'oauth',
success: function (res : any) {
uni.login({
onlyAuthorize: true,
provider: res.provider,
success: function (loginRes) {
vpLoginService.WxLogin(loginRes.code, res.provider == 'weixin' ? 1 : 3, longitude.value, latitude.value, scene.value).then(content => {
if (content.code == 0) {
Service.SetUserToken(content.data.accToken)
getdata()
} else {
Service.Msg(content.msg)
}
})
}
})
}
});
}
const getdata = () => {
leftGoodsList.value = []
rightGoodsList.value = []
status.value = 'loadmore'
page.value = 1
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpMerchService.GetMerchList('', '', longitude.value, latitude.value,0,0, page.value).then(res => {
loding.value = false
res.data.merchList.forEach((goodsItem : any, GoodsIndex : number) => {
if (GoodsIndex % 2 == 0) {
leftGoodsList.value.push(goodsItem)
} else {
rightGoodsList.value.push(goodsItem)
}
})
tabList.value = res.data.assortList
status.value = res.data.merchList.length == 10 ? 'loadmore' : 'nomore'
page.value++
})
}
const getAddress = () => {
vpUserService.GetAddressInfo(longitude.value, latitude.value).then(res => {
if (res.code == 0) {
location.value = res.data.addrName
} else {
Service.Msg(res.msg)
}
})
}
const choooseLocation = () => {
uni.chooseLocation({
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
location.value = res.address
getdata()
}
});
}
// 格式化距离
const formatDistance = (distance : any) => {
if (distance < 1) {
return `${Number(distance*1000).toFixed(1)}m`
}
return `${(distance).toFixed(1)}km`
}
// 根据标签文本获取样式类
const getTagClass = (tagText : string) => {
const tagMap = {
'可用积分': 'tag-points-available',
'可用券': 'tag-coupon'
}
return tagMap[tagText] || 'tag-points'
}
</script>
<style lang="scss" scoped>
.index-page {
min-height: 100vh;
background: #F5F5F5;
padding-bottom: 20rpx;
}
/* 状态栏占位 - 沉浸式 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 固定顶部区域 - 始终固定 */
.top-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
}
.status-bar-placeholder {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
.top-section-fixed {
background: linear-gradient(135deg, #FF6B00, #FF9500);
padding: 65rpx 20rpx 28rpx;
}
/* 占位区域 - 抵消固定顶部的高度 */
.top-placeholder {
height: calc(var(--status-bar-height) + 170rpx);
width: 100%;
}
.location-box {
display: flex;
align-items: center;
margin-bottom: 28rpx;
}
.location-icon-small {
font-size: 28rpx;
color: #FFFFFF;
margin-right: 8rpx;
}
.location-text {
font-size: 30rpx;
color: #FFFFFF;
font-weight: 500;
margin-right: 8rpx;
}
.arrow-icon {
font-size: 24rpx;
color: #FFFFFF;
}
/* 搜索区域 */
.search-wrapper {
display: flex;
align-items: center;
margin-top: 20rpx;
}
/* 搜索框 */
.search-box {
flex: 1;
display: flex;
align-items: center;
background: #FFFFFF;
border-radius: 32rpx;
padding: 8rpx 28rpx;
gap: 8rpx;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #222222;
height: 48rpx;
line-height: 48rpx;
}
.search-placeholder {
color: #999999;
}
.search-btn {
background: #FF6B00;
color: #FFFFFF;
font-size: 24rpx;
padding: 6rpx 18rpx;
border-radius: 20rpx;
font-weight: 500;
flex-shrink: 0;
}
/* 分类导航 - 三行五列 */
.category-section {
background: #FFFFFF;
padding: 32rpx 20rpx;
margin-bottom: 20rpx;
}
.category-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 32rpx 20rpx;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
}
.category-icon-box {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16rpx;
margin-bottom: 12rpx;
}
.category-icon-box text {
font-size: 44rpx;
color: #FFFFFF;
}
.category-name {
font-size: 24rpx;
color: #222222;
white-space: nowrap;
}
/* 商家列表 - 瀑布流布局 */
.shop-list {
padding: 0 20rpx;
}
.shop-waterfall {
display: flex;
gap: 16rpx;
}
.waterfall-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.shop-card {
background: #FFFFFF;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
break-inside: avoid;
}
.shop-cover-wrapper {
position: relative;
width: 100%;
height: 200rpx;
}
.shop-cover {
width: 100%;
height: 100%;
}
/* 浮动标签 */
.float-tag {
position: absolute;
padding: 2rpx 6rpx;
border-radius: 3rpx;
font-size: 16rpx;
backdrop-filter: blur(10rpx);
-webkit-backdrop-filter: blur(10rpx);
display: flex;
align-items: center;
gap: 2rpx;
}
.float-distance {
left: 6rpx;
bottom: 6rpx;
background: rgba(0, 0, 0, 0.65);
color: #FFFFFF;
}
.float-distance .tag-icon {
font-size: 14rpx;
color: #FFFFFF;
}
.float-distance .tag-text {
font-size: 16rpx;
color: #FFFFFF;
}
.shop-info {
padding: 16rpx;
}
// 右上角
.shop-badges {
position: absolute;
top: 16rpx;
right: 16rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
align-items: flex-end;
}
/* 商家信息 */
.shop-header {
margin-bottom: 12rpx;
}
.shop-name {
font-size: 28rpx;
font-weight: 500;
color: #222222;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.4;
}
.shop-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.shop-sales {
font-size: 22rpx;
color: #999999;
}
.shop-avg-price {
font-size: 26rpx;
color: #FF6B00;
font-weight: 700;
}
/* 商家标签 */
.shop-tags {
display: flex;
flex-wrap: wrap;
gap: 4rpx;
}
/* 标签 */
.tag-badge {
display: inline-block;
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-size: 20rpx;
font-weight: 500;
margin-right: 12rpx;
flex-shrink: 0;
}
.tag-coupon {
background: #FFF8E1;
color: #FF6B00;
}
.shop-price {
font-size: 28rpx;
font-weight: 600;
color: #FF4D4F;
margin-right: 8rpx;
}
.shop-original {
font-size: 22rpx;
color: #999999;
text-decoration: line-through;
}
/* 空状态 */
.empty {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.empty-icon {
font-size: 120rpx;
color: #CCCCCC;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
/* 底部导航栏 - 小巧精致风格 */
.tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-around;
padding: 12rpx 0 calc(12rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
z-index: 999;
height: calc(90rpx + env(safe-area-inset-bottom));
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0;
position: relative;
height: 100%;
}
.tabbar-icon {
font-size: 44rpx;
color: #CCCCCC;
margin-bottom: 8rpx;
transition: all 0.3s ease;
line-height: 1;
}
.tabbar-text {
font-size: 22rpx;
color: #CCCCCC;
font-weight: 400;
transition: all 0.3s ease;
line-height: 1.2;
}
/* 激活状态 */
.tabbar-item.active .tabbar-icon {
color: #FF6B00;
transform: scale(1.1);
}
.tabbar-item.active .tabbar-text {
color: #FF6B00;
font-weight: 500;
}
/* 首页头部骨架屏样式 */
.location-box-skeleton {
display: flex;
align-items: center;
margin-bottom: 28rpx;
}
.skeleton-location-icon {
width: 28rpx;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
margin-right: 8rpx;
}
.skeleton-location-text {
width: 200rpx;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
margin-right: 8rpx;
}
.skeleton-arrow-icon {
width: 24rpx;
height: 24rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
}
.search-box-skeleton {
width: 100%;
height: 60rpx;
border-radius: 32rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
}
@keyframes loading-white {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.page-loading {
min-height: 100vh;
background: #F5F5F5;
}
</style>

View File

@@ -0,0 +1,225 @@
## 1.9.6.62024-09-25
- fix: 修复background-position无效的问题
## 1.9.6.52024-04-14
- fix: 修复`nvue`无法生图的问题
## 1.9.6.42024-03-10
- fix: 修复代理ctx导致H5不能使用ctx.save
## 1.9.6.32024-03-08
- fix: 修复支付宝真机无法使用的问题
## 1.9.6.22024-02-22
- fix: 修复使用render函数报错的问题
## 1.9.6.12023-12-22
- fix: 修复字节小程序非2d字体偏移
- fix: 修复`canvasToTempFilePathSync`会触发两次的问题
- fix: 修复`parser`图片没有宽度的问题
## 1.9.62023-12-06
- fix: 修复背景图受padding影响
- fix: 修复因字节报错改了代理实现导致微信报错
- 1.9.5.82023-11-16
- fix: 修复margin问题
- fix: 修复borderWidth问题
- fix: 修复textBox问题
- fix: 修复字节开发工具报`could not be cloned.`问题
## 1.9.5.72023-07-27
- fix: 去掉多余的方法
- chore: 更新文档,增加自定义字体说明
## 1.9.5.62023-07-21
- feat: 有限的支持富文本
- feat: H5和APP 增加 `hidpi` prop主要用于大尺寸无法生成图片时用
- fix: 修复 钉钉小程序 缺少 `measureText` 方法
- chore: 由于微信小程序 pc 端的 canvas 2d 时不时抽风故不使用canvas 2d
## 1.9.5.52023-06-27
- fix: 修复把`emoji`表情字符拆分成多个字符的情况
## 1.9.5.42023-06-05
- fix: 修复因`canvasToTempFilePathSync`监听导致重复调用
## 1.9.5.32023-05-23
- fix: 因isPc错写成了isPC导致小程序PC不能生成图片
## 1.9.5.22023-05-22
- feat: 删除多余文件
## 1.9.5.12023-05-22
- fix: 修复 文字行数与`line-clamp`相同但不满一行时也加了省略号的问题
## 1.9.52023-05-14
- feat: 增加 `text-indent` 和 `calc` 方法
- feat: 优化 布局时间
## 1.9.4.42023-04-15
- fix: 修复无法匹配负值
- fix: 修复 Nvue IOS getImageInfo `useCORS` 为 undefined
## 1.9.4.32023-04-01
- feat: 增加支持文字描边 `text-stroke: '5rpx #fff'`
## 1.9.4.22023-03-30
- fix: 修复 支付宝小程序 isPC 在手机也为true的问题
- feat: 由 微信开发工具 3060 版 无法获取图片尺寸,现 微信开发工具 3220 版 修复该问题,故还原上一版的获取图片方式。
## 1.9.4.12023-03-28
- fix: 修复固定高度不正确问题
## 1.9.42023-03-17
- fix: nvue ios getImageInfo缺少this报错
- fix: pathType 非2d无效问题
- fix: 修复 小米9se 可能会存在多次init 导致画面多次放大
- fix: 修复 border 分开写 width style无效问题
- fix: 修复 支付宝小程序IOS 再次进入不渲染的问题
- fix: 修复 支付宝小程序安卓Zindex排序错乱问题
- fix: 修复 微信开发工具 3060 版 无法获取图片的问题
- feat: 把 for in 改为 forEach
- feat: 增加 hidden
- feat: 根节点 box-sizing 默认 `border-box`
- feat: 增加支持 `vw` `wh`
- chore: pathType 取消 默认值,因为字节开发工具不能显示
- chore: 支付宝小程序开发工具不支持 生成图片 请以真机调试为准
- bug: 企业微信 2.20.3无法使用
## 1.9.3.52022-06-29
- feat: justifyContent 增加 `space-around`、`space-between`
- feat: canvas 2d 也使用`getImageInfo`
- fix: 修复 `text`的 `text-decoration`错位
## 1.9.3.42022-06-20
- fix: 修复 因创建节点速度问题导致顺序出错。
- fix: 修复 微信小程序 PC 无法显示本地图片
- fix: 修复 flex-box 对齐问题
- feat: 增加 `text-shadow`
- feat: 重写 `text` 对齐方式
- chore: 更新文档
## 1.9.3.32022-06-17
- fix: 修复 支付宝小程序 canvas 2d 存在ctx.draw问题导致报错
- fix: 修复 支付宝小程序 toDataURL 存在权限问题改用 `toTempFilePath`
- fix: 修复 支付宝小程序 image size 问题导致 `objectFit` 无效
## 1.9.3.22022-06-14
- fix: 修复 image 设置背景色不生效问题
- fix: 修复 nvue 环境判断缺少参数问题
## 1.9.3.12022-06-14
- fix: 修复 bottom 定位不对问题
- fix: 修复 因小数导致计算出错换行问题
- feat: 增加 `useCORS` h5端图片跨域 在设置请求头无效果后试一下设置这个值
- chore: 更新文档
## 1.9.32022-06-13
- feat: 增加 `zIndex`
- feat: 增加 `flex-box` 该功能处于原始阶段,非常简陋。
- tips: QQ小程序 vue3 不支持, 为 uni 官方BUG
## 1.9.2.92022-06-10
- fix: 修复`text-align`及`margin`居中问题
## 1.9.2.82022-06-10
- fix: 修复 Nvue `canvasToTempFilePathSync` 不生效问题
## 1.9.2.72022-06-10
- fix: 修复 margin及padding的bug
- fix: 修复 Nvue `isCanvasToTempFilePath` 不生效问题
## 1.9.2.62022-06-09
- fix: 修复 Nvue 不显示
- feat: 增加支持字体渐变
```html
<l-painter-text
text="水调歌头\n明月几时有把酒问青天。不知天上宫阙今夕是何年。我欲乘风归去又恐琼楼玉宇高处不胜寒。起舞弄清影何似在人间。"
css="background: linear-gradient(,#ff971b 0%, #1989fa 100%); background-clip: text" />
```
## 1.9.2.52022-06-09
- chore: 更变获取父级宽度的设定
- chore: `pathType` 在canvas 2d 默认为 `url`
## 1.9.2.42022-06-08
- fix: 修复 `pathType` 不生效问题
## 1.9.2.32022-06-08
- fix: 修复 `canvasToTempFilePath` 漏写 `success` 参数
## 1.9.2.22022-06-07
- chore: 更新文档
## 1.9.2.12022-06-07
- fix: 修复 vue3 赋值给this再传入导致image无法绘制
- fix: 修复 `canvasToTempFilePathSync` 时机问题
- feat: canvas 2d 更改图片生成方式 `toDataURL`
## 1.9.22022-05-30
- fix: 修复 `canvasToTempFilePathSync` 在 vue3 下只生成一次
## 1.9.1.72022-05-28
- fix: 修复 `qrcode`显示不全问题
## 1.9.1.62022-05-28
- fix: 修复 `canvasToTempFilePathSync` 会重复多次问题
- fix: 修复 `view` css `backgroundImage` 图片下载失败导致 子节点不渲染
## 1.9.1.52022-05-27
- fix: 修正支付宝小程序 canvas 2d版本号 2.7.15
## 1.9.1.42022-05-22
- fix: 修复字节小程序无法使用xml方式
- fix: 修复字节小程序无法使用base64(非2D情况下工具上无法显示)
- fix: 修复支付宝小程序 `canvasToTempFilePath` 报错
## 1.9.1.32022-04-29
- fix: 修复vue3打包后uni对象为空后的报错
## 1.9.1.22022-04-25
- fix: 删除多余文件
## 1.9.1.12022-04-25
- fix: 修复图片不显示问题
## 1.9.12022-04-12
- fix: 因四舍五入导致有些机型错位
- fix: 修复无views报错
- chore: nvue下因ios无法读取插件内static文件改由下载方式
## 1.9.02022-03-20
- fix: 因无法固定尺寸导致生成图片不全
- fix: 特定情况下text判断无效
- chore: 本地化APP Nvue webview
## 1.8.92022-02-20
- fix: 修复 小程序下载最多10次并发的问题
- fix: 修复 APP端无法获取本地图片
- fix: 修复 APP Nvue端不执行问题
- chore: 增加图片缓存机制
## 1.8.8.82022-01-27
- fix: 修复 主动调用尺寸问题
## 1.8.8.62022-01-26
- fix: 修复 nvue 下无宽度时获取父级宽度
- fix: 修复 ios app 无法渲染问题
## 1.8.82022-01-23
- fix: 修复 主动调用时无节点问题
- fix: 修复 `box-shadow` 颜色问题
- fix: 修复 `transform:rotate` 角度位置问题
- feat: 增加 `overflow:hidden`
## 1.8.72022-01-07
- fix: 修复 image 方向为 `right` 时原始宽高问题
- feat: 支持 view 设置背景图 `background-image: url(xxx)`
- chore: 去掉可选链
## 1.8.62021-11-28
- feat: 支持`view`对`inline-block`的子集使用`text-align`
## 1.8.5.52021-08-17
- chore: 更新文档,删除 replace
- fix: 修复 text 值为 number时报错
## 1.8.5.42021-08-16
- fix: 字节小程序兼容
## 1.8.5.32021-08-15
- fix: 修复线性渐变与css现实效果不一致的问题
- chore: 更新文档
## 1.8.5.22021-08-13
- chore: 增加`background-image`、`background-repeat` 能力,主要用于背景纹理的绘制,并不是代替`image`。例如:大面积的重复平铺的水印
- 注意这个功能H5暂时无法使用因为[官方的API有BUG](https://ask.dcloud.net.cn/question/128793),待官方修复!!!
## 1.8.5.12021-08-10
- fix: 修复因`margin`报错问题
## 1.8.52021-08-09
- chore: 增加margin支持`auto`,以达到居中效果
## 1.8.42021-08-06
- chore: 增加判断缓存文件条件
- fix: 修复css 多余空格报错问题
## 1.8.32021-08-04
- tips: 1.6.x 以下的版本升级到1.8.x后要为每个元素都加上定位position: 'absolute'
- fix: 修复只有一个view子元素时不计算高度的问题
## 1.8.22021-08-03
- fix: 修复 path-type 为 `url` 无效问题
- fix: 修复 qrcode `text` 为空时报错问题
- fix: 修复 image `src` 动态设置时不生效问题
- feat: 增加 css 属性 `min-width` `max-width`
## 1.8.12021-08-02
- fix: 修复无法加载本地图片
## 1.8.02021-08-02
- chore 文档更新
- 使用旧版的同学不要升级!
## 1.8.0-beta2021-07-30
- ## 全新布局方式 不兼容旧版!
- chore: 布局方式变更
- tips: 微信canvas 2d 不支持真机调试
## 1.6.62021-07-09
- chore: 统一命名规范,无须主动引入组件
## 1.6.52021-06-08
- chore: 去掉console
## 1.6.42021-06-07
- fix: 修复 数字 为纯字符串时不转换的BUG
## 1.6.32021-06-06
- fix: 修复 PC 端放大的BUG
## 1.6.22021-05-31
- fix: 修复 报`adaptor is not a function`错误
- fix: 修复 text 多行高度
- fix: 优化 默认文字的基准线
- feat: `@progress`事件,监听绘制进度
## 1.6.12021-02-28
- 删除多余节点
## 1.6.02021-02-26
- 调整为uni_modules目录规范
- 修复transform的rotate不能为负数问题
- 新增:`pathType` 指定生成图片返回的路径类型,可选值有 `base64`、`url`

View File

@@ -0,0 +1,239 @@
/**
* 美团风格主题 - 参考美团外卖
*/
/* 主色调 - 美团黄色 */
$primary-color: #FFCC00; // 美团黄
$primary-light: #FFD700; // 亮黄色
$primary-dark: #F5A623; // 深黄色
/* 辅助色 */
$secondary-bg: #FFF8E1; // 浅黄背景
$accent-color: #FF6B00; // 橙红色
/* 中性色 */
$text-primary: #0F172A; // 深色文字
$text-secondary: #64748B; // 次要文字
$text-tertiary: #94A3B8; // 辅助文字
$border-color: #E2E8F0; // 边框色
$bg-color: #F8FAFC; // 背景色
/* 功能色 */
$success: #10B981; // 绿色
$warning: #F59E0B; // 橙色
$error: #EF4444; // 红色
$info: #3B82F6; // 蓝色
/* 间距 */
$spacing-xs: 8rpx;
$spacing-sm: 12rpx;
$spacing-md: 16rpx;
$spacing-lg: 20rpx;
$spacing-xl: 24rpx;
/* 圆角 */
$radius-sm: 6rpx;
$radius-md: 10rpx;
$radius-lg: 16rpx;
$radius-round: 999rpx;
/* 阴影 */
$shadow-sm: 0 1rpx 3rpx rgba(0, 0, 0, 0.05);
$shadow-md: 0 4rpx 6rpx -1rpx rgba(0, 0, 0, 0.08);
$shadow-lg: 0 10rpx 15rpx -3rpx rgba(0, 0, 0, 0.08);
/* 字体大小 */
$font-xs: 22rpx;
$font-sm: 24rpx;
$font-md: 26rpx;
$font-lg: 28rpx;
$font-xl: 32rpx;
$font-xxl: 36rpx;
/* 通用类 */
.container {
padding: $spacing-md;
}
.flex {
display: flex;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.text-center {
text-align: center;
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 卡片样式 */
.card {
background: #FFFFFF;
border-radius: $radius-lg;
padding: $spacing-lg;
box-shadow: $shadow-sm;
}
/* 按钮样式 */
.btn-primary {
background: linear-gradient(135deg, $primary-color, $primary-dark);
color: #FFFFFF;
border-radius: $radius-md;
padding: $spacing-md $spacing-xl;
font-size: $font-md;
font-weight: 500;
}
.btn-outline {
background: #FFFFFF;
color: $primary-color;
border: 1rpx solid $primary-color;
border-radius: $radius-md;
padding: $spacing-md $spacing-xl;
font-size: $font-md;
font-weight: 500;
}
/* 标签样式 - 现代风格 */
.tag {
display: inline-flex;
align-items: center;
padding: 4rpx 10rpx;
border-radius: $radius-sm;
font-size: $font-xs;
font-weight: 500;
}
.tag-point {
background: linear-gradient(135deg, #F0F9FF, #E0F2FE);
color: #0284C7;
border: none;
}
.tag-usable {
background: #F1F5F9;
color: $text-secondary;
border: none;
}
.tag-coupon {
background: linear-gradient(135deg, #FAF5FF, #F3E8FF);
color: #9333EA;
border: none;
}
.tag-distance {
background: transparent;
color: $text-tertiary;
}
/* 分割线 */
.divider {
height: 1rpx;
background-color: $border-color;
margin: $spacing-md 0;
}
/* 空状态 */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-xl 0;
color: $text-tertiary;
font-size: $font-md;
}
.empty-icon {
font-size: 120rpx;
margin-bottom: $spacing-md;
opacity: 0.3;
}
/* 商家标签 - 右上角徽章 */
.shop-badges {
position: absolute;
top: 8px;
right: 8px;
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-end;
}
.badge-partner {
background: linear-gradient(135deg, #FF6B35, #F7931E);
color: #FFFFFF;
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
font-weight: 500;
}
.badge-coop {
background: linear-gradient(135deg, #667EEA, #764BA2);
color: #FFFFFF;
font-size: 9px;
padding: 2px 6px;
border-radius: 3px;
font-weight: 500;
}
/* 商家优惠券标签 */
.shop-tag {
display: inline-block;
padding: 2rpx 6rpx;
border-radius: 8rpx;
font-size: 12rpx;
font-weight: 500;
line-height: 1.3;
}
/* 消费得积分标签 - 蓝紫色系 */
.shop-tag.tag-points {
background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
color: #1565C0;
border: 1rpx solid #64B5F6;
}
/* 可用积分标签 - 绿色系 */
.shop-tag.tag-points-available {
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
color: #2E7D32;
border: 1rpx solid #81C784;
}
/* 可用券标签 - 橙红色系 */
.shop-tag.tag-coupon {
background: linear-gradient(135deg, #FFEBEE, #FFCDD2);
color: #D32F2F;
border: 1rpx solid #EF5350;
}
/* 默认标签 - 灰色系 */
.shop-tag.tag-default {
background: linear-gradient(135deg, #F5F5F5, #E0E0E0);
color: #616161;
border: 1rpx solid #BDBDBD;
}

View File

@@ -0,0 +1,216 @@
<template>
<view class="points-mall-page">
<!-- 全新方案:纯 CSS 手动构建的骨架屏 -->
<view v-if="loading" class="skeleton-wrapper">
<view class="skeleton-item skeleton-rect" style="height: 300rpx; margin: 24rpx; border-radius: 16rpx;"></view>
<view class="skeleton-content">
<view class="skeleton-left-panel">
<view v-for="i in 6" :key="i" class="skeleton-item skeleton-text" style="width: 80%; height: 60rpx; margin-bottom: 50rpx;"></view>
</view>
<view class="skeleton-right-panel">
<view v-for="i in 4" :key="i" class="skeleton-card">
<view class="skeleton-item skeleton-rect" style="width: 160rpx; height: 160rpx;"></view>
<view style="flex:1; margin-left: 24rpx;">
<view class="skeleton-item skeleton-text" style="width: 90%; height: 30rpx;"></view>
<view class="skeleton-item skeleton-text" style="width: 60%; height: 32rpx; margin-top: 40rpx;"></view>
</view>
</view>
</view>
</view>
</view>
<!-- 页面实际内容 -->
<view v-else class="page-content">
<!-- 1. 顶部轮播图 (原生 <swiper> 实现) -->
<view class="swiper-section">
<swiper class="swiper-container" circular autoplay :interval="3000" :duration="500" @change="e => swiperCurrent = e.detail.current">
<swiper-item v-for="(item, index) in swiperList" :key="index">
<view class="swiper-item">
<view class="image-placeholder swiper-image">
<image :src="item" style="width: 100%; height: 100%;" mode=""></image>
</view>
</view>
</swiper-item>
</swiper>
<!-- 自定义指示点 -->
<view class="swiper-dots">
<view class="dot" :class="{ active: index === swiperCurrent }" v-for="(item, index) in swiperList" :key="index"></view>
</view>
</view>
<!-- 2. 主体内容:左右联动 -->
<view class="main-content">
<!-- 左侧分类栏 -->
<scroll-view class="left-panel" scroll-y>
<view
class="category-item"
:class="{ active: currentCategory === index }"
v-for="(cat, index) in categories"
:key="cat.id"
@click="selectCategory(index)"
>
<text class="name">{{ cat.name }}</text>
</view>
</scroll-view>
<!-- 右侧商品列表 -->
<scroll-view class="right-panel" scroll-y>
<view class="product-list">
<view class="product-card" v-for="product in products" :key="product.id" @click="Service.GoPage('/pages/goods/integralGoods')">
<view class="image-placeholder product-image">
<image :src="product.img" style="width: 100%; height: 100%; border-radius: 20rpx;" mode=""></image>
</view>
<view class="product-info">
<text class="name">{{ product.name }}</text>
<view class="price-line">
<text class="points">{{ product.points }} 积分</text>
<text class="original-price" v-if="product.originalPrice">+ ¥{{ product.originalPrice }}</text>
</view>
</view>
</view>
</view>
<up-loadmore status="nomore" nomoreText="没有更多商品了"></up-loadmore>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { Service } from "@/Service/Service";
const loading = ref<boolean>(true);
const currentCategory = ref(0);
const swiperCurrent = ref(0);
// 轮播图数据
const swiperList = reactive([
'/static/dele/dele1.jpg',
'/static/dele/dele2.jpg',
'/static/dele/dele3.png',
]);
// 分类数据
const categories = reactive([
{ id: 1, name: '热门兑换' }, { id: 2, name: '生活家居' }, { id: 3, name: '数码家电' },
{ id: 4, name: '美妆个护' }, { id: 5, name: '食品饮料' }, { id: 6, name: '虚拟卡券' },
]);
// 商品数据
const products = reactive([
{ id: 101, name: '品牌充电宝 10000mAh 金属外壳 超薄便携', img:'/static/dele/dele1.jpg', points: 2000, originalPrice: 19.9 },
{ id: 102, name: '声波震动电动牙刷 智能计时', img:'/static/dele/dele2.jpg', points: 5000 },
{ id: 103, name: '知名视频网站月度会员卡 直充', img:'/static/dele/dele3.png', points: 1500, originalPrice: 5 },
{ id: 104, name: '“天命打工人”限定版帆布袋', img:'/static/dele/dele4.jpg', points: 800 },
]);
const selectCategory = (index: number) => {
if (currentCategory.value === index) return;
currentCategory.value = index;
console.log(`切换到分类: ${categories[index].name}`);
};
onLoad(() => {
setTimeout(() => { loading.value = false; }, 1500);
});
onShow(() => {});
</script>
<style lang="scss" scoped>
/* 骨架屏样式 */
.skeleton-wrapper {
background-color: #f7f7f7;
.skeleton-content { display: flex; }
.skeleton-left-panel { width: 180rpx; padding: 30rpx 20rpx; background: #f7f7f7; }
.skeleton-right-panel { flex: 1; padding: 30rpx; background: #fff; }
.skeleton-card { display: flex; align-items: center; gap: 24rpx; padding-bottom: 30rpx; margin-bottom: 30rpx; border-bottom: 1rpx solid #f0f0f0;}
}
.points-mall-page {
background-color: #f7f7f7; height: 100vh;
display: flex; flex-direction: column;
}
.swiper-section {
padding: 24rpx;
position: relative;
.swiper-container {
height: 300rpx;
border-radius: 16rpx;
overflow: hidden;
}
.swiper-item {
width: 100%; height: 100%;
.swiper-image { width: 100%; height: 100%; }
}
.swiper-dots {
position: absolute;
bottom: 40rpx; left: 50%;
transform: translateX(-50%);
display: flex; gap: 12rpx;
.dot {
width: 12rpx; height: 12rpx;
border-radius: 50%; background-color: rgba(255, 255, 255, 0.5);
transition: all 0.3s;
&.active { width: 30rpx; background-color: #fff; }
}
}
}
.main-content {
flex: 1; display: flex; overflow: hidden;
}
.left-panel {
width: 180rpx; background-color: #f7f7f7; height: 100%;
.category-item {
padding: 30rpx 20rpx; font-size: 28rpx; color: #666;
text-align: center; position: relative;
&.active {
background-color: #fff; color: #333; font-weight: bold;
&::before {
content: ''; position: absolute; left: 0; top: 50%;
transform: translateY(-50%); width: 8rpx; height: 40rpx;
background-color: #fa6400;
}
}
}
}
.right-panel {
flex: 1; background-color: #fff; height: 100%; padding: 30rpx;
.product-list { display: flex; flex-direction: column; gap: 30rpx; }
.product-card {
display: flex; gap: 24rpx; align-items: center;
border-bottom: 1rpx solid #e2e2e2;
padding-bottom: 10rpx;
.product-image {
width: 160rpx; height: 160rpx;
border-radius: 12rpx; flex-shrink: 0;
}
.product-info {
flex: 1; min-width: 0;
.name {
font-size: 28rpx; color: #333; font-weight: 500;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; text-overflow: ellipsis;
}
.price-line {
margin-top: 20rpx; display: flex; align-items: baseline;
.points { font-size: 32rpx; font-weight: bold; color: #fa6400; }
.original-price { font-size: 22rpx; color: #999; margin-left: 8rpx; }
}
}
.exchange-btn {
background-color: #fa6400; color: #fff;
border-radius: 30rpx; font-size: 24rpx;
padding: 10rpx 24rpx; margin: 0;
height: fit-content; line-height: 1.5;
&::after { border: none; }
}
}
}
</style>

View File

@@ -0,0 +1,13 @@
// import { createPinia, defineStore } from "pinia";
// import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
// export class StoreAssist{
// private pinia:any=createPinia();
// constructor() {
// this.pinia.use(piniaPluginPersistedstate);
// }
// }

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
onLaunch(() => {
console.log("App Launch");
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
const getUpData=()=>{
// #ifdef APP
// plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => {
// NvpMerchService.GetAppVersion().then(res=>{
// console.log('wgtinfo.versionCode',wgtinfo.versionCode);
// if (res.data.version > wgtinfo.versionCode) {
// setTimeout(function() {
// uni.navigateTo({
// url: "/pages/upData/upData?info=" +
// encodeURIComponent(
// JSON.stringify(res.data))
// })
// }, 1000)
// }
// })
// })
// #endif
}
</script>
<style lang="scss">
@import "uview-plus/index.scss";
@import "colorui/main.css";
@import "colorui/icon.css";
page {
--nav-mian: #FF6B35; //全局颜色
--nav-vice: #F59D77; //副颜色
--nav-diluted: #F2C0A3; //淡颜色
}
</style>

View File

@@ -0,0 +1,497 @@
<template>
<view class="page">
<!-- 商品图片展示 -->
<view class="product-image-section">
<image :src="Service.GetMateUrlByImg(merchInfo.showImg)" mode="aspectFill" class="product-image">
</image>
</view>
<!-- 商品信息 -->
<view class="product-info-section">
<view class="shop-header">
<view class="shop-name">{{merchInfo.name}}</view>
<view class="" style="display: flex; align-items: center; gap: 10rpx;" >
<text class="tag" style="background-color: #FF9500;" >{{ merchInfo.code=='Discounts'?'积分可用':'积分不可用'}}</text>
</view>
</view>
<view class="" style="display: flex; align-items: center;">
<view class="func-detail">
<text style="color: #FF6B35;">{{merchInfo.score}}</text>分
</view>
<view class="func-detail">
月销 <text style="color: #FF6B35;">{{merchInfo.sale}}</text>
</view>
<view class="func-detail">
人均 <text style="color: #FF6B35;">¥{{merchInfo.price}}</text>
</view>
</view>
</view>
<!-- 商家信息卡片 -->
<view class="shop-card-section">
<view class="shop-info">
<view class="" @click="handleViewLocation()"
style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; ">
<view class="info-item">
<u-icon name="map" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{merchInfo.address}}</text>
</view>
<view class="">
<u-icon name="arrow-right" size="18" color="#999999" class="info-icon"></u-icon>
</view>
</view>
<view class="" @click="handleContactShop()"
style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; ">
<view class="info-item">
<u-icon name="phone" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{merchInfo.phone}}</text>
</view>
<view class="">
<u-icon name="arrow-right" size="18" color="#999999" class="info-icon"></u-icon>
</view>
</view>
<view class=""
style="display: flex; align-items: center; justify-content: space-between; ">
<view class="info-item">
<u-icon name="clock" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{merchInfo.time}}</text>
</view>
</view>
<!-- <view v-if="communityInfo" class="info-item">
<u-icon name="home" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">{{communityInfo.name}}</text>
</view> -->
</view>
</view>
<!-- 同店推荐 -->
<view v-if="recommendations.length>0" class="recommendations-section">
<text class="section-title">店内商品</text>
<view class="recommendations-list">
<view @click="Service.GoPage('/pages/goods/goodsDetail?goodsId='+item.goodsId)" class="recommendation-item"
v-for="(item, index) in recommendations" :key="index">
<image :src="Service.GetMateUrlByImg(item.img)" mode="aspectFill" class="recommendation-image"></image>
<view class="recommendation-info">
<text class="recommendation-name">{{ item.name }}</text>
<text class="recommendation-price">{{ item.price }}</text>
</view>
<u-icon name="arrow-right" size="28rpx" color="#999999" class="arrow-icon"></u-icon>
</view>
</view>
</view>
<up-loadmore v-if="recommendations.length>0" :status="status" />
<view class="" style="width: 100%; height: 150rpx;">
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from "@/Service/Service"
import { onLoad } from '@dcloudio/uni-app';
import { vpMerchService } from '@/Service/vp/vpMerchService'
// 商家数据类型
interface shop{
showImg : string
price:number
name:string
score:number
tag:string
merchId:string
address:string
phone:string
sale:number
lat:string
lon:string
code:string
time:string
}
// 推荐商品数据类型
interface Recommendation {
goodsId: string
name : string
price : number
img : string
}
let merchId = ref()
let page = ref(0)
let status = ref('loadmore')
// 营业时间处理
let weekList = ref<Array<number>>([])
let openTime = ref()
let closeTime = ref()
let timefunc = ref(-1)
let merchInfo=ref<shop>({
showImg : '',
price:0,
name:'',
score: 0,
tag: '',
merchId: '',
address: '',
phone: '',
sale: 0,
lat: '',
lon: '',
code:'',
time:''
})
let communityInfo=ref()
// 同店推荐商品数据
const recommendations = ref<Array<Recommendation>>([{
goodsId:'',
name: '',
price: 0,
img: '',
}]);
// 联系商家按钮样式
const contactButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginRight: '20rpx'
});
// 查看位置按钮样式
const locationButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginLeft: '20rpx'
});
onLoad((data : any) => {
merchId.value = data.merchId
getData()
})
const getData = () => {
status.value = 'loadmore'
page.value = 1
recommendations.value=[]
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpMerchService.GetMerchInfo(merchId.value,page.value).then(res=>{
merchInfo.value=res.data.merchInfo
communityInfo.value=res.data.communityInfo
recommendations.value=res.data.merchGoods
status.value = res.data.merchGoods.length == 10 ? 'loadmore' : 'nomore'
merchInfo.value.time=!res.data.merchInfo.busTime?'': timeCancle(res.data.merchInfo.busTime)
page.value++
})
}
// 处理联系商家
const handleContactShop = () => {
uni.makePhoneCall({
phoneNumber: merchInfo.value.phone, // 要拨打的电话号码
success: function () {
console.log('拨打电话成功');
},
fail: function (err) {
console.error('拨打电话失败', err);
}
});
};
// 处理查看位置
const handleViewLocation = () => {
wx.openLocation({
latitude: Number(merchInfo.value.lat),
longitude: Number(merchInfo.value.lon),
name: merchInfo.value.name,
address: merchInfo.value.address,
success: function (e) {
console.log(e);
},
fail: function (e) {
console.log(e);
}
})
};
const timeCancle=(busTime:string)=>{
let data= busTime.split('_')
openTime.value=data[1].split('-')[0]
closeTime.value=data[1].split('-')[1]
let timeData=data[0].split('-')
for(let i=0;i<timeData.length;i++){
if(timeData[i]=='0'){
timeData[i]='7'
}
}
let time=''
timeData.sort((a:any,b:any)=>{
return a-b
})
timeData.map((item:any)=>{
weekList.value.push(item=='7'?(Number(6)):Number(item-1))
})
let timeIndex = weekList.value[0] - 0
let judgment = weekList.value.findIndex((item, index) => {
return item - index !== timeIndex
})
// 1是至 /0是全显示
if (judgment == -1) {
timefunc.value = 1
} else {
timefunc.value = 0
}
if (timefunc.value == 0) {
weekList.value.map((item) => {
time = time + '周' + chinese(item) + ' '
})
} else {
time = '周' + chinese((weekList.value[0])) + '至' + '周' + chinese((weekList.value[weekList.value.length - 1]))
}
time=time+' '+data[1]
return time
}
const chinese = (item : number) => {
if (item + 1 == 1) {
return '一'
}
if (item + 1 == 2) {
return '二'
} if (item + 1 == 3) {
return '三'
} if (item + 1 == 4) {
return '四'
} if (item + 1 == 5) {
return '五'
} if (item + 1 == 6) {
return '六'
} if (item + 1 == 7) {
return '日'
}
}
</script>
<style scoped lang="scss">
/* 商品图片展示 */
.product-image-section {
width: 100%;
height: 500rpx;
margin-bottom: 30rpx;
}
.product-image {
width: 100%;
height: 100%;
}
/* 商家信息 */
.func-detail {
margin-right: 30rpx;
font-size: 26rpx;
}
.product-info-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.product-name {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-price {
font-size: 40rpx;
color: #FF6600;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-description {
font-size: 28rpx;
color: #666666;
line-height: 40rpx;
}
/* 商家信息卡片 */
.shop-card-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.shop-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
justify-content: space-between;
}
.shop-name {
font-size: 38rpx;
color: #333333;
font-weight: bold;
margin-right: 20rpx;
}
.shop-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 24rpx;
color: #FFFFFF;
padding: 4rpx 16rpx;
border-radius: 10rpx;
white-space: nowrap;
}
.tag.new-shop {
background-color: #4CD964;
}
.tag.popular {
background-color: #FF9500;
}
.shop-info {
gap: 20rpx;
}
.info-item {
display: flex;
align-items: center;
// margin-bottom: 20rpx;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-icon {
margin-right: 16rpx;
}
.info-text {
font-size: 28rpx;
margin-left: 15rpx;
color: #666666;
}
/* 同店推荐 */
.recommendations-section {
padding: 0 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 30rpx;
}
.recommendations-list {
gap: 30rpx;
}
.recommendation-item {
display: flex;
align-items: center;
padding: 30rpx;
margin: 20rpx 0;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.recommendation-item:last-child {
border-bottom: none;
}
.recommendation-image {
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
margin-right: 24rpx;
}
.recommendation-info {
flex: 1;
}
.recommendation-name {
font-size: 32rpx;
color: #333333;
display: block;
margin-bottom: 12rpx;
}
.recommendation-price {
font-size: 30rpx;
color: #FF6600;
}
.arrow-icon {
margin-left: 16rpx;
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,349 @@
<template>
<view class="page">
<!-- 商品图片展示 -->
<view class="product-image-section">
<image :src="Service.GetMateUrlByImg('/static/dele/dele4.jpg')" mode="aspectFill" class="product-image">
</image>
</view>
<!-- 商品信息 -->
<view class="product-info-section">
<view class="shop-header">
<view class="shop-name">美食小铺</view>
<view class="shop-tags">
<text class="tag new-shop">新店</text>
<text class="tag popular">人气店</text>
</view>
</view>
<view class="" style="display: flex; align-items: center;">
<view class="func-detail">
<text style="color: #FF6B35;">4.7</text>分
</view>
<view class="func-detail">
月销<text style="color: #FF6B35;">4.7</text>
</view>
<view class="func-detail">
人均<text style="color: #FF6B35;">¥47</text>
</view>
</view>
</view>
<!-- 商家信息卡片 -->
<view class="shop-card-section">
<view class="shop-info">
<view class="" @click="handleViewLocation()" style="display: flex; align-items: center; justify-content: space-between;">
<view class="info-item">
<u-icon name="map" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">朝阳区美食街123号</text>
</view>
<view class="">
<u-icon name="arrow-right" size="18" color="#999999" class="info-icon"></u-icon>
</view>
</view>
<view class="" @click="handleContactShop()" style="display: flex; align-items: center; justify-content: space-between;">
<view class="info-item">
<u-icon name="phone" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">010-12345678</text>
</view>
<view class="">
<u-icon name="arrow-right" size="18" color="#999999" class="info-icon"></u-icon>
</view>
</view>
<view class="info-item">
<u-icon name="home" size="22" color="#999999" class="info-icon"></u-icon>
<text class="info-text">朝阳美食社区</text>
</view>
</view>
</view>
<!-- 同店推荐 -->
<view class="recommendations-section">
<text class="section-title">店内商品</text>
<view class="recommendations-list">
<view @click="Service.GoPage('/pages/goods/goodsDetail')" class="recommendation-item"
v-for="(item, index) in recommendations" :key="index">
<image :src="item.image" mode="aspectFill" class="recommendation-image"></image>
<view class="recommendation-info">
<text class="recommendation-name">{{ item.name }}</text>
<text class="recommendation-price">{{ item.price }}</text>
</view>
<u-icon name="arrow-right" size="28rpx" color="#999999" class="arrow-icon"></u-icon>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 150rpx;">
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from "@/Service/Service"
// 推荐商品数据类型
interface Recommendation {
id : number;
name : string;
price : string;
image : string;
}
// 同店推荐商品数据
const recommendations = ref<Recommendation[]>([
{
id: 1,
name: '招牌卤肉饭',
price: '¥26',
image: '/static/dele/dele4.jpg'
},
{
id: 2,
name: '红烧牛肉面',
price: '¥32',
image: '/static/dele/dele4.jpg'
},
{
id: 3,
name: '香菇三明治',
price: '¥28',
image: '/static/dele/dele4.jpg'
}
]);
// 联系商家按钮样式
const contactButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginRight: '20rpx'
});
// 查看位置按钮样式
const locationButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginLeft: '20rpx'
});
// 处理联系商家
const handleContactShop = () => {
uni.makePhoneCall({
phoneNumber: '10086', // 要拨打的电话号码
success: function () {
console.log('拨打电话成功');
},
fail: function (err) {
console.error('拨打电话失败', err);
}
});
};
// 处理查看位置
const handleViewLocation = () => {
wx.chooseLocation({
success: res => {
// latitude.value = res.latitude.toString();
// longitude.value = res.longitude.toString();
}
})
};
</script>
<style scoped lang="scss">
/* 商品图片展示 */
.product-image-section {
width: 100%;
height: 500rpx;
margin-bottom: 30rpx;
}
.product-image {
width: 100%;
height: 100%;
}
/* 商家信息 */
.func-detail {
margin-right: 30rpx;
font-size: 26rpx;
}
.product-info-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.product-name {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-price {
font-size: 40rpx;
color: #FF6600;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.product-description {
font-size: 28rpx;
color: #666666;
line-height: 40rpx;
}
/* 商家信息卡片 */
.shop-card-section {
padding: 30rpx;
margin: 30rpx;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.shop-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
justify-content: space-between;
}
.shop-name {
font-size: 38rpx;
color: #333333;
font-weight: bold;
margin-right: 20rpx;
}
.shop-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 24rpx;
color: #FFFFFF;
padding: 4rpx 16rpx;
border-radius: 16rpx;
}
.tag.new-shop {
background-color: #4CD964;
}
.tag.popular {
background-color: #FF9500;
}
.shop-info {
gap: 20rpx;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-icon {
margin-right: 16rpx;
}
.info-text {
font-size: 28rpx;
margin-left: 15rpx;
color: #666666;
}
/* 同店推荐 */
.recommendations-section {
padding: 0 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 36rpx;
color: #333333;
font-weight: bold;
display: block;
margin-bottom: 30rpx;
}
.recommendations-list {
gap: 30rpx;
}
.recommendation-item {
display: flex;
align-items: center;
padding: 30rpx;
margin: 20rpx 0;
box-shadow: 0 0 10rpx 4rpx #E2e2e2;
border-radius: 20rpx;
}
.recommendation-item:last-child {
border-bottom: none;
}
.recommendation-image {
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
margin-right: 24rpx;
}
.recommendation-info {
flex: 1;
}
.recommendation-name {
font-size: 32rpx;
color: #333333;
display: block;
margin-bottom: 12rpx;
}
.recommendation-price {
font-size: 30rpx;
color: #FF6600;
}
.arrow-icon {
margin-left: 16rpx;
}
/* 底部操作按钮 */
.action-buttons {
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,457 @@
<template>
<view class="promotion-page" :style="showCode?'height: 100vh; overflow: hidden;':''">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">我的推广</text>
<view class="nav-placeholder"></view>
</view>
<!-- 统计卡片 -->
<view class="stats-section">
<view class="stats-card">
<view @click="showCode=true" class="stat-item">
<up-icon name="share" color="#fff" size="20"></up-icon>
<text class="stat-label">推广码</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-value">{{ list.length }}</text>
<text class="stat-label">推广会员</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-value">{{ allAward }}</text>
<text class="stat-label">获得积分</text>
</view>
</view>
</view>
<!-- 会员列表 -->
<view class="content">
<view class="list-header">
<text class="header-title">推广会员列表</text>
<text class="header-count">共{{ list.length }}人</text>
</view>
<view v-if="list.length > 0" class="members-list">
<view v-for="member in list" :key="member.id" class="member-card">
<!-- 会员信息 -->
<view class="member-info">
<image class="member-avatar" :src="Service.GetMateUrlByImg(member.headImg)" mode="aspectFill" />
<view class="member-details">
<view class="name-tag-row">
<text class="member-name">{{ member.nick }}</text>
</view>
<view class="user-id-tag">
<image :src="Service.GetIconImg('/static/iconMent/user/viptype.png')" style="width: 20rpx; height: 20rpx; margin-right: 10rpx;"></image>
<text class="id-text">ID: {{ member.userNo }}</text>
</view>
<text class="register-time">注册时间:{{ Service.formatDate(new Date(member.addTime),1) }}</text>
</view>
</view>
<!-- 积分奖励 -->
<view class="points-info">
<text class="points-label">奖励</text>
<text class="points-value">+{{ member.award }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="ri-team-line empty-icon"></text>
<text class="empty-text">暂无推广会员</text>
<text class="empty-desc">邀请好友加入,获得积分奖励</text>
</view>
</view>
<up-popup :show="showCode" round='10' mode='center' @close="showCode=false" :closeable="true"
:safeAreaInsetTop='true' :safeAreaInsetBottom='false' >
<view class="" style=" margin: 0 auto; padding: 20rpx; background-color: #fff; " >
<l-painter :board="poster" ref="posterFun">
</l-painter>
</view>
<view class="" style="padding: 0 20rpx 20rpx;" >
<up-button color='var(--nav-mian)' @click="save()" text="保存图片" ></up-button>
</view>
</up-popup>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { vpUserService, Service } from "@/Service/vp/vpUserService"
let list = ref<Array<any>>([])
let status = ref<string>('loadmore')
let pageNo = ref<number>(1)
let allAward = ref<Number>(0)
let showCode=ref(false)
let code=ref('')
let url = ref<string>('')
let imgurl = ref<string>('')
const posterFun = ref(null)
const poster = ref<any>({
css: {
// 根节点若无尺寸,自动获取父级节点
position: ' relative',
borderRadius: '18rpx',
overflow: 'hidden',
width:'550rpx'
},
views: [{
type: 'image',
src: 'https://vp.clouds.xypays.cn/poster/poster1.png',
css: {
width: '550rpx',
margin: '0 auto',
},
mode: "widthFix"
}, {
type: 'qrcode',
text: '',
css: {
width: '80rpx',
height: '80rpx',
position: 'absolute',
top: '870rpx',
left: '65rpx'
},
}],
})
let picture = ref<string>('')
onLoad(() => {
getData()
getCode()
})
onReachBottom(() => {
getList()
});
const getData = () => {
list.value = []
status.value = 'loadmore'
pageNo.value = 1
getList()
}
const getList = () => {
if (status.value == 'nomore' || status.value == 'loading') {
return
}
status.value = 'loading'
vpUserService.GetUseRemList(pageNo.value).then(res => {
if (res.code == 0) {
list.value = [...list.value, ...res.data.list]
status.value = 10 == res.data.list.length ? 'loadmore' : 'nomore'
allAward.value = res.data.allAward
pageNo.value++
} else {
Service.Msg(res.msg)
}
})
}
const getCode=()=>{
vpUserService.GetShareEwm().then(res=>{
if(res.code==0){
url.value=res.data.url
imgurl.value = res.data.bgUrl
poster.value.views[1].text = url.value
poster.value.views[0].src = imgurl.value
}
})
}
// 返回
const goBack = () => {
Service.GoPageBack()
}
const save=()=>{
Service.LoadClose('开始下载')
posterFun.value.canvasToTempFilePathSync({
fileType: 'jpg',
pathType:'url',
quality: 1,
success: (res) => {
picture.value = res.tempFilePath
saveImage()
},
fail(e) {
Service.Msg('下载失败')
console.log('???????????', e)
}
})
}
const saveImage = () => {
uni.saveImageToPhotosAlbum({
filePath: picture.value,
success(res) {
Service.Msg('保存成功!')
},
fail: function (err) {
console.log(err,'===')
Service.Msg('保存失败')
}
});
}
</script>
<style lang="scss" scoped>
/* 引入全局标签样式 */
@import '@/styles/member-tags.scss';
.promotion-page {
min-height: 100vh;
background: #F5F5F5;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 24rpx 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* 统计卡片 */
.stats-section {
padding: 20rpx;
}
.stats-card {
background: linear-gradient(135deg, #FF6B00, #FF9500);
border-radius: 16rpx;
padding: 32rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-around;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 0, 0.3);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stat-value {
font-size: 40rpx;
font-weight: 700;
color: #FFFFFF;
line-height: 1;
}
.stat-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
}
.stat-divider {
width: 1rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.3);
}
/* 内容区域 */
.content {
padding: 0 20rpx 20rpx;
}
/* 列表头部 */
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
}
.header-title {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
.header-count {
font-size: 22rpx;
color: #999999;
}
/* 会员列表 */
.members-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.member-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.member-info {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
}
.member-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
flex-shrink: 0;
}
.member-details {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.member-name-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.name-tag-row {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 8rpx;
}
.member-name {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
.user-member-tag {
transform: scale(0.92);
transform-origin: left center;
}
.user-id-tag {
margin-bottom: 8rpx;
width: fit-content;
display: inline-flex;
}
.register-time {
font-size: 22rpx;
color: #CCCCCC;
}
/* 积分信息 */
.points-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
padding: 16rpx;
background: #FFF4E6;
border-radius: 12rpx;
margin-left: 16rpx;
}
.points-label {
font-size: 20rpx;
color: #FF9800;
}
.points-value {
font-size: 28rpx;
font-weight: 700;
color: #FF6B00;
}
/* 空状态 */
.empty-state {
background: #FFFFFF;
border-radius: 16rpx;
padding: 120rpx 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.empty-icon {
font-size: 120rpx;
color: #CCCCCC;
}
.empty-text {
font-size: 28rpx;
color: #999999;
font-weight: 500;
}
.empty-desc {
font-size: 24rpx;
color: #CCCCCC;
}
</style>

View File

@@ -0,0 +1,583 @@
<template>
<view class="settings-page">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">设置</text>
<view class="nav-placeholder"></view>
</view>
<!-- 主要内容区域 -->
<view class="content">
<!-- 个人信息卡片 -->
<view class="profile-card">
<view class="card-title">
<text class="ri-user-line title-icon"></text>
<text class="title-text">个人信息</text>
</view>
<!-- 头像 -->
<view class="setting-item" @click="uploadFImg(140,140,'Avatar','headimg')">
<view class="item-left">
<text class="item-label">头像</text>
</view>
<view class="item-right">
<image class="avatar-preview" :src="Service.GetMateUrlByImg(userInfo.headImg)"
mode="aspectFill" />
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<!-- 昵称 -->
<view class="setting-item" @click="changeNickname">
<view class="item-left">
<text class="item-label">昵称</text>
</view>
<view class="item-right">
<text class="item-value">{{ userInfo.nick }}</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<!-- 手机号 -->
<view class="setting-item">
<view class="item-left">
<text class="item-label">手机号</text>
</view>
<view class="item-right">
<text class="item-value">{{ userInfo.phone }}</text>
</view>
</view>
<!-- 会员ID -->
<view class="setting-item">
<view class="item-left">
<text class="item-label">会员ID</text>
</view>
<view class="item-right">
<text class="item-value">{{ userInfo.userNo }}</text>
</view>
</view>
</view>
<!-- 其他设置 -->
<!-- <view class="other-settings-card">
<view class="card-title">
<text class="ri-settings-4-line title-icon"></text>
<text class="title-text">其他设置</text>
</view>
<view class="setting-item" @click="clearCache">
<view class="item-left">
<text class="item-label">清除缓存</text>
</view>
<view class="item-right">
<text class="item-value">23.5MB</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="setting-item" @click="checkUpdate">
<view class="item-left">
<text class="item-label">检查更新</text>
</view>
<view class="item-right">
<text class="item-value">当前版本 v1.0.0</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="setting-item" @click="viewPrivacy">
<view class="item-left">
<text class="item-label">隐私政策</text>
</view>
<view class="item-right">
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="setting-item" @click="viewAgreement">
<view class="item-left">
<text class="item-label">用户协议</text>
</view>
<view class="item-right">
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
</view> -->
<!-- <view class="logout-section">
<button class="logout-btn" @click="save()">
<text class="btn-text">保存信息</text>
</button>
</view> -->
</view>
<!-- 修改昵称弹窗 -->
<view v-if="showNicknameModal" class="modal-overlay" @click="closeNicknameModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">修改昵称</text>
<text class="ri-close-line modal-close" @click="closeNicknameModal"></text>
</view>
<view class="modal-body">
<input class="nickname-input" v-model="tempNickname" placeholder="请输入新昵称" maxlength="20" />
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @click.stop="closeNicknameModal">取消</button>
<button class="modal-btn confirm" @click.stop="saveNickname">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from '@/Service/Service';
import { ref } from "vue";
import { vpUserService } from '@/Service/vp/vpUserService';
import ImageCropperFunc from "@/components/ImageCropper";
const userInfo = ref<any>({
nick: '',
sex: '',
phone: '',
headImg: ''
})
// 临时昵称
const tempNickname = ref('')
onLoad(() => {
getUserinfo()
});
onShow(() => {
});
const getUserinfo = () => {
vpUserService.GetUserInfo().then(res => {
userInfo.value = res.data.userInfo
})
}
const uploadFImg = (width : any, height : any, Type : any, Name : any) => {
uni.chooseImage({
count: 1, // 最多选择3张图片
sizeType: ['original', 'compressed'], // 支持原图和压缩图
sourceType: ['album', 'camera'], // 可从相册选择或使用相机拍照
success: function (res) {
let path = res.tempFiles[0].path
let arr = path.split('.')
let name = arr[arr.length - 1]
Service.uploadH5(path, 'Avatar', (data) => {
userInfo.value.headImg = data
vpUserService.UpdateUser(userInfo.value.headImg, userInfo.value.nick, userInfo.value.sex, '').then(res => {
if (res.code == 0) {
Service.Msg('修改成功!')
getUserinfo()
} else {
Service.Msg(res.msg)
}
})
})
},
fail: function (err) {
console.error('选择失败:', err.errMsg);
}
})
}
// 修改昵称
const changeNickname = () => {
tempNickname.value = userInfo.value.nick
showNicknameModal.value = true
}
// 关闭昵称弹窗
const closeNicknameModal = () => {
showNicknameModal.value = false
}
// 保存昵称
const saveNickname = () => {
if (!tempNickname.value.trim()) {
uni.showToast({
title: '昵称不能为空',
icon: 'none'
})
return
}
vpUserService.UpdateUser(userInfo.value.headImg, tempNickname.value, userInfo.value.sex, '').then(res => {
if (res.code == 0) {
Service.Msg('修改成功!')
showNicknameModal.value = false
userInfo.value.nick = tempNickname.value
} else {
Service.Msg(res.msg)
}
})
}
// 用户信息
const user = ref({
nickname: '美食达人',
avatar: 'https://picsum.photos/200/200?random=100',
phone: '138****8888',
memberId: '8888888'
})
// 显示昵称弹窗
const showNicknameModal = ref(false)
// 返回
const goBack = () => {
uni.navigateBack({
delta: 1
})
}
// 修改头像
const changeAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
// 这里应该上传到服务器,现在暂时直接使用本地路径
user.value.avatar = res.tempFilePaths[0]
uni.showToast({
title: '头像已更新',
icon: 'success'
})
}
})
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '清除缓存',
content: '确定要清除缓存吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '清除中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}, 1000)
}
}
})
}
// 检查更新
const checkUpdate = () => {
uni.showLoading({
title: '检查中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '已是最新版本',
icon: 'success'
})
}, 1500)
}
// 查看隐私政策
const viewPrivacy = () => {
uni.showToast({
title: '隐私政策页面',
icon: 'none'
})
}
// 查看用户协议
const viewAgreement = () => {
uni.showToast({
title: '用户协议页面',
icon: 'none'
})
}
// 退出登录
const logout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '退出中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '已退出登录',
icon: 'success'
})
// 这里可以跳转到登录页面
}, 1000)
}
}
})
}
</script>
<style lang="scss" scoped>
.settings-page {
min-height: 100vh;
background: #F5F5F5;
display: flex;
flex-direction: column;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 24rpx 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* 内容区域 */
.content {
flex: 1;
padding: 32rpx 24rpx;
}
/* 卡片通用样式 */
.profile-card,
.other-settings-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin-bottom: 20rpx;
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #F5F5F5;
}
.title-icon {
font-size: 32rpx;
color: #FF6B00;
}
.title-text {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
/* 设置项 */
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #F5F5F5;
}
.setting-item:last-child {
border-bottom: none;
}
.item-left {
display: flex;
align-items: center;
}
.item-label {
font-size: 28rpx;
color: #222222;
font-weight: 500;
}
.item-right {
display: flex;
align-items: center;
gap: 8rpx;
}
.item-value {
font-size: 26rpx;
color: #999999;
}
.item-arrow {
font-size: 28rpx;
color: #CCCCCC;
}
.avatar-preview {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
/* 退出登录按钮 */
.logout-section {
margin-top: 32rpx;
}
.logout-btn {
width: 100%;
height: 88rpx;
background: #FFFFFF;
border: 2rpx solid #F44336;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #F44336;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
width: 560rpx;
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #222222;
}
.modal-close {
font-size: 40rpx;
color: #999999;
}
.modal-body {
margin-bottom: 32rpx;
}
.nickname-input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
background: #F5F5F5;
border-radius: 12rpx;
font-size: 28rpx;
color: #222222;
}
.modal-footer {
display: flex;
gap: 16rpx;
}
.modal-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
border: none;
}
.modal-btn.cancel {
background: #F5F5F5;
color: #666666;
}
.modal-btn.confirm {
background: linear-gradient(135deg, #FF6B00, #FF9500);
color: #FFFFFF;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<view style="margin: 30rpx 50rpx;">
<view class="" style="font-size: 26rpx;">
提现金额
</view>
<view class="" style="margin: 20rpx 0;">
<up-input :customStyle="{'padding':'12rpx 0','height':'100rpx'}" fontSize='18'
prefixIconStyle="font-size: 28px;color: #000;font-weight:bold" placeholder="请输入提现金额" border="bottom"
prefixIcon="rmb"></up-input>
</view>
<view class="" style="font-size: 26rpx; display: flex; align-items: center; ">
<view class="" style="color: #999; ">
当前积分余额57.28元,
</view>
<view class="" style="color:#5d75a9 ;">
全部提现
</view>
</view>
<view class="action-buttons">
<u-button type="primary" :custom-style="contactButtonStyle">立即提现</u-button>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
// 按钮样式
const contactButtonStyle = ref({
backgroundColor: '#FF6600',
borderColor: '#FF6600',
color: '#FFFFFF',
fontSize: '28rpx',
height: '75rpx',
borderRadius: '45rpx',
marginRight: '20rpx'
});
onLoad(() => {
});
onShow(() => {
});
</script>
<style lang="scss">
.action-buttons{
display: flex;
padding: 30rpx 30rpx;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
}
</style>

View File

@@ -0,0 +1,43 @@
import { Service } from '@/Service/Service';
/*****用户*****/
class vpUserService {
private static GetUserInfoPath : string = '/User/GetUserInfo';
/*****获取用户信息*****/
static GetUserInfo() {
var result = Service.Request(this.GetUserInfoPath, 'GET', {});
return result;
}
private static GetUserAccInfoPath : string = '/User/GetUserAccInfo';
/*****获取用户账户信息*****/
static GetUserAccInfo(page : number) {
var result = Service.Request(this.GetUserAccInfoPath, 'GET', { page });
return result;
}
private static UpdateUserPath : string = '/User/UpdateUser';
/*****修改用户信息*****/
static UpdateUser(headImg:string,nick:string,sex:string,phone:string) {
var result = Service.Request(this.UpdateUserPath, 'POST', { headImg,nick,sex,phone });
return result;
}
private static PayMerchPath : string = '/Order/PayMerch';
/*****支付*****/
static PayMerch(merchId:string,amount:number,payway:string,openId:string) {
var result = Service.Request(this.PayMerchPath, 'POST', { merchId,amount,payway,openId });
return result;
}
private static GetShareEwmPath : string = '/User/GetShareEwm';
/*****获取用户二维码*****/
static GetShareEwm() {
var result = Service.Request(this.GetShareEwmPath, 'GET', { });
return result;
}
}
export { Service, vpUserService };

View File

@@ -0,0 +1,19 @@
export class UploadAssist {
static Upload(url: string, path: string, fromData: any) {
return new Promise(function(resolve, reject) {
uni.uploadFile({
url: url, //仅为示例,非真实的接口地址
filePath: path,
name: 'file',
formData: fromData,
success: (uploadFileRes) => {
resolve(uploadFileRes);
},
fail: (err) => {
reject(err);
},
});
})
}
}

View File

@@ -0,0 +1,235 @@
<template>
<view>
<view class="" style=" width: 100%; height: 310rpx; background: linear-gradient(45deg,#FF6B35,#FF8B65);">
</view>
<view class=""
style=" margin: 0 30rpx; margin-top: -110rpx; padding: 30rpx; background-color: #fff; border-radius: 20rpx; ">
<view class="" style="display: flex; align-items: center; justify-content: space-between;">
<view class="" style="font-size: 36rpx; font-weight: 600;">
{{userInfo?.nick}}
</view>
<view @click="Service.GoPage('/pages/userFunc/setData')" class="">
<up-icon name="arrow-right" size="18" color='#333333' :bold='true'></up-icon>
</view>
</view>
<view class=""
style="display: flex; align-items: center; margin-top: 40rpx; justify-content: space-between; ">
<view @click="Service.GoPage('/pages/userFunc/integration')" class=""
style=" width: 48%; display: flex;align-items: center;">
<view class=""
style=" display: flex;align-items: center;justify-content: center; width: 80rpx; height: 80rpx; background-color: #FF6B35; border-radius: 50%; ">
<img :src="Service.GetIconImg('/static/index/user/code.png')"
style="width: 50rpx; height: 50rpx; " alt="" />
</view>
<view class=""
style=" margin-left: -20rpx; flex: 1; text-align: center; font-size: 28rpx; font-weight: 600;">
{{accInfo?.integral}}积分
</view>
<view class="">
<up-icon name="arrow-right" size="14" color='#9CA3AF' :bold='true'></up-icon>
</view>
</view>
<view class="" @click="Service.GoPage('/pages/userFunc/trade?type='+1)"
style=" width: 48%; display: flex;align-items: center;">
<view class=""
style=" display: flex;align-items: center;justify-content: center; width: 80rpx; height: 80rpx; background-color: #FF6B35; border-radius: 50%; ">
<img :src="Service.GetIconImg('/static/index/user/list.png')"
style="width: 50rpx; height: 50rpx; " alt="" />
</view>
<view class=""
style=" margin-left: -20rpx; flex: 1; text-align: center; font-size: 28rpx; font-weight: 600;">
交易记录
</view>
<view class="">
<up-icon name="arrow-right" size="14" color='#9CA3AF' :bold='true'></up-icon>
</view>
</view>
</view>
</view>
<!-- 管理中心 -->
<view class="service-section" style="margin: 40rpx 30rpx 0; border-radius: 20rpx; ">
<text class="section-title" style="font-weight: 600;">商家管理中心</text>
<view class="service-grid">
<view class="service-item" @click="gotopage(controItem)" v-for="(controItem,serviceIndex) in controList"
:key="serviceIndex">
<view class="flex-center" style=" border-radius: 50%; padding: 20rpx; background-color: #FFF5F0; ">
<image :src="Service.GetIconImg(controItem.img)" class="service-icon"></image>
</view>
<text class="service-text">{{controItem.name}}</text>
</view>
</view>
</view>
<!-- 个人服务 -->
<view class="service-section" style="margin: 40rpx 30rpx 0; border-radius: 20rpx; ">
<text class="section-title" style="font-weight: 600;">我的服务</text>
<view class=""
style=" margin-top: 40rpx; display: flex; align-items: center; justify-content: space-between;">
<view style=" width: 45%; display: flex; align-items: center;" @click="serviceFunc(myServiceItem,serviceIndex)"
v-for="(myServiceItem,serviceIndex) in myServiceList" :key="serviceIndex">
<view class="flex-center" style=" border-radius: 50%; padding: 20rpx; background-color: #FFF5F0; ">
<image :src="Service.GetIconImg(myServiceItem.img)" class="service-icon"></image>
</view>
<text style="margin-left: 30rpx; font-size: 28rpx; font-weight: 600; ">{{myServiceItem.name}}</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
import { vpUserService } from '@/Service/vp/vpUserService'
interface accInfo{
account:number
integral:number
userId:string
}
interface userInfo{
nick:string
integral:number
}
let controList = ref([
{
img: '/static/index/user/analysis.png',
name: '数据统计',
path: '/pages/userFunc/statistics'
},
{
img: '/static/index/user/shop.png',
name: '商品管理',
path: '/pages/goods/goodsContro'
},
{
img: '/static/index/user/trad.png',
name: '交易明细',
path: '/pages/userFunc/trade?type=' + 0
},
{
img: '/static/index/user/store.png',
name: '编辑店铺',
path: '/pages/userFunc/editStore'
}
])
let myServiceList = ref([
{
img: '/static/index/user/request.png',
name: '客服咨询',
path: ''
},
{
img: '/static/index/user/set.png',
name: '系统设置',
path: '/pages/userFunc/set'
}
])
let page=ref(1)
let accInfo=ref<accInfo>()
let userInfo=ref<userInfo>()
onLoad(() => {
});
onShow(() => {
getUserinfo()
getUseraccInfo()
});
const gotopage = (item : any) => {
if (item.path) {
Service.GoPage(item.path)
}
}
const serviceFunc=(item:any,index:any)=>{
if(index==0){
wx.openCustomerServiceChat({
extInfo: { url: 'https://work.weixin.qq.com/kfid/kfc959c128ce7801256' },
corpId: 'wwb1123fbb286554ab',
success(res) { },
fail(err) {
console.log(err, '失败')
// 失败回调
}
})
}else{
Service.GoPage(item.path)
}
}
const getUserinfo=()=>{
vpUserService.GetUserInfo().then(res=>{
userInfo.value=res.data.userInfo
})
}
const getUseraccInfo=()=>{
vpUserService.GetUserAccInfo(page.value).then(res=>{
accInfo.value=res.data.accInfo
})
}
</script>
<style lang="scss">
page {
background-color: #f6f6f6;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/* 服务区域通用样式 */
.service-section,
.value-added-section {
background-color: #fff;
margin: 20rpx 0rpx;
overflow: hidden;
padding: 20rpx;
}
/* 服务网格 */
.service-grid {
display: flex;
flex-wrap: wrap;
padding: 10rpx 20rpx 30rpx;
}
.service-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0;
}
.service-icon {
width: 40rpx;
height: 40rpx;
}
.service-text {
font-size: 24rpx;
color: #666;
margin-top: 16rpx;
text-align: center;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,185 @@
<template>
<view style="padding: 20rpx;">
<view class=""
style=" display: flex; align-items: center; justify-content: space-around; background-color: #fff; padding: 20rpx">
<view v-for="(item, index) in tabList" @click="chooseTab(index)"
style="display: flex; flex-direction: column; align-items: center; justify-content: center;"
:key="index">
<view class="" :class="{tabimgActive:index==tabCurrent,tabimg: index!=tabCurrent }"
style=" border-radius: 50%; display: flex; align-items: center; justify-content: center; height: 100rpx; width: 100rpx; ">
<img :src="Service.GetIconImg( index==tabCurrent? item.imged:item.img)"
style="width: 50rpx; height: 50rpx; "></img>
</view>
<view :class="{tabActivefont:index==tabCurrent,tabfont:index!=tabCurrent}"
style="font-size: 26rpx; margin-top: 15rpx;" class="">
{{item.name}}
</view>
</view>
</view>
<view class="" style=" background-color: #fff; padding: 20rpx; " >
<view class="" @click="Service.GoPage('/pages/community/merchantDetail')"
style="padding: 20rpx; margin-top: 20rpx; border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #e2e2e2;">
<view class="" style="display: flex; ">
<img :src="Service.GetMateUrlByImg('/static/dele/dele1.jpg')"
style=" border-radius: 20rpx; width: 140rpx; height: 140rpx;" alt="" />
<view class=""
style=" flex: 1; margin-left: 20rpx; display: flex; flex-direction: column; justify-content: space-between; ">
<view class="" style="display: flex; align-items: center;">
<view class="" style="font-weight: 700; font-size: 32rpx;">
老北京炸酱面
</view>
<view class="tag"
style=" margin-left: 15rpx; color: #fff; border-radius: 12rpx; background-color: #FF6B35; padding: 4rpx 20rpx; ">
新店
</view>
</view>
<view class="" style="display: flex;align-items: center;">
<up-rate count="1" activeColor='#FF6B35' size='16' :readonly='true'></up-rate>
<text style="color: #666666; font-size: 26rpx;">3.8</text>
<text style="margin-left: 10rpx;color: #666666; font-size: 26rpx; ">月售892单</text>
</view>
<view class="" style="display: flex; align-items: center; justify-content: space-between; ">
<view class="" style="display: flex;align-items: center;">
<up-icon name="map" color="#666666" size="18"></up-icon>
<text style="color: #666666; margin-left: 12rpx; font-size: 26rpx;">0.8km</text>
</view>
<view class="" style="margin-right: 20rpx;">
<text style="font-size: 28rpx;font-weight: 600; color: #FF6B35; "> ¥58/人 </text>
</view>
</view>
</view>
</view>
<view class="" style="margin: 20rpx; margin-bottom: 0; " >
<up-scroll-list :indicator='false' >
<view v-for="(item, index) in scrollList" :key="index" style="display: flex; margin-right: 50rpx; flex-direction: column; justify-content: center; align-items: center;" >
<img :src="Service.GetIconImg(item.img)" alt="" style="width: 100rpx; height: 100rpx; border-radius: 20rpx; " />
<view class="" style="font-size: 24rpx; color: #666666; margin-top: 10rpx; " >
{{item.name}}
</view>
</view>
</up-scroll-list>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import {Service} from "@/Service/Service"
let search = ref()
let tabCurrent=ref(0)
let tabList = ref(
[
{
name: '美食',
img: '/static/index/index/food.png',
imged: '/static/index/index/fooded.png'
},
{
name: '饮品',
img: '/static/index/index/cofe.png',
imged: '/static/index/index/cofed.png'
},
{
name: '超市',
img: '/static/index/index/shop.png',
imged: '/static/index/index/shoped.png'
},
{
name: '美妆',
img: '/static/index/index/good.png',
imged: '/static/index/index/gooded.png'
},
{
name: '医疗',
img: '/static/index/index/medical.png',
imged: '/static/index/index/medicaled.png'
}
]
)
let scrollList = ref([
{
name: '牛肉面',
img: '/static/dele/dele3.png'
},
{
name: '牛肉面',
img: '/static/dele/dele3.png'
},
{
name: '牛肉面',
img: '/static/dele/dele3.png'
},
{
name: '牛肉面',
img: '/static/dele/dele3.png'
},
{
name: '牛肉面',
img: '/static/dele/dele3.png'
},
{
name: '牛肉面',
img: '/static/dele/dele3.png'
},
{
name: '牛肉面',
img: '/static/dele/dele3.png'
},
{
name: '牛肉面',
img: '/static/dele/dele3.png'
}
])
onLoad(() => {
});
onShow(() => {
});
const chooseTab=(e)=>{
tabCurrent.value=e
}
</script>
<style lang="scss">
.tabimgActive{
background-color: var(--nav-mian);
}
.tabimg{
background-color: #F5F5F5;
}
.tabActivefont{
color: var(--nav-mian);
}
.tabfont{
color:#333333
}
.tag{
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
font-size: 24rpx;
margin-left: 10rpx;
}
</style>

View File

@@ -0,0 +1,368 @@
export const networkReg = /^(http|\/\/)/;
export const isBase64 = (path) => /^data:image\/(\w+);base64/.test(path);
export function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay))
}
let {platform, SDKVersion} = uni.getSystemInfoSync()
export const isPC = /windows|mac/.test(platform)
// 缓存图片
let cache = {}
export function isNumber(value) {
return /^-?\d+(\.\d+)?$/.test(value);
}
export function toPx(value, baseSize, isDecimal = false) {
// 如果是数字
if (typeof value === 'number') {
return value
}
// 如果是字符串数字
if (isNumber(value)) {
return value * 1
}
// 如果有单位
if (typeof value === 'string') {
const reg = /^-?([0-9]+)?([.]{1}[0-9]+){0,1}(em|rpx|px|%)$/g
const results = reg.exec(value);
if (!value || !results) {
return 0;
}
const unit = results[3];
value = parseFloat(value);
let res = 0;
if (unit === 'rpx') {
res = uni.upx2px(value);
} else if (unit === 'px') {
res = value * 1;
} else if (unit === '%') {
res = value * toPx(baseSize) / 100;
} else if (unit === 'em') {
res = value * toPx(baseSize || 14);
}
return isDecimal ? res.toFixed(2) * 1 : Math.round(res);
}
return 0
}
// 计算版本
export function compareVersion(v1, v2) {
v1 = v1.split('.')
v2 = v2.split('.')
const len = Math.max(v1.length, v2.length)
while (v1.length < len) {
v1.push('0')
}
while (v2.length < len) {
v2.push('0')
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i], 10)
const num2 = parseInt(v2[i], 10)
if (num1 > num2) {
return 1
} else if (num1 < num2) {
return -1
}
}
return 0
}
function gte(version) {
// #ifdef MP-ALIPAY
SDKVersion = my.SDKVersion
// #endif
return compareVersion(SDKVersion, version) >= 0;
}
export function canIUseCanvas2d() {
// #ifdef MP-WEIXIN
return gte('2.9.2');
// #endif
// #ifdef MP-ALIPAY
return gte('2.7.15');
// #endif
// #ifdef MP-TOUTIAO
return gte('1.78.0');
// #endif
return false
}
// #ifdef MP
export const prefix = () => {
// #ifdef MP-TOUTIAO
return tt
// #endif
// #ifdef MP-WEIXIN
return wx
// #endif
// #ifdef MP-BAIDU
return swan
// #endif
// #ifdef MP-ALIPAY
return my
// #endif
// #ifdef MP-QQ
return qq
// #endif
// #ifdef MP-360
return qh
// #endif
}
// #endif
/**
* base64转路径
* @param {Object} base64
*/
export function base64ToPath(base64) {
const [, format] = /^data:image\/(\w+);base64,/.exec(base64) || [];
return new Promise((resolve, reject) => {
// #ifdef MP
const fs = uni.getFileSystemManager()
//自定义文件名
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
let pre = prefix()
// #ifdef MP-TOUTIAO
const filePath = `${pre.getEnvInfoSync().common.USER_DATA_PATH}/${time}.${format}`
// #endif
// #ifndef MP-TOUTIAO
const filePath = `${pre.env.USER_DATA_PATH}/${time}.${format}`
// #endif
fs.writeFile({
filePath,
data: base64.split(',')[1],
encoding: 'base64',
success() {
resolve(filePath)
},
fail(err) {
console.error(err)
reject(err)
}
})
// #endif
// #ifdef H5
// mime类型
let mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
//base64 解码
let byteString = atob(base64.split(',')[1]);
//创建缓冲数组
let arrayBuffer = new ArrayBuffer(byteString.length);
//创建视图
let intArray = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
intArray[i] = byteString.charCodeAt(i);
}
resolve(URL.createObjectURL(new Blob([intArray], {
type: mimeString
})))
// #endif
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.loadBase64Data(base64, () => {
if (!format) {
reject(new Error('ERROR_BASE64SRC_PARSE'))
}
const time = new Date().getTime();
const filePath = `_doc/uniapp_temp/${time}.${format}`
bitmap.save(filePath, {},
() => {
bitmap.clear()
resolve(filePath)
},
(error) => {
bitmap.clear()
reject(error)
})
}, (error) => {
bitmap.clear()
reject(error)
})
// #endif
})
}
/**
* 路径转base64
* @param {Object} string
*/
export function pathToBase64(path) {
if (/^data:/.test(path)) return path
return new Promise((resolve, reject) => {
// #ifdef H5
let image = new Image();
image.setAttribute("crossOrigin", 'Anonymous');
image.onload = function() {
let canvas = document.createElement('canvas');
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
canvas.getContext('2d').drawImage(image, 0, 0);
let result = canvas.toDataURL('image/png')
resolve(result);
canvas.height = canvas.width = 0
}
image.src = path + '?v=' + Math.random()
image.onerror = (error) => {
reject(error);
};
// #endif
// #ifdef MP
if (uni.canIUse('getFileSystemManager')) {
uni.getFileSystemManager().readFile({
filePath: path,
encoding: 'base64',
success: (res) => {
resolve('data:image/png;base64,' + res.data)
},
fail: (error) => {
console.error({error, path})
reject(error)
}
})
}
// #endif
// #ifdef APP-PLUS
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), (entry) => {
entry.file((file) => {
const fileReader = new plus.io.FileReader()
fileReader.onload = (data) => {
resolve(data.target.result)
}
fileReader.onerror = (error) => {
reject(error)
}
fileReader.readAsDataURL(file)
}, reject)
}, reject)
// #endif
})
}
export function getImageInfo(path, useCORS) {
const isCanvas2D = this && this.canvas && this.canvas.createImage
return new Promise(async (resolve, reject) => {
// let time = +new Date()
let src = path.replace(/^@\//,'/')
if (cache[path] && cache[path].errMsg) {
resolve(cache[path])
} else {
try {
// #ifdef MP || APP-PLUS
if (isBase64(path) && (isCanvas2D ? isPC : true)) {
src = await base64ToPath(path)
}
// #endif
// #ifdef H5
if(useCORS) {
src = await pathToBase64(path)
}
// #endif
} catch (error) {
reject({
...error,
src
})
}
// #ifndef APP-NVUE
if(isCanvas2D && !isPC) {
const img = this.canvas.createImage()
img.onload = function() {
const image = {
path: img,
width: img.width,
height: img.height
}
cache[path] = image
resolve(cache[path])
}
img.onerror = function(err) {
reject({err,path})
}
img.src = src
return
}
// #endif
uni.getImageInfo({
src,
success: (image) => {
const localReg = /^\.|^\/(?=[^\/])/;
// #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
image.path = localReg.test(src) ? `/${image.path}` : image.path;
// #endif
if(isCanvas2D) {
const img = this.canvas.createImage()
img.onload = function() {
image.path = img
cache[path] = image
resolve(cache[path])
}
img.onerror = function(err) {
reject({err,path})
}
img.src = src
return
}
// #ifdef APP-PLUS
// console.log('getImageInfo', +new Date() - time)
// ios 比较严格 可能需要设置跨域
if(uni.getSystemInfoSync().osName == 'ios' && useCORS) {
pathToBase64(image.path).then(base64 => {
image.path = base64
cache[path] = image
resolve(cache[path])
}).catch(err => {
console.error({err, path})
reject({err,path})
})
return
}
// #endif
cache[path] = image
resolve(cache[path])
},
fail(err) {
console.error({err, path})
reject({err,path})
}
})
}
})
}
// #ifdef APP-PLUS
const getLocalFilePath = (path) => {
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path
.indexOf('_downloads') === 0) {
return path
}
if (path.indexOf('file://') === 0) {
return path
}
if (path.indexOf('/storage/emulated/0/') === 0) {
return path
}
if (path.indexOf('/') === 0) {
const localFilePath = plus.io.convertAbsoluteFileSystem(path)
if (localFilePath !== path) {
return localFilePath
} else {
path = path.substr(1)
}
}
return '_www/' + path
}
// #endif

View File

@@ -0,0 +1,81 @@
{
"id": "qf-image-cropper",
"displayName": "图片裁剪插件",
"version": "2.2.5",
"description": "图片裁剪插件,支持自定义尺寸、定点等比例缩放、拖动、图片翻转、剪切圆形/圆角图片、定制样式,功能多性能高体验好注释全。",
"keywords": [
"qf-image-cropper",
"图片裁剪",
"图片编辑",
"头像裁剪",
"小程序"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "n"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "u",
"钉钉": "n",
"快手": "n",
"飞书": "n",
"京东": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}

View File

@@ -0,0 +1,221 @@
<template>
<view class="page">
<!-- 设置列表 -->
<view class="settings-list">
<!-- 账户安全 -->
<!-- <view class="settings-section">
<view class="section-title">账户安全</view>
<view @click="Service.GoPage(accountItem.path)" v-for="(accountItem,accountIndex) in account" :key="accountIndex" class="section-item">
<view class="" style="display: flex; align-items: center;">
<view class="icon" style=" ">
<img class="icon-img" :src="Service.GetIconImg(accountItem.icon)" alt="" />
</view>
<text style="margin-left: 14rpx; font-weight: 500; ">{{accountItem.name}}</text>
</view>
<view class="" style="display: flex; align-items: baseline; font-size: 24rpx; color: #9CA3AF; ">
<text v-if="accountItem.name=='实名认证'" style="margin-right: 10rpx;">未认证</text>
<up-icon name="arrow-right" size="12"></up-icon>
</view>
</view>
</view> -->
<!-- 支持与帮助 -->
<view class="settings-section">
<view class="section-title">支持与帮助</view>
<view v-for="(helpItem,helpIndex) in help" :key="helpIndex" class="section-item">
<view class="" style="display: flex; align-items: center;">
<view class="icon" style=" ">
<img class="icon-img" :src="Service.GetIconImg(helpItem.icon)" alt="" />
</view>
<text style="margin-left: 14rpx; font-weight: 500; ">{{helpItem.name}}</text>
</view>
<view class="" style="display: flex; align-items: baseline; font-size: 24rpx; color: #9CA3AF; ">
<up-icon name="arrow-right" size="12"></up-icon>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service'
const account = ref([
{
name: '绑定手机号',
icon: '/static/userFunc/set/security.png',
path:'/pages/userFunc/bind'
}, {
name: '修改支付密码',
icon: '/static/userFunc/set/password.png',
path:'/pages/userFunc/password'
}
])
const help = ref([
{
name: '用户协议',
icon: '/static/userFunc/set/agreement.png'
},
{
name: '隐私政策',
icon: '/static/userFunc/set/privacy.png'
},
{
name: '联系客服',
icon: '/static/userFunc/set/service.png'
}
])
</script>
<style scoped lang="scss">
.page {
background-color: #f5f5f5;
overflow: hidden;
height: 100vh;
}
// 图标
.icon {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
background-color: #FFEDD5;
display: flex;
align-items: center;
justify-content: center;
}
.icon-img {
width: 32rpx;
height: 32rpx;
}
/* 用户信息卡片 */
.user-card {
margin: 30rpx;
border-radius: 20rpx;
background: linear-gradient(to right, #F97316, #FB923C);
padding: 40rpx 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.user-avatar {
background-color: #FFFFFF;
padding: 10rpx;
}
.user-info {
margin-left: 30rpx;
flex: 1;
}
.user-name {
font-size: 32rpx;
color: #FFFFFF;
font-weight: 500;
margin-bottom: 10rpx;
display: block;
}
.user-level {
width: fit-content;
font-size: 18rpx;
color: #fff;
background-color: rgba(255, 255, 255, 0.2);
padding: 4rpx 20rpx;
border-radius: 24rpx;
}
/* 设置列表 */
.settings-list {
padding: 0 30rpx;
}
.settings-section {
background-color: #FFFFFF;
border-radius: 20rpx;
margin-bottom: 30rpx;
overflow: hidden;
padding: 30rpx;
margin-top: 20rpx;
}
.section-title {
font-size: 28rpx;
color: #6B7280;
font-weight: 600;
}
.section-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 1rpx solid #e2e2e2;
// &:last-child{
// border-bottom: none;
// }
}
.u-cell {
height: 100rpx;
font-size: 32rpx;
--u-cell-value-color: #999999;
--u-cell-title-color: #333333;
--u-cell-arrow-size: 40rpx;
}
/* 版本信息 */
.version-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
background-color: #FFFFFF;
border-radius: 20rpx;
margin-bottom: 60rpx;
}
.version-text {
font-size: 28rpx;
color: #333333;
}
.version-number {
font-size: 28rpx;
color: #999999;
margin-right: 10rpx;
}
.check-update {
font-size: 24rpx;
color: #FF6600;
background-color: #FFF7ED;
display: flex;
align-items: center;
justify-content: center;
padding: 6rpx 16rpx;
border-radius: 15rpx;
}
/* 退出登录按钮 */
.logout-btn {
margin: 0 30rpx;
}
</style>

View File

@@ -0,0 +1,585 @@
<template>
<view class="settings-page">
<!-- 沉浸式状态栏 -->
<view v-if="!isZFB" class="status-bar"></view>
<!-- 顶部导航 -->
<view v-if="!isZFB" class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">设置</text>
<view class="nav-placeholder"></view>
</view>
<!-- 主要内容区域 -->
<view class="content">
<!-- 个人信息卡片 -->
<view class="profile-card">
<view class="card-title">
<text class="ri-user-line title-icon"></text>
<text class="title-text">个人信息</text>
</view>
<!-- 头像 -->
<view class="setting-item" @click="uploadFImg(140,140,'Avatar','headimg')">
<view class="item-left">
<text class="item-label">头像</text>
</view>
<view class="item-right">
<image class="avatar-preview" :src="Service.GetMateUrlByImg(userInfo.headImg)"
mode="aspectFill" />
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<!-- 昵称 -->
<view class="setting-item" @click="changeNickname">
<view class="item-left">
<text class="item-label">昵称</text>
</view>
<view class="item-right">
<text class="item-value">{{ userInfo.nick }}</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<!-- 手机号 -->
<view class="setting-item">
<view class="item-left">
<text class="item-label">手机号</text>
</view>
<view class="item-right">
<text class="item-value">{{ userInfo.phone }}</text>
</view>
</view>
<!-- 会员ID -->
<view class="setting-item">
<view class="item-left">
<text class="item-label">会员ID</text>
</view>
<view class="item-right">
<text class="item-value">{{ userInfo.userNo }}</text>
</view>
</view>
</view>
<!-- 其他设置 -->
<!-- <view class="other-settings-card">
<view class="card-title">
<text class="ri-settings-4-line title-icon"></text>
<text class="title-text">其他设置</text>
</view>
<view class="setting-item" @click="clearCache">
<view class="item-left">
<text class="item-label">清除缓存</text>
</view>
<view class="item-right">
<text class="item-value">23.5MB</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="setting-item" @click="checkUpdate">
<view class="item-left">
<text class="item-label">检查更新</text>
</view>
<view class="item-right">
<text class="item-value">当前版本 v1.0.0</text>
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="setting-item" @click="viewPrivacy">
<view class="item-left">
<text class="item-label">隐私政策</text>
</view>
<view class="item-right">
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
<view class="setting-item" @click="viewAgreement">
<view class="item-left">
<text class="item-label">用户协议</text>
</view>
<view class="item-right">
<text class="ri-arrow-right-s-line item-arrow"></text>
</view>
</view>
</view> -->
<!-- <view class="logout-section">
<button class="logout-btn" @click="save()">
<text class="btn-text">保存信息</text>
</button>
</view> -->
</view>
<!-- 修改昵称弹窗 -->
<view v-if="showNicknameModal" class="modal-overlay" @click="closeNicknameModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">修改昵称</text>
<text class="ri-close-line modal-close" @click="closeNicknameModal"></text>
</view>
<view class="modal-body">
<input class="nickname-input" v-model="tempNickname" placeholder="请输入新昵称" maxlength="20" />
</view>
<view class="modal-footer">
<button class="modal-btn cancel" @click.stop="closeNicknameModal">取消</button>
<button class="modal-btn confirm" @click.stop="saveNickname">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from '@/Service/Service';
import { ref } from "vue";
import { vpUserService } from '@/Service/vp/vpUserService';
import ImageCropperFunc from "@/components/ImageCropper";
import {
inject
} from 'vue';
const isZFB = inject('isZFB');
const userInfo = ref<any>({
nick: '',
sex: '',
phone: '',
headImg: ''
})
// 临时昵称
const tempNickname = ref('')
onLoad(() => {
getUserinfo()
});
onShow(() => {
});
const getUserinfo = () => {
vpUserService.GetUserInfo().then(res => {
userInfo.value = res.data.userInfo
})
}
const uploadFImg = (width : any, height : any, Type : any, Name : any) => {
uni.chooseImage({
count: 1, // 最多选择3张图片
sizeType: ['original', 'compressed'], // 支持原图和压缩图
sourceType: ['album', 'camera'], // 可从相册选择或使用相机拍照
success: function (res) {
let path = res.tempFiles[0].path
let arr = path.split('.')
let name = arr[arr.length - 1]
Service.uploadH5(path, 'Avatar', (data) => {
userInfo.value.headImg = data
vpUserService.UpdateUser(userInfo.value.headImg, userInfo.value.nick, userInfo.value.sex, '').then(res => {
if (res.code == 0) {
Service.Msg('修改成功!')
getUserinfo()
} else {
Service.Msg(res.msg)
}
})
})
},
fail: function (err) {
console.error('选择失败:', err.errMsg);
}
})
}
// 修改昵称
const changeNickname = () => {
tempNickname.value = userInfo.value.nick
showNicknameModal.value = true
}
// 关闭昵称弹窗
const closeNicknameModal = () => {
showNicknameModal.value = false
}
// 保存昵称
const saveNickname = () => {
if (!tempNickname.value.trim()) {
uni.showToast({
title: '昵称不能为空',
icon: 'none'
})
return
}
vpUserService.UpdateUser(userInfo.value.headImg, tempNickname.value, userInfo.value.sex, '').then(res => {
if (res.code == 0) {
Service.Msg('修改成功!')
showNicknameModal.value = false
userInfo.value.nick = tempNickname.value
} else {
Service.Msg(res.msg)
}
})
}
// 用户信息
const user = ref({
nickname: '美食达人',
avatar: 'https://picsum.photos/200/200?random=100',
phone: '138****8888',
memberId: '8888888'
})
// 显示昵称弹窗
const showNicknameModal = ref(false)
// 返回
const goBack = () => {
uni.navigateBack({
delta: 1
})
}
// 修改头像
const changeAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
// 这里应该上传到服务器,现在暂时直接使用本地路径
user.value.avatar = res.tempFilePaths[0]
uni.showToast({
title: '头像已更新',
icon: 'success'
})
}
})
}
// 清除缓存
const clearCache = () => {
uni.showModal({
title: '清除缓存',
content: '确定要清除缓存吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '清除中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '缓存已清除',
icon: 'success'
})
}, 1000)
}
}
})
}
// 检查更新
const checkUpdate = () => {
uni.showLoading({
title: '检查中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '已是最新版本',
icon: 'success'
})
}, 1500)
}
// 查看隐私政策
const viewPrivacy = () => {
uni.showToast({
title: '隐私政策页面',
icon: 'none'
})
}
// 查看用户协议
const viewAgreement = () => {
uni.showToast({
title: '用户协议页面',
icon: 'none'
})
}
// 退出登录
const logout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.showLoading({
title: '退出中...'
})
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '已退出登录',
icon: 'success'
})
// 这里可以跳转到登录页面
}, 1000)
}
}
})
}
</script>
<style lang="scss" scoped>
.settings-page {
min-height: 100vh;
background: #F5F5F5;
display: flex;
flex-direction: column;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 24rpx 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* 内容区域 */
.content {
flex: 1;
padding: 32rpx 24rpx;
}
/* 卡片通用样式 */
.profile-card,
.other-settings-card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin-bottom: 20rpx;
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 2rpx solid #F5F5F5;
}
.title-icon {
font-size: 32rpx;
color: #FF6B00;
}
.title-text {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
/* 设置项 */
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #F5F5F5;
}
.setting-item:last-child {
border-bottom: none;
}
.item-left {
display: flex;
align-items: center;
}
.item-label {
font-size: 28rpx;
color: #222222;
font-weight: 500;
}
.item-right {
display: flex;
align-items: center;
gap: 8rpx;
}
.item-value {
font-size: 26rpx;
color: #999999;
}
.item-arrow {
font-size: 28rpx;
color: #CCCCCC;
}
.avatar-preview {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
}
/* 退出登录按钮 */
.logout-section {
margin-top: 32rpx;
}
.logout-btn {
width: 100%;
height: 88rpx;
background: #FFFFFF;
border: 2rpx solid #F44336;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #F44336;
}
/* 弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
width: 560rpx;
background: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #222222;
}
.modal-close {
font-size: 40rpx;
color: #999999;
}
.modal-body {
margin-bottom: 32rpx;
}
.nickname-input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
background: #F5F5F5;
border-radius: 12rpx;
font-size: 28rpx;
color: #222222;
}
.modal-footer {
display: flex;
gap: 16rpx;
}
.modal-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
border: none;
}
.modal-btn.cancel {
background: #F5F5F5;
color: #666666;
}
.modal-btn.confirm {
background: linear-gradient(135deg, #FF6B00, #FF9500);
color: #FFFFFF;
}
</style>

View File

@@ -0,0 +1,408 @@
<template>
<view class="coupon-page">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">优惠券</text>
<view class="nav-placeholder"></view>
</view>
<!-- Tab 切换 - 简约文字Tab -->
<view class="tab-bar">
<view v-for="(tab, index) in tabs" :key="index" class="tab-item" :class="{ active: currentTab === index }"
@click="switchTab(index)">
<text class="tab-text">{{ tab.label }}</text>
<text v-if="tab.count > 0" class="tab-count">({{ tab.count }})</text>
</view>
</view>
<!-- 优惠券列表 - 简约卡片 -->
<view class="coupon-list">
<view v-for="coupon in coupons" :key="coupon.id" class="coupon-card" :class="`coupon-${coupon.status}`">
<!-- 左侧金额 -->
<view class="coupon-left">
<!-- <view v-if="coupon.code === 'discount'" class="coupon-amount">
<text class="amount-number">{{ coupon.deductMoney }}</text>
<text class="amount-unit">折</text>
</view> -->
<view v-if="coupon.code === 'Special'" class="coupon-amount">
<text class="amount-symbol">¥</text>
<text class="amount-number">{{ coupon.deductMoney }}</text>
</view>
<view v-else class="coupon-amount">
<text class="amount-gift">礼品</text>
</view>
</view>
<!-- 分割线 -->
<view class="coupon-divider">
<view class="divider-circle top"></view>
<view class="divider-line"></view>
<view class="divider-circle bottom"></view>
</view>
<!-- 右侧信息 -->
<view class="coupon-right">
<view class="coupon-title">{{ coupon.code=='Special'?'满减券':'' }}</view>
<view class="coupon-condition">
<text v-if="coupon.needMoney > 0">满{{ coupon.needMoney }}元可用</text>
<text v-else>无门槛</text>
</view>
<view class="coupon-expire">有效期至 {{ coupon.endTime }}</view>
<!-- 状态标识 -->
<view v-if="coupon.status === 1" class="coupon-status used">
<text>已使用</text>
</view>
<view v-else-if="coupon.status === 2" class="coupon-status expired">
<text>已过期</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="coupons.length === 0" class="empty">
<text class="empty-text">{{ emptyText }}</text>
</view>
<view v-else class="empty">
<up-loadmore :status="status" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref, computed } from "vue";
import { vpDiscountService } from "@/Service/vp/vpDiscountService"
let coupons = ref<Array<any>>([])
let status = ref<string>('loadmore')
let pageNo = ref<number>(1)
// 数据
const currentTab = ref(0)
const tabs = ref([
{ label: '未使用', value: 'unused', count: 0 ,type:1 },
{ label: '已使用', value: 'used', count: 0 , type:2 },
{ label: '已过期', value: 'expired', count: 0 ,type:3}
])
// 空状态文本
const emptyText = computed(() => {
const textMap = ['暂无可用优惠券', '暂无已使用的优惠券', '暂无已过期的优惠券']
return textMap[currentTab.value]
})
onLoad(() => {
getData()
})
onReachBottom(() => {
getList()
});
const getData = () => {
coupons.value = []
status.value = 'loadmore'
pageNo.value = 1
getList()
}
const getList = () => {
if (status.value == 'nomore' || status.value == 'loading') {
return
}
status.value = 'loading'
vpDiscountService.GetUserDiscountList( tabs.value[currentTab.value].type, pageNo.value).then(res => {
if (res.code == 0) {
coupons.value = [...coupons.value, ...res.data.list]
status.value = 10 == res.data.list.length ? 'loadmore' : 'nomore'
tabs.value[0].count =res.data.wsy
tabs.value[1].count =res.data.ysy
tabs.value[2].count =res.data.ygq
pageNo.value++
} else {
Service.Msg(res.msg)
}
})
}
// 切换Tab
const switchTab = (index : number) => {
currentTab.value = index
getData()
}
// 返回我的页面
const goBack = () => {
Service.GoPageBack()
}
</script>
<style lang="scss" scoped>
.coupon-page {
min-height: 100vh;
background-color: #F5F5F5;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 24rpx 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* Tab栏 - 简约文字Tab */
.tab-bar {
display: flex;
background: #FFFFFF;
padding: 20rpx 20rpx 0;
border-bottom: 1rpx solid #E5E5E5;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 20rpx;
position: relative;
}
.tab-item.active .tab-text {
color: #222;
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 32rpx;
height: 4rpx;
background: #FF6B00;
border-radius: 2rpx;
}
.tab-text {
font-size: 28rpx;
color: #666;
}
.tab-count {
font-size: 24rpx;
color: inherit;
margin-left: 4rpx;
}
/* 优惠券列表 */
.coupon-list {
padding: 20rpx;
}
.coupon-card {
background: #FFFFFF;
border-radius: 12rpx;
margin-bottom: 20rpx;
display: flex;
overflow: hidden;
}
/* 未使用 - 橙色边框 */
.coupon-unused {
border: 2rpx solid #FF6B00;
}
/* 已使用 - 灰色 */
.coupon-used {
opacity: 0.5;
}
/* 已过期 - 灰色 */
.coupon-expired {
opacity: 0.5;
}
/* 左侧金额区 */
.coupon-left {
flex-shrink: 0;
width: 200rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FFF4E6, #FFE0B2);
padding: 32rpx 20rpx;
}
.coupon-amount {
display: flex;
align-items: baseline;
}
.amount-symbol {
font-size: 28rpx;
font-weight: 600;
color: #FF6B00;
}
.amount-number {
font-size: 56rpx;
font-weight: 600;
color: #FF6B00;
line-height: 1;
}
.amount-unit {
font-size: 24rpx;
font-weight: 500;
color: #FF6B00;
margin-left: 4rpx;
}
.amount-gift {
font-size: 28rpx;
font-weight: 600;
color: #FF6B00;
}
/* 分割线 */
.coupon-divider {
position: relative;
width: 4rpx;
background: #F5F5F5;
}
.divider-circle {
position: absolute;
width: 20rpx;
height: 20rpx;
background: #F5F5F5;
border-radius: 50%;
left: 50%;
transform: translateX(-50%);
}
.divider-circle.top {
top: -10rpx;
}
.divider-circle.bottom {
bottom: -10rpx;
}
.divider-line {
position: absolute;
top: 10rpx;
bottom: 10rpx;
left: 50%;
width: 2rpx;
background: repeating-linear-gradient(to bottom,
#F5F5F5 0rpx,
#F5F5F5 6rpx,
transparent 6rpx,
transparent 12rpx);
transform: translateX(-50%);
}
/* 右侧信息 */
.coupon-right {
flex: 1;
padding: 24rpx 20rpx;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.coupon-title {
font-size: 30rpx;
font-weight: 600;
color: #222;
margin-bottom: 12rpx;
}
.coupon-condition {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.coupon-expire {
font-size: 22rpx;
color: #999;
}
/* 状态标识 */
.coupon-status {
position: absolute;
top: 50%;
right: 20rpx;
transform: translateY(-50%);
}
.coupon-status text {
font-size: 24rpx;
font-weight: 600;
}
.coupon-status.used text {
color: #999;
}
.coupon-status.expired text {
color: #999;
}
/* 空状态 */
.empty {
display: flex;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.empty-text {
font-size: 26rpx;
color: #999;
}
</style>

View File

@@ -0,0 +1,99 @@
{
"id": "t-cropper",
"displayName": "t-cropper图片裁剪插件",
"version": "1.0.8",
"description": "vue3图片裁剪插件头像裁剪、支持自定义尺寸、等比例缩放、拖动、图片翻转、剪切圆形/圆角图片,高性能裁剪图片插件",
"keywords": [
"图片裁剪",
",头像裁剪,缩放,旋转,拖动",
""
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0",
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -0,0 +1,431 @@
<template>
<view class="" style="padding: 30rpx;">
<view class="" style="position: relative;">
<image v-if="!storeData.photo" :src="Service.GetMateUrlByImg('/static/userFunc/null.png')"
style="border-radius: 20rpx; width: 100%; height: 350rpx; " mode="aspectFit" alt="" />
<image v-else :src="Service.GetMateUrlByImg(storeData.photo)"
style="border-radius: 20rpx; width: 100%; height: 350rpx; " mode="aspectFill" alt="" />
<view @click="uploaduserImg()"
style=" display: flex; align-items: center; justify-content: center; border-radius: 50%; width: 70rpx; height: 70rpx; position: absolute; bottom: 24rpx; right: 12rpx; background-color: #F97316; ">
<img :src="Service.GetIconImg('/static/userFunc/photo-store.png')"
style="width: 40rpx; height: 40rpx; " alt="" />
</view>
</view>
<view class=""
style=" margin-top: 20rpx; padding: 20rpx;border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #E2e2e2; ">
<up-form labelWidth='180rpx' :labelStyle="{'font-weight':600}" labelPosition="top" :model="storeData" :
ref="form1">
<up-form-item label="店铺名称: " :borderBottom="true" ref="item1">
<up-input v-model="storeData.name" placeholder="请输入店铺名称" border="none"></up-input>
</up-form-item>
<up-form-item label="店铺地址: " :borderBottom="true" ref="item1">
<view class="">
<view v-if="!storeData.address" @click="chooseAddress()" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择地址 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="chooseAddress()" class="">
{{storeData.address}}
</view>
</view>
</up-form-item>
<up-form-item label="联系电话: " :borderBottom="true" ref="item1">
<up-input v-model="storeData.phone" placeholder="请输入您的联系电话" border="none"></up-input>
</up-form-item>
<up-form-item label="营业时间: " :borderBottom="true" ref="item1">
<view class="">
<view v-if="!storeData.time" @click="showDate=!showDate" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择营业时间 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showDate=!showDate" class="">
{{storeData.time}}
</view>
</view>
</up-form-item>
</up-form>
</view>
<view class=""
style="margin-top: 20rpx; padding: 20rpx;border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #E2e2e2; ">
<view style="display: flex; align-items: center; justify-content: space-between;" class="">
<view class="" style="font-weight: 600;" >
店铺标签
</view>
<view class="">
<up-icon @click="showTagfunc()" name="plus" color="#000" size="16" :bold='true'></up-icon>
</view>
</view>
<view class="" style="display: grid; grid-template-columns: repeat(4,1fr); ">
<view class="tag" v-for="(item,index) in tagList" :key="index"
style=" margin-top: 15rpx; padding: 10rpx 20rpx; border-radius: 8rpx; margin-right: 15rpx; background-color: var(--nav-mian); color: #fff; ">
{{item}}
</view>
</view>
</view>
<view class=""
style="margin-top: 20rpx; padding: 20rpx;border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #E2e2e2; ">
<view style="display: flex; align-items: center; justify-content: space-between;" class="">
<view class="" style="font-weight: 600;" >
所属社区
</view>
<view class="">
</view>
</view>
<view class="" style="padding: 15rpx 0;" >
<view v-if="!storeData.community" @click="showCommunity=!showCommunity" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择所属社区 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showCommunity=!showCommunity" class="">
{{storeData.community}}
</view>
</view>
</view>
<view class=""
style="margin-top: 20rpx; padding: 20rpx;border-radius: 20rpx; box-shadow: 0 0 10rpx 4rpx #E2e2e2; ">
<view style="display: flex; align-items: center; justify-content: space-between;" class="">
<view class="" style="font-weight: 600;" >
商品管理
</view>
<view class="">
</view>
</view>
<view class="" style="padding: 20rpx 0;" >
<view @click="Service.GoPage('/pages/goods/goodsDetail')" class="" style="display: flex; align-items: center; justify-content: space-between; " >
<view class="" style=" flex: 1; display: flex; align-items: center;" >
<image :src="Service.GetMateUrlByImg('/static/dele/dele3.png')" style="width: 150rpx; height: 150rpx; " mode="aspectFit" alt="" />
<view class="" style="margin-left: 20rpx;" >
<view class="" style="font-weight: 600;" >
招牌牛肉面
</view>
<view class="" style=" margin-top: 15rpx; color: #F97316; font-weight: 600; font-size: 28rpx; " >
¥28
</view>
</view>
</view>
<view class="">
<up-icon name="arrow-right" color="#c0c4cc" size="26"></up-icon>
</view>
</view>
<view class="tag" @click="Service.GoPage('/pages/goods/addGoods')" style=" color: #F97316; margin-top: 20rpx; width: 100%; border-radius: 15rpx; border: 5rpx dashed #F97316; padding: 20rpx; text-align: center; " >
<up-icon name="plus" color="#F97316" size="16" :bold='true'></up-icon>
<text style="margin-left: 20rpx;" >添加商品</text>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 200rpx; " >
</view>
</view>
<view class="" style="width: 100%; background-color: #fff; position: fixed; bottom: 0; left: 0; padding: 20rpx; ">
<up-button shape='circle' color='#FF6B35' text="保存修改"></up-button>
</view>
<!-- 弹窗 -->
<up-popup :show="showDate">
<view style="width: 100%; padding: 30rpx; ">
<view class="" style="display: flex; align-items: center; justify-content: space-between;">
<view class="">
</view>
<up-icon @click="showDate=!showDate" name="close" color="#000" size="18"></up-icon>
</view>
<up-form labelWidth='180rpx' :labelStyle="{'font-weight':600}" labelPosition="top" :model="storeData" :
ref="form1">
<up-form-item label="营业时间: ">
<view class="" style=" width: 100%; display: grid; grid-template-columns: repeat(4,1fr);">
<view v-for="(item,index) in 7" @click="chooseWeek(index)"
style=" margin-top: 15rpx; font-size: 32rpx; padding: 10rpx 30rpx;border-radius: 14rpx;"
:class="{ tab:weekList.findIndex(item => item == index)===-1,tabActive: weekList.findIndex(item => item == index)!=-1 }"
:key="index" class="tag">
周{{chinese(index)}}
</view>
</view>
</up-form-item>
<up-form-item label="开业时间: " labelPosition="left">
<view v-if="!openTime" @click="showOpenTime=!showOpenTime" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择开业时间 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showOpenTime=!showOpenTime" class="">
{{openTime}}
</view>
</up-form-item>
<up-form-item label="结业时间: " labelPosition="left">
<view v-if="!closeTime" @click="showCloseTime=!showCloseTime" class=""
style="display: flex; align-items: center;">
<text style="font-size: 26rpx; color: #c0c4cc; margin-right: 10rpx;">请选择开业时间 </text>
<up-icon name="arrow-right" color="#c0c4cc" size="12"></up-icon>
</view>
<view v-else @click="showCloseTime=!showCloseTime" class="">
{{closeTime}}
</view>
</up-form-item>
</up-form>
<view class="" style="margin-top: 20rpx;">
<up-button @click="dateconfiom" text="确认"></up-button>
</view>
<up-datetime-picker @confirm='showOpenTime=!showOpenTime' @cancel="showOpenTime=!showOpenTime"
:show="showOpenTime" v-model="openTime" mode="time"></up-datetime-picker>
<up-datetime-picker @confirm='showCloseTime=!showCloseTime' @cancel="showCloseTime=!showCloseTime"
:show="showCloseTime" v-model="closeTime" mode="time"></up-datetime-picker>
</view>
</up-popup>
<up-popup :show="showTag">
<view style="width: 100%; padding: 30rpx; ">
<view class="" style="display: flex; align-items: center; justify-content: space-between;">
<view class="">
</view>
<up-icon @click="showTag=!showTag" name="close" color="#000" size="18"></up-icon>
</view>
<view class="">
<view style="display: flex; align-items: center; justify-content: space-between;" class="">
<view class="">
店铺标签
</view>
<view class="">
</view>
</view>
<view class="" style="margin: 20rpx 0;">
<up-input placeholder="请输入标签" border="bottom" v-model="tag"></up-input>
</view>
<view class="" style="margin-top: 20rpx;">
<up-button @click="addTag()" text="确认"></up-button>
</view>
</view>
</view>
</up-popup>
<!-- 社区 -->
<up-picker :show="showCommunity" @confirm="communityFunc" @cancel='showCommunity=!showCommunity' :columns="columns"></up-picker>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
interface data {
photo : string,
name : string,
address : string,
phone : string,
time : string,
community : string
}
let storeData = ref<data>({
photo: '',
name: '',
address: '',
phone: '',
time: '',
community: ''
})
// 时间
let showDate = ref(false)
let weekList = ref<Array<number>>([])
let openTime = ref()
let showOpenTime = ref(false)
let closeTime = ref()
let showCloseTime = ref(false)
// 时间方式
let timefunc = ref(-1)
// 标签
let showTag = ref(false)
let tag = ref()
let tagList = ref<Array<string>>([])
// 社区
let showCommunity = ref(false)
const columns = ref([
['平安街', '镇魂街', '美食街']
]);
onLoad(() => {
});
onShow(() => {
});
// 社区
const communityFunc=(e:any)=>{
showCommunity.value=!showCommunity.value
storeData.value.community=e.value[0]
}
// 标签
const showTagfunc = () => {
showTag.value = !showTag.value
tag.value = ''
}
const addTag = () => {
if (!tag.value) {
Service.Msg('请输入标签内容')
return
}
tagList.value.push(tag.value)
showTag.value = !showTag.value
}
// 选择照片
const uploaduserImg = () => {
uni.chooseImage({
count: 1, // 最多选择1张图片
sizeType: ['original', 'compressed'], // 支持原图和压缩图
sourceType: ['album', 'camera'], // 可从相册选择或使用相机拍照
success: function (res) {
storeData.value.photo = res.tempFiles[0].path
// let path = res.tempFiles[0].path
// Service.uploadH5(path, 'Avatar', data => {
// userInfo.value.userPhoto = data.split(',')[2].split(':')[1].split('"')[1]
// })
},
fail: function (err) {
console.error('选择失败:', err.errMsg);
}
})
}
// 地址
const chooseAddress = () => {
wx.chooseLocation({
success: res => {
storeData.value.address = res.address
// latitude.value = res.latitude.toString();
// longitude.value = res.longitude.toString();
}
})
}
const chooseWeek = (index : number) => {
if (weekList.value.findIndex(item => item == index) != -1) {
let i = weekList.value.findIndex(item => item == index)
weekList.value.splice(i, 1)
return
}
weekList.value.push(index)
}
const chinese = (item : number) => {
if (item + 1 == 1) {
return '一'
}
if (item + 1 == 2) {
return '二'
} if (item + 1 == 3) {
return '三'
} if (item + 1 == 4) {
return '四'
} if (item + 1 == 5) {
return '五'
} if (item + 1 == 6) {
return '六'
} if (item + 1 == 7) {
return '日'
}
}
const dateconfiom = () => {
storeData.value.time = ''
if (weekList.value.length == 0) {
Service.Msg('请选择营业时间')
return
}
if (!openTime.value) {
Service.Msg('请选择开业时间')
return
}
if (!closeTime.value) {
Service.Msg('请选择结业时间')
return
}
if (openTime.value > closeTime.value) {
Service.Msg('开业时间不得小于结业时间')
return
}
weekList.value.sort((a, b) => {
return a - b
})
let timeIndex = weekList.value[0] - 0
let judgment = weekList.value.findIndex((item, index) => {
return item - index !== timeIndex
})
// 1是至 /0是全显示
if (judgment == -1) {
timefunc.value = 1
} else {
timefunc.value = 0
}
if (timefunc.value == 0) {
weekList.value.map((item) => {
storeData.value.time = storeData.value.time + '周' + chinese(item) + ' '
})
} else {
storeData.value.time = '周' + chinese((weekList.value[0])) + '至' + '周' + chinese((weekList.value[weekList.value.length - 1]))
}
storeData.value.time = storeData.value.time + ' ' + openTime.value + '-' + closeTime.value
showDate.value = !showDate.value
}
</script>
<style lang="scss">
.tag {
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
font-size: 24rpx;
}
.tabActive {
background-color: var(--nav-mian);
color: #fff;
}
.tab {
background-color: #F5F5F5;
color: #333333
}
</style>

View File

@@ -0,0 +1,531 @@
<template>
<view class="order-detail-page">
<view class="detail-container">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">订单详情</text>
<view class="nav-placeholder"></view>
</view>
<!-- 店铺信息卡片 -->
<view class="shop-info-card" @click="goToShop">
<view @click="Service.GoPage('/pages/community/merchantDetail?merchId='+merchInfo.merchId)"
class="shop-header">
<image class="shop-avatar" :src="Service.GetMateUrlByImg(merchInfo.logo)" mode="aspectFill" />
<view class="shop-info">
<text class="shop-name">{{ merchInfo.name }}</text>
<text class="order-time">{{ Service.formatDate(merchInfo.addTime,2) }}</text>
</view>
<up-icon name="arrow-right" bold="true" size="22"></up-icon>
</view>
</view>
<!-- 订单信息卡片 -->
<view class="order-info-card">
<view class="card-title">
<text class="title-icon">📋</text>
<text class="title-text">订单信息</text>
</view>
<view class="info-section">
<view class="info-row">
<text class="info-label">订单号</text>
<text class="info-value">{{ orderInfo.orderId }}</text>
</view>
<view class="info-row">
<text class="info-label">订单金额</text>
<text class="info-value amount">¥{{ orderInfo.amount }}</text>
</view>
<view class="info-row">
<text class="info-label">订单状态</text>
<view class="status-tag .status-completed">
<text class="status-tag-text">已完成</text>
</view>
</view>
<view v-if="orderInfo.getIntegral > 0" class="info-row">
<text class="info-label">获得积分</text>
<view class="points-badge earned">
<text class="ri-coin-line points-icon"></text>
<text class="points-text">+{{ orderInfo.getIntegral }}分</text>
</view>
</view>
<view v-if="orderInfo.useIntegral > 0" class="info-row">
<text class="info-label">使用积分</text>
<view class="points-badge used">
<text class="ri-wallet-3-line points-icon"></text>
<text class="points-text">-{{ orderInfo.useIntegral }}分</text>
</view>
</view>
<view v-if="orderInfo.discount" class="info-row">
<text class="info-label">优惠券</text>
<view class="coupon-badge">
<text
class="coupon-text">{{ '满'+JSON.parse(orderInfo.discount).needMoney+'减'+JSON.parse(orderInfo.discount).deductMoney }}</text>
</view>
</view>
<view class="info-row">
<text class="info-label">付款金额</text>
<text class="info-value amount">¥{{ orderInfo.payAmount }}</text>
</view>
<view class="info-row">
<text class="info-label">支付方式</text>
<text class="info-value">{{ orderInfo.payway=='wx'?'微信':'支付宝' }}</text>
</view>
<view class="info-row">
<text class="info-label">下单时间</text>
<text class="info-value">{{ orderInfo.payTime }}</text>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<!-- <view v-if="order.statusType === 'completed'" class="bottom-actions">
<button class="action-btn secondary" @click="contactService">
<text class="ri-customer-service-2-line btn-icon"></text>
<text class="btn-text">联系客服</text>
</button>
<button class="action-btn primary" @click="writeReview">
<text class="ri-edit-line btn-icon"></text>
<text class="btn-text">评价订单</text>
</button>
</view>
<view v-else-if="order.statusType === 'pending'" class="bottom-actions">
<button class="action-btn secondary" @click="contactService">
<text class="ri-customer-service-2-line btn-icon"></text>
<text class="btn-text">联系客服</text>
</button>
<button class="action-btn primary" @click="writeReview">
<text class="ri-star-line btn-icon"></text>
<text class="btn-text">评价订单</text>
</button>
</view>
<view v-else-if="order.statusType === 'refunding'" class="bottom-actions">
<button class="action-btn warning" @click="checkRefundStatus">
<text class="ri-time-line btn-icon"></text>
<text class="btn-text">查看退款进度</text>
</button>
</view> -->
</view>
<!-- 加载状态 -->
<!-- <view v-else class="loading">
<text class="loading-text">加载中...</text>
</view> -->
</view>
</template>
<script lang="ts" setup>
import {
ref
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
vpOrderService,
Service
} from '@/Service/vp/vpOrderService'
let merchInfo = ref<any>({})
let orderInfo = ref<any>({})
let orderId = ref('')
// 页面加载
onLoad((options) => {
if (options.orderId) {
orderId.value = options.orderId
getData()
}
})
const getData = () => {
vpOrderService.GetOrderInfo(orderId.value).then(res => {
if (res.code == 0) {
merchInfo.value = res.data.merchInfo
orderInfo.value = res.data.orderInfo
} else {
Service.Msg(res.msg)
}
})
}
// 跳转到商家详情
const goToShop = () => {
if (!order.value) return
// 使用商家ID跳转这里使用order.id作为商家ID
uni.navigateTo({
url: `/pages/shop/shop?id=${order.value.id}`
})
}
// 返回
const goBack = () => {
uni.navigateBack({
delta: 1
})
}
// 联系客服
const contactService = () => {
uni.showToast({
title: '正在联系客服...',
icon: 'none'
})
}
// 评价订单
const writeReview = () => {
uni.showToast({
title: '跳转到评价页面',
icon: 'none'
})
}
// 查看退款进度
const checkRefundStatus = () => {
uni.showToast({
title: '退款处理中,请耐心等待',
icon: 'none'
})
}
</script>
<style lang="scss" scoped>
.order-detail-page {
min-height: 100vh;
background: #F5F5F5;
}
.detail-container {
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 50rpx 24rpx 20rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* 店铺信息卡片 */
.shop-info-card {
background: #FFFFFF;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
cursor: pointer;
transition: all 0.3s ease;
}
.shop-info-card:active {
transform: scale(0.98);
opacity: 0.9;
}
.shop-header {
display: flex;
align-items: center;
position: relative;
}
.shop-arrow {
font-size: 32rpx;
color: #CCCCCC;
margin-left: auto;
position: absolute;
right: 0;
}
.shop-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 16rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.shop-name {
font-size: 32rpx;
font-weight: 700;
color: #222222;
}
.order-time {
font-size: 24rpx;
color: #999999;
}
.order-status-badge {
padding: 12rpx 24rpx;
border-radius: 24rpx;
font-size: 24rpx;
font-weight: 600;
}
.order-status-badge.status-completed {
background: #E8F5E9;
color: #4CAF50;
}
.order-status-badge.status-pending {
background: #FFF4E6;
color: #FF6B00;
}
.order-status-badge.status-refunding {
background: #E3F2FD;
color: #2196F3;
}
.order-status-badge.status-refunded {
background: #FFEBEE;
color: #F44336;
}
/* 订单信息卡片 */
.order-info-card {
background: #FFFFFF;
margin: 0 20rpx 20rpx;
border-radius: 16rpx;
padding: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.card-title {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #F5F5F5;
}
.title-icon {
font-size: 32rpx;
}
.title-text {
font-size: 28rpx;
font-weight: 600;
color: #222222;
}
.info-section {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-label {
font-size: 26rpx;
color: #666666;
}
.info-value {
font-size: 26rpx;
color: #222222;
font-weight: 500;
}
.info-value.amount {
font-size: 36rpx;
color: #FF6B00;
font-weight: 700;
}
.status-tag {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
}
.status-tag.status-completed {
background: #E8F5E9;
color: #4CAF50;
}
.status-tag.status-pending {
background: #FFF4E6;
color: #FF6B00;
}
.status-tag.status-refunding {
background: #E3F2FD;
color: #2196F3;
}
.status-tag.status-refunded {
background: #FFEBEE;
color: #F44336;
}
/* 积分徽章 */
.points-badge {
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 600;
}
.points-badge.earned {
background: #E8F5E9;
color: #4CAF50;
}
.points-badge.used {
background: #FFF4E6;
color: #FF6B00;
}
.points-icon {
font-size: 20rpx;
}
.points-text {
font-size: 24rpx;
}
/* 优惠券徽章 */
.coupon-badge {
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 16rpx;
background: linear-gradient(135deg, #FFF4E6, #FFE0B2);
border-radius: 20rpx;
}
.coupon-icon {
font-size: 20rpx;
color: #FF6B00;
}
.coupon-text {
font-size: 24rpx;
color: #FF6B00;
font-weight: 600;
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 20rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
gap: 12rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
font-size: 26rpx;
font-weight: 500;
border: none;
}
.action-btn.secondary {
background: linear-gradient(135deg, #F5F5F5, #EEEEEE);
color: #666666;
}
.action-btn.primary {
background: linear-gradient(135deg, #FF6B00, #FF9500);
color: #FFFFFF;
}
.action-btn.warning {
background: linear-gradient(135deg, #2196F3, #42A5F5);
color: #FFFFFF;
flex: 1;
}
.btn-icon {
font-size: 28rpx;
}
.btn-text {
font-size: 26rpx;
}
/* 加载状态 */
.loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.loading-text {
font-size: 26rpx;
color: #999999;
}
</style>

View File

@@ -0,0 +1,22 @@
## 1.1.72024-10-29
修复底部露出部分组件bug
## 1.1.62024-08-20
更新本地数据源为最新数据源
## 1.1.52024-06-12
使用说明文档优化
## 1.1.42024-06-12
增加问题反馈描述
## 1.1.32024-02-29
更新使用文档
## 1.1.22024-01-16
解决Vue3项目导入导出报错问题
## 1.1.12023-12-06
defaultValue可以传入defaultValue:['河北省','唐山市','丰南区']数组类型以及defaultValue: '420103'地区编码字符串类型
## 1.1.02023-12-05
即默认值传入地区编码,也支持传入中文省市区数组
## 1.0.92023-12-04
优化
## 1.0.82023-10-24
修复东菀市和中山市下各镇的行政编码错误问题
## 1.0.42023-09-15
改为uni_modules规范

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
import { Service } from '@/Service/Service';
/*****积分商城*****/
class vpGoodsService {
private static GetGoodsListPath : string = '/Goods/GetGoodsList';
/*****获取商品列表*****/
static GetGoodsList(typeId:string,page:number) {
var result = Service.Request(this.GetGoodsListPath, 'GET', {typeId,page});
return result;
}
private static GetGoodsInfoPath : string = '/Goods/GetGoodsInfo';
/*****获取商品详情*****/
static GetGoodsInfo(goodsId:string) {
var result = Service.Request(this.GetGoodsInfoPath, 'GET', {goodsId});
return result;
}
private static AddGoodsOrderPath : string = '/Goods/AddGoodsOrder';
/*****生成订单*****/
static AddGoodsOrder(goodsId:string,count:number) {
var result = Service.Request(this.AddGoodsOrderPath, 'POST', {goodsId,count});
return result;
}
private static GetOrderInfoPath : string = '/Goods/GetOrderInfo';
/*****订单详情*****/
static GetOrderInfo(orderId:string) {
var result = Service.Request(this.GetOrderInfoPath, 'GET', {orderId});
return result;
}
private static UpdateOrderAddressPath : string = '/Goods/UpdateOrderAddress';
/*****修改订单地址*****/
static UpdateOrderAddress(orderId:string,addressId:string) {
var result = Service.Request(this.UpdateOrderAddressPath, 'POST', {orderId,addressId});
return result;
}
private static PayOrderPath : string = '/Goods/PayOrder';
/*****支付订单*****/
static PayOrder(orderId:string) {
var result = Service.Request(this.PayOrderPath, 'POST', {orderId});
return result;
}
private static GetUserOrderListPath : string = '/Goods/GetUserOrderList';
/*****获取用户订单列表*****/
static GetUserOrderList(state:string,page:number) {
var result = Service.Request(this.GetUserOrderListPath, 'GET', {state,page});
return result;
}
}
export { Service, vpGoodsService };

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -0,0 +1,29 @@
import { Service } from '@/Service/Service';
/*****社区*****/
class vpMerchService {
private static GetMerchListPath: string = '/Merch/GetMerchList';
/*****商家列表*****/
static GetMerchList(assId: string,comId:string,lon:number,lat:number,page:number) {
var result = Service.Request(this.GetMerchListPath, 'GET', {assId,comId,lon,lat,page});
return result;
}
private static GetMerchInfoPath: string = '/Merch/GetMerchInfo';
/*****店铺详情*****/
static GetMerchInfo(merchId: string,page:number) {
var result = Service.Request(this.GetMerchInfoPath, 'GET', {merchId,page});
return result;
}
private static GetGoodsInfoPath: string = '/Merch/GetGoodsInfo';
/*****商品详情*****/
static GetGoodsInfo(goodsId: string,page:number) {
var result = Service.Request(this.GetGoodsInfoPath, 'GET', {goodsId,page});
return result;
}
}
export { Service, vpMerchService };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,312 @@
<template>
<view class="favorites-page">
<!-- 沉浸式状态栏 -->
<view v-if="!isZFB" class="status-bar"></view>
<!-- 顶部导航栏 -->
<view v-if="!isZFB" class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">我的收藏</text>
<view class="nav-placeholder"></view>
</view>
<!-- 收藏列表 -->
<view class="content">
<view v-if="collectList.length > 0" class="favorites-list">
<view v-for="item in collectList" :key="item.merchId" class="shop-card" @click="Service.GoPage('/pages/community/merchantDetail?merchId='+item.merchId)">
<!-- 店铺图片 -->
<image class="shop-image" :src="Service.GetMateUrlByImg(item.logo)" mode="aspectFill" />
<!-- 店铺信息 -->
<view class="shop-info">
<text class="shop-name">{{ item.name }}</text>
<view class="shop-meta">
<text class="ri-star-fill rating-icon"></text>
<text class="rating-text">{{ Number(item.score ).toFixed(1)}}分</text>
<text class="sales-text">月售{{ item.sale }}</text>
</view>
<view class="shop-tags">
<view v-for="(coupon, idx) in item.tips" :key="idx" :class="getTagClass(coupon)" style="font-size: 24rpx;"
class="shop-tag">
{{ coupon }}
</view>
</view>
</view>
<!-- 取消收藏按钮 -->
<view class="cancel-btn" @click.stop="cancelFavorite(item)">
<up-icon name="heart-fill" color="#fc5151" :bold="true" size="18"></up-icon>
</view>
</view>
<up-loadmore :status="status" />
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="ri-heart-line empty-icon"></text>
<text class="empty-text">暂无收藏店铺</text>
<text class="empty-desc">去首页逛逛,收藏喜欢的店铺吧</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {onShow,onLoad,onReachBottom} from "@dcloudio/uni-app";
import {Service} from "@/Service/Service"
import { ref, computed } from "vue";
import { vpDiscountService } from "@/Service/vp/vpDiscountService"
import { vpUserService } from "@/Service/vp/vpUserService"
import { inject } from 'vue';
const isZFB = inject('isZFB');
let collectList = ref<Array<any>>([])
let status = ref<string>('loadmore')
let pageNo = ref<number>(1)
let longitude=ref(0)
let latitude=ref(0)
onLoad(()=>{
getLocation()
})
onReachBottom(()=>{
getList()
})
// 返回
const goBack = () => {
Service.GoPageBack()
}
const getLocation = () => {
uni.getLocation({
type: 'wgs84',
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
getData()
},
fail: function (e) {
console.log(e);
}
});
}
const getData=()=>{
pageNo.value=1
status.value='loadmore'
collectList.value=[]
getList()
}
const getList=()=>{
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpUserService.GetUserCollectList(longitude.value,latitude.value,pageNo.value).then(res=>{
if(res.code==0){
collectList.value=[...collectList.value,...res.data.collectList]
status.value=res.data.collectList.length==10?'loadmore':'nomore'
pageNo.value++
}
})
}
// 根据标签文本获取样式类
const getTagClass = (tagText : string) => {
const tagMap = {
'可用积分': 'tag-points-available',
'可用券': 'tag-coupon'
}
return tagMap[tagText] || 'tag-points'
}
// 取消收藏
const cancelFavorite = (item:any) => {
uni.showModal({
title: '取消收藏',
content: `确定取消收藏吗?`,
success: (res) => {
if (res.confirm) {
vpUserService.CollectMerch(item.merchId).then(res=>{
if(res.code==0){
getData()
}else{
Service.Msg(res.msg)
}
})
}
}
})
}
</script>
<style lang="scss" scoped>
.favorites-page {
min-height: 100vh;
background: #F5F5F5;
}
.action-pill-icon {
font-size: 28rpx;
color: #FF6B00;
line-height: 1;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 24rpx 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* 内容区域 */
.content {
padding: 20rpx;
}
/* 收藏列表 */
.favorites-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.shop-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 20rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
position: relative;
}
.shop-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.shop-name {
font-size: 32rpx;
font-weight: 600;
color: #222222;
}
.shop-meta {
display: flex;
align-items: center;
gap: 8rpx;
}
.rating-icon {
font-size: 24rpx;
color: #FFB800;
}
.rating-text {
font-size: 24rpx;
color: #FF6B00;
font-weight: 600;
}
.sales-text {
font-size: 22rpx;
color: #999999;
}
.shop-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 20rpx;
color: #666666;
background: #F5F5F5;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.cancel-btn {
width: 64rpx;
height: 64rpx;
background: #FFEBEE;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 16rpx;
flex-shrink: 0;
}
.cancel-icon {
font-size: 32rpx;
color: #F44336;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
gap: 16rpx;
}
.empty-icon {
font-size: 120rpx;
color: #CCCCCC;
}
.empty-text {
font-size: 28rpx;
color: #999999;
font-weight: 500;
}
.empty-desc {
font-size: 24rpx;
color: #CCCCCC;
}
</style>

View File

@@ -0,0 +1,231 @@
<template>
<view style="display: flex; flex-direction: column; height: 100vh; ">
<view class="merchant-info">
<view class="">
<text class="section-title">付款给商户</text>
<text class="merchant-name">星巴克 · 北京国贸店</text>
</view>
<image :src="Service.GetMateUrlByImg('/static/dele/dele4.jpg')" mode="aspectFill" class="merchant-icon">
</image>
</view>
<view class=""
style=" padding: 30rpx 40rpx; flex: 1; background-color: #fff; width: 100%; border-top-right-radius: 30rpx; border-top-left-radius: 30rpx; ">
<view class="" style="font-size: 24rpx; font-weight: 600;">
余额
</view>
<view class="" style="margin: 20rpx 0; padding: 20rpx 0; border-bottom: 1rpx solid #e2e2e2; ">
<!-- <up-input prefixIcon='rmb' :prefixIconStyle="{ 'color':'#000','font-weight': 600,'font-size':'60rpx' }"
fontSize='50rpx'
auto-blur="false"
@focus="focusFunc"
:customStyle="{'color':'#000', height: '90rpx', 'padding-left': 0, 'font-weight': 600,'padding-bottom':'20rpx' }"
border="bottom" v-model="account"></up-input> -->
<view class="" style="display: flex; align-items: center; width: 100%; " >
<view class="" style="height: 70rpx; display: flex; align-items: center; " >
<up-icon name="rmb" bold='true' size='50rpx' color="#000" ></up-icon>
</view>
<view class="" style=" height: 70rpx; line-height: 70rpx; font-weight: 600;font-size: 70rpx; " >
{{account}}
</view>
<view class="" v-if="isShow" style="margin: 0 10rpx; width: 4rpx; height: 70rpx; background-color: var(--nav-mian); " >
</view>
</view>
</view>
<view class="" style="font-size: 24rpx; color: #6B6B6B; ">
<text>我的积分 1,250 积分</text>
<text style="margin-left: 10rpx;">可抵扣 ¥12.50</text>
</view>
<view class="" style="font-size: 28rpx; margin-top: 10rpx; ">
<text :style="{'margin-right':!des?'':'15rpx'}">{{des}}</text>
<text @click="showDes=true" style="font-weight: 600; color: #586B95;">添加备注</text>
</view>
<view class=""
style="background-color: #f5f5f5; padding: 20rpx; position: fixed; bottom: 0; left: 0; width: 100%; padding-top: 25rpx; padding-bottom: 20rpx; ">
<view class="" style="display: grid; grid-template-columns: repeat(4,1fr); ">
<view class="button" @click="input(item)" v-for="(item,index) in 3" :key="index">
{{item}}
</view>
<view @click="deleInput()" class="button">
<up-icon name="backspace" :bold='true' size="26"></up-icon>
</view>
</view>
<view class="" style="display: grid; grid-template-columns: 3fr 1fr; ">
<view class="">
<view class="" style="display: grid; grid-template-columns: repeat(3,1fr); ">
<view class="button" @click="input(item+3)" v-for="(item,index) in 3" :key="index">
{{item+3}}
</view>
</view>
<view class="" style="display: grid; grid-template-columns: repeat(3,1fr); ">
<view class="button" @click="input(item+6)" v-for="(item,index) in 3" :key="index">
{{item+6}}
</view>
</view>
<view class="" style="display: grid; grid-template-columns: 2fr 1fr; ">
<view @click="input(0)" class="button" >
0
</view>
<view @click="input('.')" class="button" >
.
</view>
</view>
</view>
<view @click="save()" class="button" style="background-color: var(--nav-mian); color: #fff; " >
付款
</view>
</view>
</view>
</view>
</view>
<up-popup :show="showDes">
<view style="width: 100%; padding: 50rpx 30rpx; ">
<view class="">
<text style="font-size: 28rpx; font-weight: 600;">添加备注</text>
</view>
<view class=""
style=" margin-top: 30rpx; padding: 20rpx 0; border-bottom: 1rpx solid #e2e2e2; border-top: 1rpx solid #e2e2e2; ">
<up-input placeholder="请输入内容" border="none" v-model="des"></up-input>
</view>
<view class=""
style=" margin: 0 110rpx; margin-top: 50rpx; display: flex; align-items: center; justify-content: space-between; ">
<view class="" @click="showDes=false,des=''"
style=" background-color: #f2f2f2; color: #000; padding: 20rpx 80rpx;border-radius: 20rpx; display: flex; align-items: center; justify-content: center; ">
取消
</view>
<view class="" @click="showDes=false"
style=" background-color: #07c160; color: #fff; padding: 20rpx 80rpx;border-radius: 20rpx; display: flex; align-items: center; justify-content: center; ">
确定
</view>
</view>
</view>
</up-popup>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { onUnmounted, ref } from "vue";
let account = ref('')
let showDes = ref(false)
let des = ref()
let isShow=ref(false)
let timeOut=ref()
onLoad(() => {
focusFunc()
});
onShow(() => {
});
onUnmounted(()=>{
clearInterval(timeOut.value)
})
const input=(val:any)=>{
if(account.value.split('').length>8){
return
}
if(val=='.'){
let arr=account.value.split('').filter((item=>item==val))
if(arr.length>0){
return
}
if(!account.value){
account.value='0.'
return
}
}
account.value=account.value+val
}
const deleInput=()=>{
let arr=account.value.split('')
arr.pop()
account.value=arr.join('')
}
const save=()=>{
}
const focusFunc=()=>{
timeOut.value=setInterval(()=>{
isShow.value=!isShow.value
},1000)
}
</script>
<style lang="scss">
page {
background-color: #f5f5f5;
}
// 按键
.button {
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 18rpx 0;
border-radius: 10rpx;
font-weight: 700;
margin: 8rpx;
font-size: 38rpx;
}
.button:active {
background-color: #ababab;
}
/* 商户信息 */
.merchant-info {
padding: 30rpx 40rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.merchant-name {
font-size: 24rpx;
color: #999999;
display: block;
margin-top: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
}
.merchant-icon {
width: 95rpx;
height: 95rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
</style>

View File

@@ -0,0 +1,24 @@
import { Service } from '@/Service/Service';
class vpLoginService {
private static GetOpenIdByWeixinPath: string = '/Login/GetOpenIdByWeixin';
/*****获取openid*****/
static GetOpenIdByWeixin(code: string,type:number) {
var result = Service.Request(this.GetOpenIdByWeixinPath, 'GET', {code,type});
return result;
}
private static WxLoginPath: string = '/Login/WxLogin';
/*****获取openid*****/
static WxLogin(code: string,type:number,lon:number,lat:number) {
var result = Service.Request(this.WxLoginPath, 'GET', {code,type,lon,lat});
return result;
}
}
export { vpLoginService }

View File

@@ -0,0 +1,993 @@
<template>
<view class="category-page">
<!-- 页面加载中 - 显示骨架屏 -->
<view v-if="shopLoading" class="page-loading-wrapper">
<!-- 顶部导航栏骨架屏 -->
<view v-if="!isZFB" class="nav-bar">
<view class="nav-content">
<view class="skeleton-nav-back"></view>
<view class="skeleton-nav-title"></view>
</view>
</view>
<!-- 搜索框骨架屏 -->
<view class="search-section">
<view class="search-box-skeleton"></view>
</view>
<!-- 分类标签栏骨架屏 -->
<view class="category-tabs">
<view class="skeleton-tabs-list">
<view class="skeleton-tab-item" v-for="i in 5" :key="i">
<view class="skeleton-tab-icon"></view>
<view class="skeleton-tab-text"></view>
</view>
</view>
</view>
<!-- 筛选栏骨架屏 -->
<view class="filter-section">
<view class="skeleton-filter-row">
<view class="skeleton-filter-sort"></view>
<view class="skeleton-filter-all"></view>
</view>
<view class="skeleton-quick-filters">
<view class="skeleton-quick-filter-item" v-for="i in 4" :key="i"></view>
</view>
</view>
<!-- 商家列表骨架屏 -->
<view class="shop-list">
<SkeletonShopCardHorizontal v-for="i in 5" :key="i" />
</view>
</view>
<!-- 页面加载完成 - 显示实际内容 -->
<view v-else>
<!-- 顶部导航栏 - 橙色背景 -->
<view v-if="!isZFB" class="nav-bar">
<view class="nav-content">
<view class="nav-back" @click="Service.GoPageBack()">
<image class="back-icon" src="/static/icons/back.svg" mode="aspectFit" />
</view>
<text class="nav-title">{{ pageTitle }}</text>
<view class="" style="width: 64rpx; height: 64rpx;">
</view>
</view>
</view>
<!-- 搜索框及内容区域 -->
<view class="content-wrapper" :style="{'margin-top':isZFB?'0':'-85px'}" >
<!-- 固定搜索框 -->
<view class="search-fixed" :class="{ show: isSearchFixed }">
<view class="search-box">
<image class="search-icon" src="https://img.icons8.com/ios-glyphs/30/999999/search--v1.png"
mode="aspectFit" />
<input v-model="search" @blur="getdata()" class="search-input" placeholder="请输入商家或商品名称"
placeholder-class="search-placeholder" />
</view>
</view>
<!-- 原搜索框占位 -->
<view class="search-section">
<view class="search-box">
<image class="search-icon" src="https://img.icons8.com/ios-glyphs/30/999999/search--v1.png"
mode="aspectFit" />
<input @confirm="getdata()" v-model="search" class="search-input" placeholder="请输入商家或商品名称"
placeholder-class="search-placeholder" />
</view>
</view>
<!-- 筛选栏 -->
<view class="filter-section">
<!-- 排序选项 -->
<view class="filter-row">
<view class="filter-sort" @click="toggleSortDropdown">
<text class="filter-label">{{ currentSort }}</text>
<text class="ri-arrow-down-s-line arrow-icon" :class="{ rotate: showSortDropdown }"></text>
<!-- 排序下拉菜单 -->
<view v-if="showSortDropdown" class="dropdown-menu sort-dropdown">
<view v-for="(option, index) in sortOptions" :key="index" class="dropdown-item"
@click.stop="selectSort(index)">
<text class="dropdown-text">{{ option }}</text>
</view>
</view>
</view>
<view class="filter-all" @click="toggleFilterDropdown">
<text class="filter-all-text">{{ currentFilter }}</text>
<text class="ri-arrow-down-s-line filter-all-arrow"
:class="{ rotate: showFilterDropdown }"></text>
<!-- 筛选下拉菜单 -->
<view v-if="showFilterDropdown" class="dropdown-menu filter-dropdown">
<view v-for="(option, index) in filterOptions" :key="index" class="dropdown-item"
@click.stop="selectFilter(index)">
<text class="dropdown-text">{{ option }}</text>
</view>
</view>
</view>
</view>
<!-- 快捷筛选按钮 -->
<!-- <scroll-view scroll-x class="quick-filters" show-scrollbar="false">
<view class="quick-filter-list">
<view v-for="(filter, index) in quickFilters" :key="index" class="quick-filter-item"
:class="{ active: filter.active }" @click="toggleFilter(index)">
<text class="filter-text">{{ filter.name }}</text>
</view>
</view>
</scroll-view> -->
</view>
<!-- 商家列表 -->
<view class="shop-list">
<view v-for="shop in merchList" :key="shop.merchId" class="shop-card"
@click="Service.GoPage('/pages/community/merchantDetail?merchId='+shop.merchId)">
<!-- 左侧图片 -->
<image class="shop-image" :src="Service.GetMateUrlByImg(shop.logo)" mode="aspectFill" />
<!-- 右侧信息 -->
<view class="shop-info">
<!-- 店名 -->
<view class="shop-header">
<text class="shop-name">{{ shop.name }}</text>
</view>
<!-- 评分和销量 -->
<view class="shop-info-left">
<view class="shop-rating">
<text class="rating-score">{{ Number(shop.score).toFixed(1) }}</text>
<text class="rating-unit">分</text>
</view>
<text class="shop-sales">月售{{ shop.sale }}</text>
</view>
<!-- 距离、用时、人均价格 - 同一行 -->
<view class="shop-info-row">
<!-- 距离和用时 - 左侧 -->
<view class="shop-info-left">
<view class="distance-wrapper">
<up-icon name="map" color="#000" size="12"></up-icon>
<text class="distance-text">{{ formatDistance(shop.distance) }}</text>
</view>
</view>
<!-- 人均价格 - 右侧 -->
<view v-if="shop.avgPrice" class="shop-avg-price-inline">
<text class="avg-price-small">¥</text>
<text class="avg-price-value-small">{{ shop.price }}</text>
<text class="avg-price-text-small">人均</text>
</view>
</view>
<!-- 底部标签 -->
<view class="shop-bottom-tags">
<text v-for="(tag, index) in shop.tips" :key="index" class="shop-tag"
style="font-size: 24rpx; margin-left: 4rpx;" :class="getTagClass(tag)">
{{ tag }}
</text>
</view>
<!-- 右上角标签 -->
<view class="shop-badges" style="top: 24rpx;">
<text v-if="shop.code=='Discounts'" class="badge-partner"
style="font-size: 24rpx;">联盟商家</text>
<text v-else class="badge-coop" style="font-size: 24rpx;">合作商家</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="merchList.length== 0" class="no-results" style="text-align: center;">
<text class="no-results-text">暂无商家</text>
</view>
<up-loadmore v-else :status="status" />
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {
ref,
computed
} from 'vue'
import {
onLoad,
onPageScroll,
onReachBottom
} from '@dcloudio/uni-app'
import SkeletonShopCardHorizontal from '../../components/skeleton/skeleton-shop-card-horizontal.vue'
import { Service } from "@/Service/Service"
import { vpMerchService } from '@/Service/vp/vpMerchService'
import { inject } from 'vue';
const isZFB = inject('isZFB');
// 页面数据
const pageTitle = ref('')
// 固定搜索框状态
const isSearchFixed = ref(false)
const categoryType = ref('food')
const shops = ref<any>([])
const currentCategory = ref(0)
// 加载状态
const shopLoading = ref(true)
// 下拉菜单状态
const showSortDropdown = ref(false)
const showFilterDropdown = ref(false)
// 当前选择的排序和筛选
let currentSort = ref('距离最近')
let sortIndex = ref(0)
let currentFilter = ref('全部')
let filterIndex = ref(0)
// 排序选项
const sortOptions = ['距离最近', '销量最高', '评分最高']
// 筛选选项
const filterOptions = ['全部', '联盟商家', '可用积分', '可用券', '合作商家']
// 快捷筛选
const quickFilters = ref([{
name: '15分钟',
active: false
},
{
name: '品质必点',
active: false
},
{
name: '堂食店',
active: false
},
{
name: '联盟商家',
active: true
}
])
let search = ref('')
let longitude = ref(0)
let latitude = ref(0)
let merchList = ref<Array<any>>([])
let status = ref('nomore')
let page = ref(1)
// 页面加载
onLoad((options : any) => {
// 接收从首页传递的分类类型
if (options.name) {
pageTitle.value = options.name
categoryType.value = options.type
getLocation()
}
})
onReachBottom(() => {
getList()
})
// 页面滚动监听 - 控制固定搜索框显示
onPageScroll((e) => {
// 当滚动超过100px时显示固定搜索框
isSearchFixed.value = e.scrollTop > 100
})
const getLocation = () => {
uni.getLocation({
type: 'wgs84',
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
getdata()
},
fail: function (e) {
console.log(e);
}
});
}
const getdata = () => {
merchList.value = []
status.value = 'loadmore'
page.value = 1
getList()
}
const getList = () => {
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpMerchService.GetMerchList(categoryType.value, search.value, longitude.value, latitude.value, sortIndex.value, filterIndex.value, page.value).then(res => {
if (res.code == 0) {
shopLoading.value = false
merchList.value = [...merchList.value, ...res.data.merchList]
status.value = res.data.merchList.length == 10 ? 'loadmore' : 'nomore'
page.value++
}
})
}
// 获取标签样式类
const getTagClass = (tagText) => {
const tagMap = {
'12%积分': 'tag-points',
'15%积分': 'tag-points',
'可用积分': 'tag-points-available',
'可用券': 'tag-coupon'
}
return tagMap[tagText] || 'tag-points'
}
// 切换筛选
const toggleFilter = (index) => {
quickFilters.value[index].active = !quickFilters.value[index].active
}
// 切换排序下拉菜单
const toggleSortDropdown = () => {
showSortDropdown.value = !showSortDropdown.value
showFilterDropdown.value = false
}
// 切换筛选下拉菜单
const toggleFilterDropdown = () => {
showFilterDropdown.value = !showFilterDropdown.value
showSortDropdown.value = false
}
// 选择排序方式
const selectSort = (index : number) => {
currentSort.value = sortOptions[index]
sortIndex.value = index
showSortDropdown.value = false
getdata()
}
// 选择筛选条件
const selectFilter = (index : number) => {
currentFilter.value = filterOptions[index]
showFilterDropdown.value = false
if (index > 0 && index <= 3) {
filterIndex.value = 1
}
if (index == 0) {
filterIndex.value = index
}
if (index == 4) {
filterIndex.value = 2
}
getdata()
}
// 点击其他地方关闭下拉菜单
const closeDropdowns = () => {
showSortDropdown.value = false
showFilterDropdown.value = false
}
// 格式化距离
const formatDistance = (distance : any) => {
if (distance < 1) {
return `${Number(distance).toFixed(1)}m`
}
return `${(distance).toFixed(1)}km`
}
// 返回首页
const goBackHome = () => {
}
</script>
<style lang="scss" scoped>
.category-page {
min-height: 100vh;
background: #F5F5F5;
padding-bottom: 60px;
}
/* 顶部导航栏 */
.nav-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: calc(var(--status-bar-height) + 320rpx);
position: relative;
z-index: 1;
}
/* 导航内容区域 - 固定位置 */
.nav-content {
position: absolute;
left: 0;
right: 0;
top: 80rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-back {
margin-left: 20rpx;
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #FFFFFF;
}
/* 搜索框及内容区域 */
.content-wrapper {
position: relative;
z-index: 2;
margin-top: -85px;
background: #FAFAFA;
border-top-left-radius: 25px;
border-top-right-radius: 25px;
padding-top: 16px;
}
/* 搜索框 */
.search-section {
background: transparent;
padding: 8px 16px 12px;
}
.search-box {
background: #F5F5F5;
border-radius: 10px;
height: 40px;
display: flex;
align-items: center;
padding: 0 12px;
}
.search-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
.search-input {
flex: 1;
font-size: 14px;
color: #222222;
height: 40px;
}
.search-placeholder {
color: #999999;
}
/* 固定搜索框 - 滚动时显示在顶部 */
.search-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background: #FAFAFA;
padding: 8px 16px 12px;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.search-fixed.show {
transform: translateY(0);
}
/* 分类标签栏 */
.category-tabs {
background: #FFFFFF;
padding: 16px 0;
margin-bottom: 10px;
}
.tabs-scroll {
white-space: nowrap;
/* 隐藏滚动条 - 兼容不同平台 */
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
/* 隐藏滚动条 - Webkit浏览器 */
.tabs-scroll ::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
background: transparent;
}
.tabs-list {
display: inline-flex;
padding: 0 16px;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
min-width: 70px;
height: auto;
min-height: 70px;
}
.tab-item.active .tab-text {
color: #FF5722;
font-weight: 600;
}
.tab-item.active {
background: transparent;
}
.tab-icon {
width: 48px;
height: 48px;
margin-bottom: 6px;
}
.tab-item.active .tab-icon {
filter: none;
}
.tab-text {
font-size: 14px;
color: #333333;
}
/* 筛选栏 */
.filter-section {
background: #FFFFFF;
padding: 10px 16px;
margin-bottom: 10px;
}
.filter-row {
display: flex;
align-items: center;
justify-content: space-between;
// margin-bottom: 10px;
}
.filter-sort {
display: flex;
align-items: center;
position: relative;
}
.filter-label {
font-size: 14px;
color: #333333;
font-weight: 500;
}
.arrow-icon {
font-size: 12px;
color: #333333;
margin-left: 4px;
transition: transform 0.3s;
}
.arrow-icon.rotate {
transform: rotate(180deg);
}
.filter-all {
display: flex;
align-items: center;
padding: 4px 10px;
background: transparent;
border-radius: 4px;
position: relative;
}
.filter-all-text {
font-size: 14px;
color: #333333;
margin-right: 4px;
}
.filter-all-arrow {
font-size: 12px;
color: #333333;
transition: transform 0.3s;
}
.filter-all-arrow.rotate {
transform: rotate(180deg);
}
/* 下拉菜单 */
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10;
overflow: hidden;
animation: dropdownSlide 0.2s ease-out;
}
@keyframes dropdownSlide {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-item {
padding: 12px 16px;
border-bottom: 1px solid #F0F0F0;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-text {
font-size: 14px;
color: #333333;
display: block;
}
.sort-dropdown {
min-width: 120px;
}
.filter-dropdown {
min-width: 120px;
left: auto;
right: 0;
}
.quick-filters {
white-space: nowrap;
/* 隐藏滚动条 - 兼容不同平台 */
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
/* 隐藏滚动条 - Webkit浏览器 */
.quick-filters ::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
background: transparent;
}
.quick-filter-list {
display: inline-flex;
gap: 8px;
}
.quick-filter-item {
padding: 4px 10px;
background: #F5F5F5;
border-radius: 4px;
flex-shrink: 0;
font-size: 12px;
}
.quick-filter-item.active {
background: #FFFFFF;
border: 1px solid #FF6B00;
position: relative;
}
.quick-filter-item.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: #FF6B00;
}
.filter-text {
font-size: 12px;
color: #333333;
}
.quick-filter-item.active .filter-text {
color: #FF6B00;
}
/* 商家列表 */
.shop-list {
padding: 0 10px;
}
.shop-card {
background: #FFFFFF;
border: 1px solid #E0E0E0;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
display: flex;
gap: 12px;
position: relative;
}
/* 商家图片 */
.shop-image {
width: 100px;
height: 100px;
border-radius: 6px;
flex-shrink: 0;
background: #F5F5F5;
}
/* 商家信息 */
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* 店名 */
.shop-header {
margin-bottom: 6px;
padding-right: 60px;
}
.shop-name {
font-size: 16px;
font-weight: 600;
color: #333333;
word-wrap: break-word;
word-break: break-all;
line-height: 1.4;
}
/* 左侧信息:评分、类型、销量 */
.shop-info-left {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.shop-rating {
display: flex;
align-items: baseline;
}
.rating-score {
font-size: 14px;
font-weight: 600;
color: #FF9800;
}
.rating-unit {
font-size: 10px;
color: #FF9800;
margin-left: 2px;
}
.shop-info-left .shop-type {
font-size: 12px;
color: #4CAF50;
font-weight: 500;
}
.shop-info-left .shop-sales {
font-size: 12px;
color: #666666;
}
/* 距离、用时、人均价格 - 同一行 */
.shop-info-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
.distance-wrapper {
display: flex;
align-items: center;
gap: 4px;
}
.location-icon {
font-size: 12px;
color: #999999;
}
.distance-text {
font-size: 12px;
color: #666666;
}
.time-text {
font-size: 12px;
color: #666666;
margin-left: 8px;
}
/* 人均价格 - 小号内联样式 */
.shop-avg-price-inline {
display: inline-flex;
align-items: baseline;
}
.avg-price-small {
font-size: 11px;
color: #FF6B00;
font-weight: 600;
}
.avg-price-value-small {
font-size: 15px;
color: #FF6B00;
font-weight: 700;
line-height: 1;
}
.avg-price-text-small {
font-size: 11px;
color: #FF6B00;
margin-left: 2px;
}
/* 底部标签 */
.shop-bottom-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* 分类页头部骨架屏样式 */
.page-loading-wrapper {
min-height: 100vh;
background: #F5F5F5;
}
.skeleton-nav-back {
width: 48rpx;
height: 48rpx;
border-radius: 8rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
}
.skeleton-nav-title {
flex: 1;
height: 36rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.3) 25%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0.3) 75%);
background-size: 200% 100%;
animation: loading-white 1.5s ease-in-out infinite;
margin-left: 16rpx;
}
.search-box-skeleton {
background: #F5F5F5;
border-radius: 10px;
height: 40px;
width: 100%;
}
.skeleton-tabs-list {
display: inline-flex;
padding: 0 16px;
gap: 12px;
}
.skeleton-tab-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.skeleton-tab-icon {
width: 48px;
height: 48px;
border-radius: 8rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
.skeleton-tab-text {
width: 60rpx;
height: 24rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
.skeleton-filter-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.skeleton-filter-sort,
.skeleton-filter-all {
height: 32rpx;
width: 140rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
.skeleton-quick-filters {
display: inline-flex;
gap: 8px;
}
.skeleton-quick-filter-item {
padding: 4px 10px;
background: #F5F5F5;
border-radius: 4px;
width: 100rpx;
height: 32rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes loading-white {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,55 @@
@mixin radius($r,$d:null ,$important: false){
$radius-value:map-get($uni-radius, $r) if($important, !important, null);
// Key exists within the $uni-radius variable
@if (map-has-key($uni-radius, $r) and $d){
@if $d == t {
border-top-left-radius:$radius-value;
border-top-right-radius:$radius-value;
}@else if $d == r {
border-top-right-radius:$radius-value;
border-bottom-right-radius:$radius-value;
}@else if $d == b {
border-bottom-left-radius:$radius-value;
border-bottom-right-radius:$radius-value;
}@else if $d == l {
border-top-left-radius:$radius-value;
border-bottom-left-radius:$radius-value;
}@else if $d == tl {
border-top-left-radius:$radius-value;
}@else if $d == tr {
border-top-right-radius:$radius-value;
}@else if $d == br {
border-bottom-right-radius:$radius-value;
}@else if $d == bl {
border-bottom-left-radius:$radius-value;
}
}@else{
border-radius:$radius-value;
}
}
@each $key, $child in $uni-radius {
@if($key){
.uni-radius-#{"" + $key} {
@include radius($key)
}
}@else{
.uni-radius {
@include radius($key)
}
}
}
@each $direction in t, r, b, l,tl, tr, br, bl {
@each $key, $child in $uni-radius {
@if($key){
.uni-radius-#{"" + $direction}-#{"" + $key} {
@include radius($key,$direction,false)
}
}@else{
.uni-radius-#{$direction} {
@include radius($key,$direction,false)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,173 @@
<template>
<view class="">
<view class="" style="padding: 0rpx 30rpx; ">
<up-form labelPosition="top" :model="userData" labelWidth='200rpx' ref="form1">
<up-form-item label="姓名" prop="userInfo.name" ref="item1">
<up-input v-model="userData.name" prefixIcon="account" placeholder="请输入姓名"
:customStyle="{padding: '20rpx','background-color':'#F8F9FA','margin-top':'15rpx','border-radius':'16rpx'}"
:prefixIconStyle="{fontSize:'50rpx' }" border="none"></up-input>
</up-form-item>
<up-form-item label="手机号" prop="userInfo.name" ref="item1">
<up-input v-model="userData.phone"
:customStyle="{padding: '20rpx','background-color':'#F8F9FA','margin-top':'15rpx','border-radius':'16rpx'}"
prefix-icon="phone" type='number' placeholder="请输入手机号" :prefixIconStyle="{fontSize:'45rpx'}"
border="none"></up-input>
</up-form-item>
<up-form-item label="所在地区" prop="userInfo.name" ref="item1">
<view class="" @click="location()"
style=" width: 100%; display: flex; align-items: center; justify-content: space-between; border-radius: 16rpx; margin-top: 15rpx; background-color: #F8F9FA;padding: 20rpx;">
<view class="" style="display: flex;align-items: center;">
<up-icon name="map" color="#606266" size="22"></up-icon>
<view v-if="!userData.province" class="" style="color: #c0c4cc; margin-left: 10rpx; ">
请选择地址
</view>
<view v-else class="" style=" margin-left: 10rpx; ">
{{ userData.primaryAddress }}
</view>
</view>
<view class="">
<up-icon name="arrow-right" color="#606266" size="20"></up-icon>
</view>
</view>
</up-form-item>
<up-form-item label="详细地址" prop="userInfo.name" ref="item1">
<up-textarea v-model="userData.address" style=" margin-top: 15rpx; background-color: #F8F9FA;"
placeholder="请输入内容"></up-textarea>
</up-form-item>
</up-form>
<view class=""
style=" margin-top: 15rpx; display: flex; align-items: center; justify-content: space-between;">
<view class="">
设为默认地址
</view>
<view class="">
<up-switch v-model="userData.isDefault" size='18' activeColor='var(--nav-mian)'
@change="switchChange"></up-switch>
</view>
</view>
</view>
<!-- 新增地址按钮 -->
<view class=""
style="position: fixed; bottom: 0; width: 100%; padding: 20rpx; border-top: 1rpx solid #e2e2e2; background-color: #fff; ">
<up-button @click="save()" color='var(--nav-mian)' shape='circle' style="width: 90%; margin: 15rpx auto 0; "
:text="userData.addressId?' 修改地址':'保存地址'"></up-button>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { vpAddressService, Service } from '@/Service/vp/vpAddressService'
import { onLoad } from '@dcloudio/uni-app';
let userData = ref({
addressId: '',
phone: '',
name: '',
address: '',
isDefault: false,
province: '',
city: '',
region: '',
primaryAddress: ''
})
onLoad((data : any) => {
if (data.addressId) {
userData.value.addressId = data.addressId
uni.setNavigationBarTitle({
title: '修改地址'
})
getData()
}
})
const getData = () => {
vpAddressService.GetUserAddressInfo(userData.value.addressId).then(res => {
if (res.code == 0) {
userData.value = res.data.addressInfo
userData.value.isDefault = res.data.addressInfo.isDefault == 1 ? true : false
userData.value.primaryAddress = res.data.addressInfo.province + res.data.addressInfo.city + res.data.addressInfo.region
} else {
Service.Msg(res.msg)
}
})
}
const location = () => {
uni.getLocation({
type: 'wgs84',
success: function (res) {
uni.chooseLocation({
latitude: res.latitude,
longitude: res.longitude,
success: function (res) {
console.log(res);
userData.value.province = res.address.split('省')[0] + '省'
userData.value.city = res.address.split('省')[1].split('市')[0] + '市'
userData.value.region = res.address.split('省')[1].split('市')[1].split('区')[0] + '区'
userData.value.primaryAddress = userData.value.province + userData.value.city + userData.value.region
userData.value.address = res.name
}
});
}
});
}
const switchChange = () => {
}
const save = () => {
if (userData.value.name == '') {
Service.Msg('请输入姓名')
return
}
if (userData.value.phone == '') {
Service.Msg('请输入电话号')
return
}
if (userData.value.province == '') {
Service.Msg('请选择地址')
return
}
if (userData.value.address == '') {
Service.Msg('请输入详细地址')
return
}
let obj = {
addressId: userData.value.addressId,
name: userData.value.name,
phone: userData.value.phone,
province: userData.value.province,
city: userData.value.city,
region: userData.value.region,
address: userData.value.address,
isDefault: userData.value.isDefault ? 1 : 0
}
vpAddressService.UpdateAddress(obj).then(res => {
if (res.code == 0) {
Service.Msg( userData.value.addressId?'修改成功!': '添加成功!')
setTimeout(() => {
Service.GoPageBack()
}, 1000)
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss">
page {
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<view style=" margin: 20rpx; padding: 20rpx;">
<view @click="uploadFImg()" class=""
style=" display: flex; flex-direction: column; justify-content: center; align-items: center; ">
<img v-if="userInfo.headImg!=''" :src="Service.GetMateUrlByImg(userInfo.headImg)" style="width: 140rpx; height: 140rpx; border-radius: 50%; " alt="" />
<view v-else class=""
style="background-color: #EBEBEB; width: 140rpx; height: 140rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; ">
<img :src="Service.GetIconImg('/static/userFunc/photo.png')" style="width: 50rpx; height: 50rpx; "
alt="" />
</view>
<view class="" style="margin-top: 15rpx; font-size: 26rpx; color: #999999; ">
点击更换头像
</view>
</view>
<view class="" style=" margin-top: 30rpx; ">
<up-form labelWidth='90' labelPosition="left" :model="userInfo" ref="form1">
<up-form-item label="昵称" prop="userInfo.name" :borderBottom="true" ref="item1">
<up-input inputAlign='right' v-model="userInfo.nick" border="none"></up-input>
</up-form-item>
<up-form-item label="性别" prop="userInfo.sex" :borderBottom="true">
<view class="" style="position: relative;" >
<view class="" style=" position: absolute; top: -8rpx; right: 0; ">
<up-radio-group v-model="userInfo.sex" placement="row">
<up-radio v-for="(item, index) in radiolist1" activeColor='#FF6A00' :key="index"
iconPlacement="right" :label="item.name" :name="item.name">
</up-radio>
</up-radio-group>
</view>
</view>
</up-form-item>
<up-form-item label="生日" prop="userInfo.sex" :borderBottom="true" style="position: relative;" >
<view @click="showDate=true" class="" style=" position: absolute; top: 25rpx; right: 0; display: flex; align-items: center; ">
{{Service.formatDate(userInfo.date,2)}}
<u-icon name="arrow-right" size="24rpx" color="#000"></u-icon>
</view>
</up-form-item>
<up-form-item label="手机号" prop="userInfo.sex" :borderBottom="true">
<up-input inputAlign='right' v-model="userInfo.phone" border="none"></up-input>
</up-form-item>
<up-form-item label="邮箱" prop="userInfo.sex" :borderBottom="true">
<up-input inputAlign='right' v-model="userInfo.age" border="none"></up-input>
</up-form-item>
</up-form>
</view>
<up-datetime-picker :show="showDate" @cancel="showDate=!showDate" @confirm="dateConfirm" v-model="userInfo.date" mode="date"></up-datetime-picker>
<view class="" style="width: 100%; height: 200rpx;">
</view>
<view class="" style=" width: 100% ; background-color: #fff; position: fixed; bottom: 15rpx; left: 0; ">
<view class=""
style=" margin: 0 20rpx; padding: 20rpx 0; color: #fff; display: flex; align-items: center; justify-content: center; border-radius: 10rpx; background-color: #FF6A00;">
保存信息
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from '@/Service/Service';
import { ref } from "vue";
let showDate=ref(false)
const userInfo = ref({
headImg: '',
age: '1',
sex: '',
phone: '1',
date:Date.now(),
nick: '大大怪将军'
})
const radiolist1 = ref([
{
name: '男',
disabled: false,
},
{
name: '女',
disabled: false,
}
]);
onLoad(() => {
});
onShow(() => {
});
const dateConfirm=(e)=>{
showDate.value=!showDate.value
userInfo.value.date=e.value
}
const uploadFImg = () => {
uni.chooseImage({
count: 1, // 最多选择3张图片
sizeType: ['original', 'compressed'], // 支持原图和压缩图
sourceType: ['album', 'camera'], // 可从相册选择或使用相机拍照
success: function (res) {
let path = res.tempFiles[0].path
userInfo.value.headImg=path
// Service.uploadH5(path, 'Avatar', data => {
// userInfo.value.headImg = data.split(',')[2].split(':')[1].split('"')[1]
// })
},
fail: function (err) {
console.error('选择失败:', err.errMsg);
}
})
}
</script>
<style lang="scss">
page {
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,24 @@
@mixin get-styles($k,$c) {
@if $k == size or $k == weight{
font-#{$k}:#{$c}
}@else{
#{$k}:#{$c}
}
}
@each $key, $child in $uni-headings {
/* #ifndef APP-NVUE */
.uni-#{$key} {
@each $k, $c in $child {
@include get-styles($k,$c)
}
}
/* #endif */
/* #ifdef APP-NVUE */
.container .uni-#{$key} {
@each $k, $c in $child {
@include get-styles($k,$c)
}
}
/* #endif */
}

View File

@@ -0,0 +1,311 @@
<template>
<view class="favorites-page">
<!-- 沉浸式状态栏 -->
<view class="status-bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<image class="back-icon" src="/static/icons/back.svg" @click="goBack" mode="aspectFit" />
<text class="nav-title">我的收藏</text>
<view class="nav-placeholder"></view>
</view>
<!-- 收藏列表 -->
<view class="content">
<view v-if="collectList.length > 0" class="favorites-list">
<view v-for="item in collectList" :key="item.merchId" class="shop-card" @click="Service.GoPage('/pages/community/merchantDetail?merchId='+item.merchId)">
<!-- 店铺图片 -->
<image class="shop-image" :src="Service.GetMateUrlByImg(item.logo)" mode="aspectFill" />
<!-- 店铺信息 -->
<view class="shop-info">
<text class="shop-name">{{ item.name }}</text>
<view class="shop-meta">
<text class="ri-star-fill rating-icon"></text>
<text class="rating-text">{{ Number(item.score ).toFixed(1)}}分</text>
<text class="sales-text">月售{{ item.sale }}</text>
</view>
<view class="shop-tags">
<view v-for="(coupon, idx) in item.tips" :key="idx" :class="getTagClass(coupon)" style="font-size: 24rpx;"
class="shop-tag">
{{ coupon }}
</view>
</view>
</view>
<!-- 取消收藏按钮 -->
<view class="cancel-btn" @click.stop="cancelFavorite(item)">
<up-icon name="heart-fill" color="#fc5151" :bold="true" size="18"></up-icon>
</view>
</view>
<up-loadmore :status="status" />
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="ri-heart-line empty-icon"></text>
<text class="empty-text">暂无收藏店铺</text>
<text class="empty-desc">去首页逛逛,收藏喜欢的店铺吧</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import {onShow,onLoad,onReachBottom} from "@dcloudio/uni-app";
import {Service} from "@/Service/Service"
import { ref, computed } from "vue";
import { vpDiscountService } from "@/Service/vp/vpDiscountService"
import { vpUserService } from "@/Service/vp/vpUserService"
let collectList = ref<Array<any>>([])
let status = ref<string>('loadmore')
let pageNo = ref<number>(1)
let longitude=ref(0)
let latitude=ref(0)
onLoad(()=>{
getLocation()
})
onReachBottom(()=>{
getList()
})
// 返回
const goBack = () => {
Service.GoPageBack()
}
const getLocation = () => {
uni.getLocation({
type: 'wgs84',
success: function (res) {
longitude.value = res.longitude
latitude.value = res.latitude
getData()
},
fail: function (e) {
console.log(e);
}
});
}
const getData=()=>{
pageNo.value=1
status.value='loadmore'
collectList.value=[]
getList()
}
const getList=()=>{
if (status.value !== 'loadmore') {
return
}
status.value = 'loading'
vpUserService.GetUserCollectList(longitude.value,latitude.value,pageNo.value).then(res=>{
if(res.code==0){
collectList.value=[...collectList.value,...res.data.collectList]
status.value=res.data.collectList.length==10?'loadmore':'nomore'
pageNo.value++
}
})
}
// 根据标签文本获取样式类
const getTagClass = (tagText : string) => {
const tagMap = {
'可用积分': 'tag-points-available',
'可用券': 'tag-coupon'
}
return tagMap[tagText] || 'tag-points'
}
// 取消收藏
const cancelFavorite = (item:any) => {
uni.showModal({
title: '取消收藏',
content: `确定取消收藏吗?`,
success: (res) => {
if (res.confirm) {
vpUserService.CollectMerch(item.merchId).then(res=>{
if(res.code==0){
getData()
}else{
Service.Msg(res.msg)
}
})
}
}
})
}
</script>
<style lang="scss" scoped>
.favorites-page {
min-height: 100vh;
background: #F5F5F5;
}
.action-pill-icon {
font-size: 28rpx;
color: #FF6B00;
line-height: 1;
}
/* 状态栏 */
.status-bar {
background: linear-gradient(135deg, #FF6B00, #FF9500);
height: var(--status-bar-height);
width: 100%;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 60rpx 24rpx 20rpx 24rpx;
background: linear-gradient(135deg, #FF6B00, #FF9500);
}
.back-icon {
width: 48rpx;
height: 48rpx;
padding: 8rpx;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #FFFFFF;
}
.nav-placeholder {
width: 48rpx;
}
/* 内容区域 */
.content {
padding: 20rpx;
}
/* 收藏列表 */
.favorites-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.shop-card {
background: #FFFFFF;
border-radius: 16rpx;
padding: 20rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
position: relative;
}
.shop-image {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.shop-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.shop-name {
font-size: 32rpx;
font-weight: 600;
color: #222222;
}
.shop-meta {
display: flex;
align-items: center;
gap: 8rpx;
}
.rating-icon {
font-size: 24rpx;
color: #FFB800;
}
.rating-text {
font-size: 24rpx;
color: #FF6B00;
font-weight: 600;
}
.sales-text {
font-size: 22rpx;
color: #999999;
}
.shop-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 20rpx;
color: #666666;
background: #F5F5F5;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.cancel-btn {
width: 64rpx;
height: 64rpx;
background: #FFEBEE;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 16rpx;
flex-shrink: 0;
}
.cancel-icon {
font-size: 32rpx;
color: #F44336;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
gap: 16rpx;
}
.empty-icon {
font-size: 120rpx;
color: #CCCCCC;
}
.empty-text {
font-size: 28rpx;
color: #999999;
font-weight: 500;
}
.empty-desc {
font-size: 24rpx;
color: #CCCCCC;
}
</style>

Some files were not shown because too many files have changed in this diff Show More