first commit
This commit is contained in:
332
src/components/AgentApplicationForm.vue
Normal file
332
src/components/AgentApplicationForm.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<wd-popup v-model="popupVisible" position="bottom" close-on-click-modal @close="handleClose">
|
||||
<view class="bg-white rounded-t-lg p-4">
|
||||
<view class="flex justify-between items-center mb-4">
|
||||
<text class="text-gray-400" @click="cancel">取消</text>
|
||||
<text class="text-lg font-medium">申请成为代理</text>
|
||||
<text class="text-gray-400" @click="cancel">关闭</text>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
|
||||
</view>
|
||||
<wd-form :model="form" :rules="rules">
|
||||
<wd-cell-group border>
|
||||
<!-- 区域选择 -->
|
||||
<wd-col-picker v-model="region" label="代理区域" label-width="100px" prop="region" placeholder="请选择代理区域"
|
||||
:columns="columns" :column-change="handleColumnChange" @confirm="handleRegionConfirm"
|
||||
:display-format="displayFormat" />
|
||||
<!-- 手机号 -->
|
||||
<wd-input label="手机号码" label-width="100px" type="number" v-model="form.mobile" prop="mobile"
|
||||
placeholder="请输入您的手机号" :disabled="true" readonly :rules="[
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ required: true, pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
|
||||
]" />
|
||||
|
||||
<!-- 验证码 -->
|
||||
<wd-input label="验证码" label-width="100px" type="number" v-model="form.code" prop="code" placeholder="请输入验证码"
|
||||
:rules="[{ required: true, message: '请输入验证码' }]" use-suffix-slot>
|
||||
<template #suffix>
|
||||
<button class="verify-btn" :disabled="countdown > 0" @click.stop="sendVerifyCode">
|
||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||
</button>
|
||||
</template>
|
||||
</wd-input>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 同意条款的复选框 -->
|
||||
<view class="p-4">
|
||||
<view class="flex items-start">
|
||||
<wd-checkbox v-model="isAgreed" size="16px" class="flex-shrink-0 mr-2"></wd-checkbox>
|
||||
<view class="text-xs text-gray-400 leading-tight">
|
||||
我已阅读并同意
|
||||
<text class="text-blue-400" @click.stop="toUserAgreement">《用户协议》</text>
|
||||
<text class="text-blue-400" @click.stop="toServiceAgreement">《信息技术服务合同》</text>
|
||||
<text class="text-blue-400" @click.stop="toAgentManageAgreement">《推广方管理制度协议》</text>
|
||||
<view class="text-xs text-gray-400 mt-1">点击勾选即代表您同意上述法律文书的相关条款并签署上述法律文书</view>
|
||||
<view class="text-xs text-gray-400 mt-1">手机号未在本平台注册账号则申请后将自动生成账号</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="p-4">
|
||||
<wd-button type="primary" block @click="submitForm">提交申请</wd-button>
|
||||
<wd-button type="default" block class="mt-2" @click="cancel">取消</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</wd-popup>
|
||||
<wd-toast />
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onUnmounted, onMounted } from 'vue'
|
||||
import { useColPickerData } from '../hooks/useColPickerData'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getCode } from '@/api/apis.js' // 导入getCode API
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ancestor: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const region = ref([]) // 存储地区代码数组
|
||||
const regionText = ref('') // 存储地区文本
|
||||
const isAgreed = ref(false) // 用户是否同意协议
|
||||
|
||||
// 格式化显示文本
|
||||
const displayFormat = (selectedItems) => {
|
||||
if (selectedItems.length === 0) return ''
|
||||
return selectedItems.map(item => item.label).join(' ')
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:show', 'submit', 'close'])
|
||||
const form = ref({
|
||||
mobile: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
// 使用wot-design-ui的toast组件
|
||||
const toast = useToast()
|
||||
|
||||
// 不再需要监听region变化触发表单验证
|
||||
// watch(region, (newVal) => {
|
||||
// // 当region变化时,触发表单验证
|
||||
// if (formRef.value) {
|
||||
// formRef.value.validate(['region'])
|
||||
// }
|
||||
// })
|
||||
|
||||
const popupVisible = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
|
||||
// 初始化省市区数据
|
||||
const { colPickerData, findChildrenByCode } = useColPickerData()
|
||||
const columns = ref([
|
||||
colPickerData.map((item) => {
|
||||
return {
|
||||
value: item.value,
|
||||
label: item.text
|
||||
}
|
||||
})
|
||||
])
|
||||
|
||||
// 表单验证规则 - 仍然保留以供wd-form组件使用
|
||||
const rules = {
|
||||
region: [{
|
||||
required: true,
|
||||
message: '请选择代理区域',
|
||||
validator: (value) => {
|
||||
// 这里直接检查region.value而不是传入的value
|
||||
if (Array.isArray(region.value) && region.value.length === 3) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.reject('请选择完整的省市区')
|
||||
}
|
||||
}],
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ required: true, pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
|
||||
],
|
||||
code: [{ required: true, message: '请输入验证码' }],
|
||||
}
|
||||
|
||||
// 协议跳转函数
|
||||
const toUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/agreement?type=user'
|
||||
})
|
||||
}
|
||||
|
||||
const toServiceAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/agreement?type=service'
|
||||
})
|
||||
}
|
||||
|
||||
const toAgentManageAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/agreement?type=manage'
|
||||
})
|
||||
}
|
||||
|
||||
// 监听show属性变化
|
||||
watch(() => props.show, (newVal) => {
|
||||
popupVisible.value = newVal
|
||||
})
|
||||
|
||||
// 监听popupVisible同步回props.show
|
||||
watch(() => popupVisible.value, (newVal) => {
|
||||
emit('update:show', newVal)
|
||||
})
|
||||
|
||||
// 组件挂载时获取用户信息并填入手机号
|
||||
onMounted(() => {
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
if (userInfo && userInfo.mobile) {
|
||||
form.value.mobile = userInfo.mobile
|
||||
}
|
||||
})
|
||||
|
||||
// Popup关闭事件
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 取消按钮
|
||||
const cancel = () => {
|
||||
popupVisible.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 处理列变化
|
||||
const handleColumnChange = ({ selectedItem, resolve, finish }) => {
|
||||
const children = findChildrenByCode(colPickerData, selectedItem.value)
|
||||
if (children && children.length) {
|
||||
resolve(children.map(item => ({
|
||||
label: item.text,
|
||||
value: item.value
|
||||
})))
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// 区域选择确认 - 获取选中项的文本值
|
||||
const handleRegionConfirm = ({ value, selectedItems }) => {
|
||||
// 存储地区文本
|
||||
regionText.value = selectedItems.map(item => item.label).join('-')
|
||||
}
|
||||
|
||||
// 判断手机号是否有效
|
||||
const isPhoneNumberValid = computed(() => {
|
||||
return /^1[3-9]\d{9}$/.test(form.value.mobile)
|
||||
})
|
||||
|
||||
// 发送验证码
|
||||
const sendVerifyCode = async () => {
|
||||
// 验证手机号
|
||||
if (!form.value.mobile) {
|
||||
toast.info('请输入手机号')
|
||||
return
|
||||
}
|
||||
if (!isPhoneNumberValid.value) {
|
||||
toast.info('手机号格式不正确')
|
||||
return
|
||||
}
|
||||
|
||||
// 发送验证码请求
|
||||
getCode({
|
||||
mobile: form.value.mobile,
|
||||
actionType: 'agentApply',
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
toast.success('验证码已发送')
|
||||
// 开始倒计时
|
||||
startCountdown()
|
||||
} else {
|
||||
toast.error(res.msg || '发送失败')
|
||||
}
|
||||
}).catch((err) => {
|
||||
toast.error('网络错误')
|
||||
})
|
||||
}
|
||||
|
||||
// 开始倒计时
|
||||
const startCountdown = () => {
|
||||
countdown.value = 60
|
||||
if (timer) clearInterval(timer)
|
||||
|
||||
timer = setInterval(() => {
|
||||
countdown.value--
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 组件卸载时清除计时器
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
|
||||
// 提交表单 - 不再使用formRef验证
|
||||
const submitForm = () => {
|
||||
try {
|
||||
// 检查区域是否已选择
|
||||
if (!Array.isArray(region.value) || region.value.length < 3) {
|
||||
toast.info('请选择完整的代理区域')
|
||||
return
|
||||
}
|
||||
|
||||
if (!regionText.value) {
|
||||
toast.info('请选择代理区域')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查手机号
|
||||
if (!form.value.mobile) {
|
||||
toast.info('请输入手机号')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isPhoneNumberValid.value) {
|
||||
toast.info('手机号格式不正确')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证码
|
||||
if (!form.value.code) {
|
||||
toast.info('请输入验证码')
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户是否同意协议
|
||||
if (!isAgreed.value) {
|
||||
toast.info('请阅读并同意相关协议')
|
||||
return
|
||||
}
|
||||
|
||||
// 所有验证通过,构建表单数据
|
||||
const formData = {
|
||||
...form.value,
|
||||
region: regionText.value,
|
||||
}
|
||||
|
||||
// 提交完整数据
|
||||
emit('submit', formData)
|
||||
} catch {
|
||||
toast.error('系统错误,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.verify-btn {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.verify-btn[disabled] {
|
||||
background-color: #a0aec0;
|
||||
}
|
||||
</style>
|
||||
12
src/components/EmptyState.vue
Normal file
12
src/components/EmptyState.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<view class="flex flex-col items-center justify-center py-16">
|
||||
<image src="/static/image/empty.svg" mode="aspectFit" class="w-48 h-48 mb-4" />
|
||||
<text class="text-gray-400 text-base">{{ text }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
text: { type: String, default: '暂无数据' }
|
||||
})
|
||||
</script>
|
||||
209
src/components/GzhQrcode.vue
Normal file
209
src/components/GzhQrcode.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'example', // 'example' | 'withdraw'
|
||||
validator: (value) => ['example', 'withdraw'].includes(value)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
// 根据类型计算显示内容
|
||||
const modalConfig = computed(() => {
|
||||
if (props.type === 'withdraw') {
|
||||
return {
|
||||
title: '提现说明',
|
||||
highlight: '点击二维码全屏查看',
|
||||
instruction: '全屏后长按识别关注公众号',
|
||||
description: '提现功能请在公众号内操作'
|
||||
}
|
||||
}
|
||||
|
||||
// 默认是查看示例报告
|
||||
return {
|
||||
title: '关注公众号',
|
||||
highlight: '点击二维码全屏查看',
|
||||
instruction: '全屏后长按识别关注公众号',
|
||||
description: '关注公众号查看示例报告'
|
||||
}
|
||||
})
|
||||
|
||||
function handleClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleMaskClick() {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
function previewImage() {
|
||||
uni.previewImage({
|
||||
urls: ['/static/qrcode/fwhqrcode.jpg'],
|
||||
current: '/static/qrcode/fwhqrcode.jpg'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view v-if="visible" class="gzh-qrcode-modal">
|
||||
<view class="modal-mask" @click="handleMaskClick" />
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<view class="modal-title">{{ modalConfig.title }}</view>
|
||||
<view class="close-btn" @click="handleClose">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="qrcode-container">
|
||||
<image class="qrcode-image" src="/static/qrcode/fwhqrcode.jpg" mode="aspectFit" @click="previewImage" />
|
||||
</view>
|
||||
|
||||
<view class="modal-message">
|
||||
<text class="highlight">{{ modalConfig.highlight }}</text>
|
||||
<text class="instruction">{{ modalConfig.instruction }}</text>
|
||||
<text class="description">{{ modalConfig.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gzh-qrcode-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
width: 300px;
|
||||
max-width: 85%;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qrcode-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
padding: 0 20px 24px;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-message text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #007aff !important;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 动画效果 */
|
||||
.gzh-qrcode-modal {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
180
src/components/Payment.vue
Normal file
180
src/components/Payment.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<wd-popup v-model="show" position="bottom" z-index="3000" custom-style="height: 50%; display: flex; flex-direction: column; justify-content: space-between; padding: 24px;">
|
||||
<view class="text-center">
|
||||
<text class="text-lg font-bold block">支付</text>
|
||||
</view>
|
||||
<view class="text-center">
|
||||
<text class="font-bold text-xl block">{{ data.product_name }}</text>
|
||||
<view class="text-3xl text-red-500 font-bold">
|
||||
<!-- 显示原价和折扣价格 -->
|
||||
<view v-if="discountPrice" class="line-through text-gray-500 mt-4" :class="{ 'text-2xl': discountPrice }">
|
||||
¥ {{ data.sell_price }}
|
||||
</view>
|
||||
<view>¥ {{ discountPrice ? (data.sell_price * 0.2).toFixed(2) : data.sell_price }}</view>
|
||||
</view>
|
||||
<!-- 仅在折扣时显示活动说明 -->
|
||||
<text v-if="discountPrice" class="text-sm text-red-500 mt-1 block">活动价:2折优惠</text>
|
||||
</view>
|
||||
<!-- 支付方式选择 -->
|
||||
<view class="">
|
||||
<wd-cell-group>
|
||||
<wd-cell clickable>
|
||||
<template #title>
|
||||
<text class="text-lg font-medium">微信支付</text>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<view class="w-5 h-5 rounded-full border-2 border-green-500 bg-green-500 flex items-center justify-center">
|
||||
<view class="w-2 h-2 bg-white rounded-full"></view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
<!-- 确认按钮 -->
|
||||
<view class="">
|
||||
<view @click="getPayment" class="w-full py-4 bg-green-500 text-center rounded-full shadow-lg active:bg-green-600 transition-colors">
|
||||
<text class="text-white text-lg font-medium">确认支付</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { payment } from '@/api/apis.js'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const show = defineModel()
|
||||
const orderNo = ref('')
|
||||
const discountPrice = ref(false) // 是否应用折扣
|
||||
|
||||
async function getPayment() {
|
||||
if (!props.id) {
|
||||
toast.error('订单信息异常,请重试')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await payment({
|
||||
id: String(props.id),
|
||||
pay_method: 'wechat',
|
||||
pay_type: props.type,
|
||||
})
|
||||
|
||||
if (res.data) {
|
||||
orderNo.value = res.data.order_no
|
||||
|
||||
// 微信支付 - 兼容多种返回格式(wechatpay-go 返回 appId/timeStamp/nonceStr/package/signType/paySign)
|
||||
const paymentData = res.data.prepay_data || res.data.prepayData || res.data
|
||||
// 若 prepay_data 是字符串则解析
|
||||
const data = typeof paymentData === 'string' ? (() => { try { return JSON.parse(paymentData) } catch { return {} } })() : (paymentData || {})
|
||||
|
||||
const timeStamp = data.timeStamp || data.timestamp || data.time_stamp
|
||||
const nonceStr = data.nonceStr || data.noncestr || data.nonce_str
|
||||
const packageVal = data.package
|
||||
const signType = data.signType || data.sign_type || 'MD5'
|
||||
const paySign = data.paySign || data.pay_sign || data.sign
|
||||
|
||||
// 校验必要参数
|
||||
if (!timeStamp || !nonceStr || !packageVal || !paySign) {
|
||||
toast.error('支付参数异常,请联系客服')
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.requestPayment({
|
||||
provider: 'wxpay',
|
||||
timeStamp: String(timeStamp),
|
||||
nonceStr: String(nonceStr),
|
||||
package: String(packageVal),
|
||||
signType: String(signType),
|
||||
paySign: String(paySign),
|
||||
success: (result) => {
|
||||
toast.success('支付成功')
|
||||
show.value = false
|
||||
handlePaymentSuccess()
|
||||
},
|
||||
fail: (error) => {
|
||||
toast.error(error.errMsg || '支付失败')
|
||||
// 用户取消不关闭弹窗,方便重试
|
||||
if (error.errMsg && !error.errMsg.includes('cancel')) {
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
uni.requestPayment({
|
||||
provider: 'wxpay',
|
||||
orderInfo: {
|
||||
appid: paymentData.appid,
|
||||
noncestr: nonceStr,
|
||||
package: packageVal,
|
||||
partnerid: paymentData.partnerid,
|
||||
prepayid: paymentData.prepayid,
|
||||
timestamp: timeStamp,
|
||||
sign: paySign
|
||||
},
|
||||
success: (result) => {
|
||||
toast.success('支付成功')
|
||||
show.value = false
|
||||
handlePaymentSuccess()
|
||||
},
|
||||
fail: (error) => {
|
||||
toast.error('支付失败')
|
||||
show.value = false
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
if (typeof WeixinJSBridge !== 'undefined') {
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', paymentData, function (result) {
|
||||
if (result.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
toast.success('支付成功')
|
||||
show.value = false
|
||||
handlePaymentSuccess()
|
||||
} else {
|
||||
toast.error('支付失败')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error('当前环境不支持微信支付')
|
||||
}
|
||||
// #endif
|
||||
} else {
|
||||
toast.error(res.msg || '获取支付信息失败')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error?.data?.msg || error?.msg || '支付请求失败')
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaymentSuccess() {
|
||||
// 支付成功后的处理
|
||||
setTimeout(() => {
|
||||
uni.switchTab({
|
||||
url: `/pages/me`
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
175
src/components/PriceInputPopup.vue
Normal file
175
src/components/PriceInputPopup.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<wd-popup v-model="show" position="bottom" round close-on-click-modal destroy-on-close>
|
||||
<div class="min-h-[500px] bg-gray-50 text-gray-600">
|
||||
<div class="h-10 bg-white flex items-center justify-center font-semibold text-lg">设置客户查询价
|
||||
</div>
|
||||
<div class="card m-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-lg">
|
||||
客户查询价 (元)</div>
|
||||
</div>
|
||||
|
||||
<div class="border-b border-gray-200">
|
||||
<wd-input v-model="price" type="number" label="¥" label-width="28px" size="large"
|
||||
:placeholder="`${productConfig.price_range_min} - ${productConfig.price_range_max}`"
|
||||
@blur="onBlurPrice" custom-class="wd-input" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span>元</div>
|
||||
<div>我的成本为<span class="text-orange-500"> {{ costPrice }} </span>元</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card m-4">
|
||||
<div class="text-lg mb-2">收益与成本说明</div>
|
||||
|
||||
<div>推广收益 = 客户查询价 - 我的成本</div>
|
||||
<div>我的成本 = 提价成本 + 底价成本</div>
|
||||
<div class="mt-1">提价成本:超过平台标准定价部分,平台会收取部分成本价</div>
|
||||
<div class="">设定范围:<span class="text-orange-500">{{
|
||||
productConfig.price_range_min }}</span>元 - <span class="text-orange-500">{{
|
||||
productConfig.price_range_max }}</span>元</div>
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<wd-button class="w-full" round type="primary" size="large" @click="onConfirm">确认</wd-button>
|
||||
</div>
|
||||
</div>
|
||||
</wd-popup>
|
||||
<wd-toast />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, toRefs } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
const props = defineProps({
|
||||
defaultPrice: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
productConfig: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const { defaultPrice, productConfig } = toRefs(props)
|
||||
const emit = defineEmits(["change"])
|
||||
const show = defineModel("show")
|
||||
const price = ref(null)
|
||||
const toast = useToast()
|
||||
|
||||
watch(show, () => {
|
||||
price.value = defaultPrice.value
|
||||
})
|
||||
|
||||
const costPrice = computed(() => {
|
||||
if (!productConfig.value) return 0.00
|
||||
// 平台定价成本
|
||||
let platformPricing = 0
|
||||
platformPricing += productConfig.value.cost_price
|
||||
if (price.value > productConfig.value.p_pricing_standard) {
|
||||
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio
|
||||
}
|
||||
|
||||
if (productConfig.value.a_pricing_standard > platformPricing && productConfig.value.a_pricing_end > platformPricing && productConfig.value.a_overpricing_ratio > 0) {
|
||||
if (price.value > productConfig.value.a_pricing_standard) {
|
||||
if (price.value > productConfig.value.a_pricing_end) {
|
||||
platformPricing += (productConfig.value.a_pricing_end - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
|
||||
} else {
|
||||
platformPricing += (price.value - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return safeTruncate(platformPricing)
|
||||
})
|
||||
|
||||
const promotionRevenue = computed(() => {
|
||||
return safeTruncate(price.value - costPrice.value)
|
||||
});
|
||||
|
||||
// 价格校验与修正逻辑
|
||||
const validatePrice = (currentPrice) => {
|
||||
const min = productConfig.value.price_range_min;
|
||||
const max = productConfig.value.price_range_max;
|
||||
let newPrice = Number(currentPrice);
|
||||
let message = '';
|
||||
|
||||
// 处理无效输入
|
||||
if (isNaN(newPrice)) {
|
||||
newPrice = defaultPrice.value;
|
||||
return { newPrice, message: '输入无效,请输入价格' };
|
||||
}
|
||||
|
||||
// 处理小数位数(兼容科学计数法)
|
||||
try {
|
||||
const priceString = newPrice.toString()
|
||||
const [_, decimalPart = ""] = priceString.split('.');
|
||||
// 当小数位数超过2位时处理
|
||||
if (decimalPart.length > 2) {
|
||||
newPrice = parseFloat(safeTruncate(newPrice));
|
||||
message = '价格已自动格式化为两位小数';
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// 范围校验(基于可能格式化后的值)
|
||||
if (newPrice < min) {
|
||||
message = `价格不能低于 ${min} 元`;
|
||||
newPrice = min;
|
||||
} else if (newPrice > max) {
|
||||
message = `价格不能高于 ${max} 元`;
|
||||
newPrice = max;
|
||||
}
|
||||
return { newPrice, message };
|
||||
}
|
||||
function safeTruncate(num, decimals = 2) {
|
||||
if (isNaN(num) || !isFinite(num)) return "0.00";
|
||||
|
||||
const factor = 10 ** decimals;
|
||||
const scaled = Math.trunc(num * factor);
|
||||
const truncated = scaled / factor;
|
||||
|
||||
return truncated.toFixed(decimals);
|
||||
}
|
||||
const isManualConfirm = ref(false)
|
||||
const onConfirm = () => {
|
||||
if (isManualConfirm.value) return
|
||||
const { newPrice, message } = validatePrice(price.value)
|
||||
if (message) {
|
||||
price.value = newPrice
|
||||
toast.show(message)
|
||||
} else {
|
||||
emit("change", price.value)
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
const onBlurPrice = () => {
|
||||
const { newPrice, message } = validatePrice(price.value)
|
||||
if (message) {
|
||||
isManualConfirm.value = true
|
||||
price.value = newPrice
|
||||
toast.show(message)
|
||||
}
|
||||
setTimeout(() => {
|
||||
isManualConfirm.value = false
|
||||
}, 0)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wd-input {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
:deep(.wd-input__label-inner) {
|
||||
font-size: 24px !important; /* 增大label字体 */
|
||||
}
|
||||
|
||||
:deep(.wd-input__inner) {
|
||||
font-size: 24px !important; /* 增大输入框内字体 */
|
||||
}
|
||||
|
||||
:deep(.wd-input) {
|
||||
height: auto !important; /* 确保高度自适应 */
|
||||
padding: 8px 0 !important; /* 增加垂直内边距 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
522
src/components/QRcode.vue
Normal file
522
src/components/QRcode.vue
Normal file
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<wd-popup v-model="show" position="bottom" round>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<view class="max-h-[calc(100vh-100px)] m-4">
|
||||
<!-- 单 canvas 离屏绘制,避免 swiper 未挂载项取不到节点 -->
|
||||
<canvas
|
||||
id="mpPosterCanvas"
|
||||
canvas-id="mpPosterCanvas"
|
||||
type="2d"
|
||||
class="mp-poster-canvas-hidden"
|
||||
/>
|
||||
<view class="p-2 flex justify-center">
|
||||
<swiper
|
||||
:key="swiperMountKey"
|
||||
class="mp-poster-swiper w-full"
|
||||
:style="{ height: swiperHeightPx + 'px' }"
|
||||
:duration="280"
|
||||
:easing-function="easeOutCubic"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="(_, idx) in posterSrcList" :key="idx" class="mp-swiper-item">
|
||||
<view class="mp-poster-item">
|
||||
<image
|
||||
v-if="renderedPaths[idx]"
|
||||
:src="renderedPaths[idx]"
|
||||
mode="aspectFit"
|
||||
class="rounded-xl shadow poster-preview-mp"
|
||||
:style="posterPreviewStyle"
|
||||
/>
|
||||
<view v-else class="text-gray-400 text-sm py-16">生成海报中…</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<view
|
||||
v-if="mode === 'promote'"
|
||||
class="text-center text-gray-500 text-xs mb-2"
|
||||
>
|
||||
← 左右滑动切换海报 →
|
||||
</view>
|
||||
<view class="divider">分享与保存</view>
|
||||
<view class="mp-share-actions">
|
||||
<button
|
||||
class="share-mp-btn flex flex-col items-center justify-center"
|
||||
open-type="share"
|
||||
plain
|
||||
@tap="onShareFriendPrepare"
|
||||
>
|
||||
<image src="/static/image/icon_share_friends.svg" class="w-10 h-10 rounded-full" />
|
||||
<text class="text-center mt-1 text-gray-600 text-xs">分享给好友</text>
|
||||
</button>
|
||||
<view class="flex flex-col items-center justify-center" @click="savePoster">
|
||||
<image src="/static/image/icon_share_img.svg" class="w-10 h-10 rounded-full" />
|
||||
<view class="text-center mt-1 text-gray-600 text-xs">保存图片</view>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center" @click="copyUrl">
|
||||
<image src="/static/image/icon_share_url.svg" class="w-10 h-10 rounded-full" />
|
||||
<view class="text-center mt-1 text-gray-600 text-xs">复制链接</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="max-h-[calc(100vh-100px)] m-4">
|
||||
<view class="p-4 flex justify-center">
|
||||
<view class="max-h-[70vh] rounded-xl overflow-hidden">
|
||||
<image
|
||||
:src="posterImageUrlRemote"
|
||||
class="rounded-xl shadow poster-image"
|
||||
:style="{ width: imageWidth + 'px', height: imageHeight + 'px' }"
|
||||
mode="aspectFit"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="divider">分享到好友</view>
|
||||
<view class="flex items-center justify-around">
|
||||
<view class="flex flex-col items-center justify-center" @click="savePoster">
|
||||
<image src="/static/image/icon_share_img.svg" class="w-10 h-10 rounded-full" />
|
||||
<view class="text-center mt-1 text-gray-600 text-xs">保存图片</view>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center" @click="copyUrl">
|
||||
<image src="/static/image/icon_share_url.svg" class="w-10 h-10 rounded-full" />
|
||||
<view class="text-center mt-1 text-gray-600 text-xs">复制链接</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, toRefs, watch, nextTick, getCurrentInstance } from 'vue'
|
||||
import { getApiBaseUrl, getAgentTabShareTitle, getShareTitle } from '@/utils/runtimeEnv.js'
|
||||
import { buildPromotionH5Url } from '@/utils/promotionH5Url.js'
|
||||
import { setMiniPromotionShareFriend } from '@/utils/miniPromotionSharePayload.js'
|
||||
// #ifdef MP-WEIXIN
|
||||
import { getPosterSrcList, drawMergedPosterWeixin } from '@/utils/posterQrWeixin.js'
|
||||
// #endif
|
||||
|
||||
const props = defineProps({
|
||||
linkIdentifier: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'promote',
|
||||
},
|
||||
})
|
||||
|
||||
const { linkIdentifier, mode } = toRefs(props)
|
||||
const show = defineModel('show')
|
||||
|
||||
const imageWidth = ref(300)
|
||||
const imageHeight = ref(500)
|
||||
|
||||
function generalUrl() {
|
||||
return buildPromotionH5Url(mode.value, linkIdentifier.value)
|
||||
}
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
const posterImageUrlRemote = computed(() => {
|
||||
const qrcodeUrl = generalUrl()
|
||||
return `${getApiBaseUrl()}/agent/promotion/qrcode?qrcode_type=${mode.value}&qrcode_url=${encodeURIComponent(qrcodeUrl)}`
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const instance = getCurrentInstance()
|
||||
const posterSrcList = computed(() => getPosterSrcList(mode.value))
|
||||
const renderedPaths = ref([])
|
||||
const currentSwiperIndex = ref(0)
|
||||
const swiperHeightPx = ref(420)
|
||||
const previewWidthPx = ref(300)
|
||||
/** 打开/切换模式时递增,强制 swiper 从第 0 页重建,避免受控 current 与手势打架导致卡顿 */
|
||||
const swiperMountKey = ref(0)
|
||||
/** 与基础库默认一致,缩短动画减少与异步生成叠在一起时的顿挫感 */
|
||||
const easeOutCubic = 'easeOutCubic'
|
||||
|
||||
const posterPreviewStyle = computed(() => ({
|
||||
width: `${previewWidthPx.value}px`,
|
||||
height: `${swiperHeightPx.value}px`,
|
||||
}))
|
||||
|
||||
/**
|
||||
* 按本地底图宽高比,把整张海报缩进可视区域(避免 widthFix + 固定 swiper 高度裁切)
|
||||
*/
|
||||
function updateMpPosterLayout() {
|
||||
const sys = uni.getSystemInfoSync()
|
||||
const maxW = Math.min(sys.windowWidth * 0.92, 360)
|
||||
const maxH = Math.min(sys.windowHeight * 0.62, 580)
|
||||
const firstSrc = getPosterSrcList(mode.value)[0]
|
||||
const fallbackRatio = 1920 / 1080
|
||||
|
||||
const applyRatio = (ratio) => {
|
||||
let w = maxW
|
||||
let h = w * ratio
|
||||
if (h > maxH) {
|
||||
h = maxH
|
||||
w = h / ratio
|
||||
}
|
||||
previewWidthPx.value = Math.floor(w)
|
||||
swiperHeightPx.value = Math.ceil(h)
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.getImageInfo({
|
||||
src: firstSrc,
|
||||
success: (info) => {
|
||||
const ratio = info.width > 0 ? info.height / info.width : fallbackRatio
|
||||
applyRatio(ratio)
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
applyRatio(fallbackRatio)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 串行生成,避免多索引共用同一 canvas 竞态 */
|
||||
let mpGenSeq = Promise.resolve()
|
||||
|
||||
function resetMpPosters() {
|
||||
mpGenSeq = Promise.resolve()
|
||||
const n = posterSrcList.value.length
|
||||
renderedPaths.value = Array.from({ length: n }, () => '')
|
||||
currentSwiperIndex.value = 0
|
||||
}
|
||||
|
||||
function generatePosterMp(index) {
|
||||
if (renderedPaths.value[index]) return Promise.resolve()
|
||||
const proxy = instance?.proxy
|
||||
if (!proxy) return Promise.resolve()
|
||||
|
||||
mpGenSeq = mpGenSeq
|
||||
.catch(() => {})
|
||||
.then(async () => {
|
||||
if (renderedPaths.value[index]) return
|
||||
await new Promise((r) => setTimeout(r, 80))
|
||||
const canvas = await new Promise((resolve, reject) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select('#mpPosterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
const node = res?.[0]?.node
|
||||
if (node) resolve(node)
|
||||
else reject(new Error('未获取到 canvas 节点'))
|
||||
})
|
||||
})
|
||||
const temp = await drawMergedPosterWeixin({
|
||||
canvas,
|
||||
linkUrl: generalUrl(),
|
||||
posterSrc: posterSrcList.value[index],
|
||||
mode: mode.value,
|
||||
index,
|
||||
componentInstance: proxy,
|
||||
})
|
||||
const next = [...renderedPaths.value]
|
||||
next[index] = temp
|
||||
renderedPaths.value = next
|
||||
})
|
||||
return mpGenSeq
|
||||
}
|
||||
|
||||
function onSwiperChange(e) {
|
||||
const cur = e.detail?.current ?? 0
|
||||
currentSwiperIndex.value = cur
|
||||
if (!renderedPaths.value[cur]) {
|
||||
// 等 swiper 切换动画走一部分再跑 canvas,减轻主线程卡顿
|
||||
setTimeout(() => {
|
||||
generatePosterMp(cur).catch((err) => {
|
||||
console.error('生成海报失败', err)
|
||||
uni.showToast({ title: '海报生成失败', icon: 'none' })
|
||||
})
|
||||
}, 160)
|
||||
}
|
||||
}
|
||||
|
||||
watch(show, (v) => {
|
||||
if (!v)
|
||||
return
|
||||
updateMpPosterLayout().then(() => {
|
||||
swiperMountKey.value += 1
|
||||
resetMpPosters()
|
||||
nextTick(() => {
|
||||
generatePosterMp(0).catch((err) => {
|
||||
console.error('生成海报失败', err)
|
||||
uni.showToast({ title: '海报生成失败', icon: 'none' })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
watch([mode, linkIdentifier], () => {
|
||||
if (!show.value)
|
||||
return
|
||||
updateMpPosterLayout().then(() => {
|
||||
swiperMountKey.value += 1
|
||||
resetMpPosters()
|
||||
nextTick(() => {
|
||||
generatePosterMp(0).catch(() => {})
|
||||
})
|
||||
})
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
function shareTitleForMode() {
|
||||
return mode.value === 'invitation' ? getShareTitle() : getAgentTabShareTitle()
|
||||
}
|
||||
|
||||
function onShareFriendPrepare() {
|
||||
if (!linkIdentifier.value) {
|
||||
uni.showToast({ title: '请先生成推广内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const idx = currentSwiperIndex.value
|
||||
const img = renderedPaths.value[idx] || ''
|
||||
setMiniPromotionShareFriend({
|
||||
title: shareTitleForMode(),
|
||||
path: `/pages/h5open?m=${mode.value}&id=${encodeURIComponent(linkIdentifier.value)}`,
|
||||
imageUrl: img,
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
function calculateImageSize(imgWidth, imgHeight) {
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
const maxHeight = sysInfo.windowHeight * 0.6
|
||||
const maxWidth = sysInfo.windowWidth * 0.8
|
||||
let width = 300
|
||||
let height = width * (imgHeight / imgWidth)
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
width = height * (imgWidth / imgHeight)
|
||||
}
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth
|
||||
height = width * (imgHeight / imgWidth)
|
||||
}
|
||||
return {
|
||||
width: Math.floor(width),
|
||||
height: Math.floor(height),
|
||||
}
|
||||
}
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
function onImageLoad() {
|
||||
uni.getImageInfo({
|
||||
src: posterImageUrlRemote.value,
|
||||
success: (res) => {
|
||||
const size = calculateImageSize(res.width, res.height)
|
||||
imageWidth.value = size.width
|
||||
imageHeight.value = size.height
|
||||
},
|
||||
fail: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
function onImageError() {
|
||||
uni.showToast({
|
||||
title: '图片加载失败',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
function savePoster() {
|
||||
uni.showLoading({ title: '正在保存…' })
|
||||
// #ifdef MP-WEIXIN
|
||||
const idx = currentSwiperIndex.value
|
||||
const runSave = (filePath) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
if (err.errMsg && err.errMsg.includes('auth')) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (r) => {
|
||||
if (r.confirm) uni.openSetting()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
const path = renderedPaths.value[idx]
|
||||
if (path) {
|
||||
runSave(path)
|
||||
return
|
||||
}
|
||||
generatePosterMp(idx)
|
||||
.then(() => {
|
||||
const p = renderedPaths.value[idx]
|
||||
if (p) runSave(p)
|
||||
else uni.hideLoading()
|
||||
})
|
||||
.catch(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '请稍候再试', icon: 'none' })
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.downloadFile({
|
||||
url: posterImageUrlRemote.value,
|
||||
success: (downloadRes) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: downloadRes.tempFilePath,
|
||||
success: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
if (err.errMsg && err.errMsg.includes('auth')) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (r) => {
|
||||
if (r.confirm) uni.openSetting()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '图片下载失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
function copyUrl() {
|
||||
uni.setClipboardData({
|
||||
data: generalUrl(),
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '链接已复制!',
|
||||
icon: 'success',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.divider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
color: #969799;
|
||||
font-size: 14px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
background-color: #ebedf0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* #ifdef MP-WEIXIN */
|
||||
.mp-poster-canvas-hidden {
|
||||
position: fixed;
|
||||
left: -2000px;
|
||||
top: 0;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.mp-poster-swiper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mp-swiper-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mp-poster-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.poster-preview-mp {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.mp-share-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
row-gap: 18px;
|
||||
column-gap: 8px;
|
||||
padding: 0 4px 8px;
|
||||
}
|
||||
|
||||
.share-mp-btn {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
line-height: 1.2;
|
||||
font-size: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.share-mp-btn::after {
|
||||
border: none;
|
||||
}
|
||||
/* #endif */
|
||||
</style>
|
||||
36
src/components/VipBanner.vue
Normal file
36
src/components/VipBanner.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<view class="card mb-4 relative overflow-hidden" @click="goToVip">
|
||||
<view class="absolute inset-0 bg-gradient-to-r from-yellow-400 to-yellow-300 opacity-40"></view>
|
||||
<view class="p-2 relative z-10">
|
||||
<view class="flex justify-between items-center">
|
||||
<view>
|
||||
<view class="text-lg font-bold text-yellow-800">会员专享特权</view>
|
||||
<view class="text-sm text-yellow-700 mt-1">升级VIP获得更多收益</view>
|
||||
</view>
|
||||
<view class="bg-yellow-500 px-3 py-1 rounded-full text-white text-sm shadow-sm">
|
||||
立即查看
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 装饰元素 -->
|
||||
<view class="absolute -right-4 -top-4 w-16 h-16 bg-yellow-200 rounded-full opacity-60"></view>
|
||||
<view class="absolute right-5 -bottom-4 w-12 h-12 bg-yellow-100 rounded-full opacity-40"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 跳转到VIP页面
|
||||
const goToVip = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/agentVipApply'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user