refactor: 重构多个组件以支持 Vue 3 语法和功能

- 将多个组件转换为 `<script setup>` 语法,提升可读性和性能
- 优化状态管理和事件处理逻辑,简化代码结构
- 更新样式和布局以适应新组件结构
- 添加新功能和修复已知问题,提升用户体验
This commit is contained in:
Yer11214
2026-07-08 18:37:11 +08:00
parent 4d0b0fb5e4
commit 349ffa4f34
189 changed files with 18278 additions and 20758 deletions

View File

@@ -10,40 +10,30 @@
</view>
</template>
<script>
export default {
props: {
// 购物车 cart 消息 msg 订单 order 查询 search
type: {
type: String,
default: 'search'
},
isBtn:{
type: Boolean,
default: false
},
title:{
type: String,
default: '没有相关内容'
}
},
data() {
return {
src:''
};
},
mounted() {
this.src ='/static/default/default_'+ this.type + '.png';
},
methods: {
toHome() {
uni.switchTab({
url: '/pages/home/home'
});
}
}
};
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const props = withDefaults(defineProps<{
type?: string
isBtn?: boolean
title?: string
}>(), {
type: 'search',
isBtn: false,
title: '没有相关内容'
})
const src = ref('')
onMounted(() => {
src.value = `/static/default/default_${props.type}.png`
})
const toHome = () => {
uni.switchTab({
url: '/pages/home/home'
})
}
</script>
<style lang="scss" scoped>

View File

@@ -13,72 +13,62 @@
</view>
</template>
<script>
import config from "@/config/config";
import logoImg from "@/icon.png";
export default {
data() {
return {
config, // 设置工具类
weChat: false, // 是否微信浏览器该项为true时不显示 当前整个页面
logo: logoImg, //显示的圆形logo
};
},
mounted() {
// #ifdef H5
// 判断是否是微信浏览器
var ua = navigator.userAgent.toLowerCase();
var isWeixin = ua.indexOf("micromessenger") != -1;
if (isWeixin) {
this.weChat = true;
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import config from "@/config/config"
import logoImg from "@/icon.png"
const weChat = ref(false)
const logo = logoImg
onMounted(() => {
// #ifdef H5
// 判断是否是微信浏览器
var ua = navigator.userAgent.toLowerCase();
var isWeixin = ua.indexOf("micromessenger") != -1;
weChat.value = isWeixin ? true : false
// #endif
})
/**
* 跳转到下载app页面
*/
const downloadApp = () => {
setTimeout(function () {
window.location.href = config.downloadLink;
}, 2000);
}
/**
* 打开app 仅在h5生效 使用ifream唤醒app
*/
const openApp = () => {
let src: string = ''
if (location.href) {
src = location.href.split("/pages")[1];
}
let t = `${config.schemeLink}pages${src}`
try {
var e = navigator.userAgent.toLowerCase(),
n = e.match(/cpu iphone os (.*?) like mac os/);
if (
((n = null !== n ? n[1].replace(/_/g, ".") : 0), parseInt(n) >= 9)
) {
window.location.href = t
downloadApp()
} else {
this.weChat = false;
var r = document.createElement("iframe");
(r.src = t), (r.style.display = "none"), document.body.appendChild(r)
downloadApp()
}
// #endif
},
methods: {
/**
* 跳转到下载app页面
*/
downloadApp() {
setTimeout(function () {
window.location.href = config.downloadLink;
}, 2000);
},
/**
* 打开app 仅在h5生效 使用ifream唤醒app
*/
openApp() {
let src;
if (location.href) {
src = location.href.split("/pages")[1];
}
let t = `${config.schemeLink}pages${src}`;
try {
var e = navigator.userAgent.toLowerCase(),
n = e.match(/cpu iphone os (.*?) like mac os/);
if (
((n = null !== n ? n[1].replace(/_/g, ".") : 0), parseInt(n) >= 9)
) {
window.location.href = t;
this.downloadApp();
} else {
var r = document.createElement("iframe");
(r.src = t), (r.style.display = "none"), document.body.appendChild(r);
this.downloadApp();
}
} catch (e) {
window.location.href = t;
this.downloadApp();
}
},
},
};
} catch (e) {
window.location.href = t
downloadApp()
}
}
</script>
<style scoped lang="scss">

View File

@@ -82,19 +82,19 @@
<!-- 拼团购买仅筛选出当前拼团类型商品 -->
<template v-for="(spec_val, spec_index) in spec.values" :key="spec_index">
<view
v-if="parentOrder && spec_val.skuId == goodsDetail.id"
:class="{ active: spec_val.value == currentSelected[specIndex] }"
class="skus-view-item"
@click="handleClickSpec(spec, specIndex, spec_val)"
>
{{ spec_val.value }}
</view>
<view
v-if="parentOrder && spec_val.skuId == goodsDetail.id"
:class="{ active: spec_val.value == currentSelected[specIndex] }"
class="skus-view-item"
@click="handleClickSpec(spec, specIndex, spec_val)"
>
{{ spec_val.value }}
</view>
</template>
</view>
</view>
<div class="soldout" v-if="goodsDetail.quantity === 0">
<u-alert-tips type="warning" title="商品已售罄" description="当前商品库存为0"></u-alert-tips>
<u-alert type="warning" title="商品已售罄" :description="'当前商品库存为0'"></u-alert>
</div>
<!-- 数量 -->
<view v-if="goodsDetail.quantity !== 0" class="goods-skus-number flex flex-a-c flex-j-sb">
@@ -111,313 +111,269 @@
</u-popup>
</div>
</template>
<script>
import * as API_trade from '@/api/trade.js';
import setup from './popup';
import uniNumberBox from '@/components/uni-number-box'
export default {
components: {
uniNumberBox
},
data() {
return {
setup,
num: this.wholesaleList && this.wholesaleList.length > 0 ? this.wholesaleList[0].num : 1,
selectName: '', //选中商品的昵称
selectSkuList: '', //选中商铺sku,
selectedSpecImg: '', //选中的图片路径
buyType: '', //用于存储促销,拼团等活动类型
parentOrder: '', //父级拼团活动的数据 - 如果是团员则有数据
formatList: [],
currentSelected: [],
skuList: '',
isClose: false //是否可以点击遮罩关闭
};
},
props: {
wholesaleList: {
type: null,
default: false
},
buyMask: {
type: Boolean,
default: false
},
isGroup: {
type: Boolean,
default: false
},
goodsDetail: {
default: '',
type: null
},
selectedSku: {
default: '',
type: null
},
goodsSpec: {
default: '',
type: null
},
addr: {
default: '',
type: null
},
pointDetail: {
default: '',
type: null
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue'
import * as API_trade from '@/api/trade.js'
import setup from './popup.js'
import uniNumberBox from '@/components/uni-number-box.vue'
import { goodsFormatPrice } from '@/utils/filters.js'
const props = withDefaults(defineProps<{
wholesaleList?: any[] | boolean
buyMask?: boolean
isGroup?: boolean
goodsDetail?: any
selectedSku?: any | null
goodsSpec?: any | null
addr?: any | null
pointDetail?: any | null
}>(), {
wholesaleList: false,
buyMask: false,
isGroup: false,
goodsDetail: () => ({}),
selectedSku: '',
goodsSpec: '',
addr: '',
pointDetail: ''
})
const emit = defineEmits(['closeBuy', 'changed', 'handleClickSku', 'queryCart'])
const num = ref(props.wholesaleList && Array.isArray(props.wholesaleList) && props.wholesaleList.length > 0 ? props.wholesaleList[0].num : 1)
const selectName = ref('')
const selectSkuList = ref<any>('')
const selectedSpecImg = ref('')
const buyType = ref('')
const parentOrder = ref<any>('')
const formatList = ref<any[]>([])
const currentSelected = ref<string[]>([])
const skuList = ref('')
watch(num, (val) => {
val == 0 ? num.value = 1 : ''
if (val) {
//超过库存后修改回库存
if (val > props.goodsDetail.quantity) {
num.value = props.goodsDetail.quantity
}
},
computed: {
wholesalePrice(key) {
return this.wholesaleList.length
? this.wholesaleList.map(item => {
return item.price;
})
: [];
},
wholesaleNum(key) {
return this.wholesaleList.length
? this.wholesaleList.map(item => {
return item.num;
})
: [];
}
},
watch: {
num(val) {
val == 0 ? this.num = 1 : ''
if (val) {
//超过库存后修改回库存
if (val > this.goodsDetail.quantity) {
this.$nextTick(function() {
this.num = this.goodsDetail.quantity;
});
}
}
},
buyType: {
handler(val) {
if (val) {
this.buyType = val;
}
},
immediate: true
},
selectSkuList: {
handler(val, oldval) {
this.$emit('changed', val);
},
deep: true
},
'goodsDetail.quantity': {
handler(val) {
if (val == 0) {
uni.showToast({
title: '商品已售罄',
duration: 2000,
icon: 'none'
})
this.num = 1;
}
}
}
},
methods: {
numCheck(val) {
if (this.wholesaleList && this.wholesaleList.length > 0) {
if (this.num <= this.wholesaleList[0].num) {
uni.showToast({
title: '批发商品购买数量不能小于起批数量!',
duration: 2000,
icon: 'none'
});
this.num = this.wholesaleList[0].num;
}
}
},
closeMask() {
this.$emit('closeBuy', false);
},
/**点击规格 */
handleClickSpec(val, index, specValue) {
this.currentSelected[index] = specValue.value;
let selectedSkuId = this.goodsSpec.find(i => {
let matched = true;
let specValues = i.specValues.filter(j => j.specName !== 'images');
for (let n = 0; n < specValues.length; n++) {
if (specValues[n].specValue !== this.currentSelected[n]) {
matched = false;
return;
}
}
if (matched) {
return i;
}
});
if (selectedSkuId?.skuId) {
this.currentSelected[index] = specValue.value;
this.selectSkuList = {
spec: {
specName: val.name,
specValue: specValue.value
},
data: this.goodsDetail
};
this.selectName = specValue.value;
this.$emit('handleClickSku', {
skuId: selectedSkuId.skuId,
goodsId: this.goodsDetail.goodsId
});
} else {
uni.showToast({
title: '暂无该商品!',
duration: 2000,
icon: 'none'
});
}
},
/**
* 直接购买
*/
buy(data) {
API_trade.addToCart(data).then(res => {
if (res.data.success) {
uni.navigateTo({
url: `/pages/order/fillorder?way=${data.cartType}&addr=${''}&parentOrder=${encodeURIComponent(JSON.stringify(this.parentOrder))}`
});
}
});
},
/**
* 添加到购物车或购买
*/
addToCartOrBuy(val) {
if (!this.selectSkuList) {
uni.showToast({
title: '请选择规格商品',
icon: 'none'
});
return;
}
let data = {
skuId: this.goodsDetail.id,
num: this.num
};
if (val == 'cart') {
API_trade.addToCart(data).then(res => {
if (res.data.code == 200) {
uni.showToast({
title: '商品已添加到购物车',
icon: 'none'
});
this.$emit('queryCart');
this.closeMask();
}
});
} else {
// 判断是否拼团商品
if (this.buyType) {
data.cartType = 'PINTUAN';
} else if (this.goodsDetail.goodsType == 'VIRTUAL_GOODS') {
data.cartType = 'VIRTUAL';
} else {
data.cartType = 'BUY_NOW';
}
API_trade.addToCart(data).then(res => {
if (res.data.code == 200) {
uni.navigateTo({
url: `/pages/order/fillorder?way=${data.cartType}&addr=${this.addr.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(this.parentOrder))}`
});
}
});
}
},
formatSku(list) {
// 格式化数据
let arr = [{}];
if (!Array.isArray(list)) {
return false;
}
list.forEach((item, index) => {
item.specValues.forEach((spec, specIndex) => {
let name = spec.specName;
let values = {
value: spec.specValue,
quantity: item.quantity,
skuId: item.skuId
};
if (name === 'images') {
return;
}
arr.forEach((arrItem, arrIndex) => {
if (
arrItem.name == name &&
arrItem.values &&
!arrItem.values.find(i => {
return i.value === values.value;
})
) {
arrItem.values.push(values);
}
let keys = arr.map(key => {
return key.name;
});
if (!keys.includes(name)) {
arr.push({
name: name,
values: [values]
});
}
});
});
});
arr.shift();
this.formatList = arr;
list.forEach(item => {
// 默认选中
if (item.skuId === this.goodsDetail.id) {
item.specValues
.filter(i => i.specName !== 'images')
.forEach((value, _index) => {
this.currentSelected[_index] = value.specValue;
this.selectName = value.specValue;
this.selectSkuList = {
spec: value,
data: this.goodsDetail
};
});
}
});
this.skuList = list;
// console.log(" this.skuList", this.skuList)
}
},
mounted() {
this.formatSku(this.goodsSpec);
}
};
})
watch(() => props.buyType, (val) => {
if (val) {
buyType.value = val as string
}
}, { immediate: true })
watch(selectSkuList, (_val) => {
emit('changed', selectSkuList.value)
}, { deep: true })
watch(() => props.goodsDetail?.quantity, (val) => {
if (val == 0) {
uni.showToast({
title: '商品已售罄',
duration: 2000,
icon: 'none'
})
num.value = 1
}
})
const numCheck = (val: number) => {
if (Array.isArray(props.wholesaleList) && props.wholesaleList.length > 0) {
if (num.value <= props.wholesaleList[0].num) {
uni.showToast({
title: '批发商品购买数量不能小于起批数量!',
duration: 2000,
icon: 'none'
})
num.value = props.wholesaleList[0].num
}
}
}
const closeMask = () => {
emit('closeBuy', false)
}
/**点击规格 */
const handleClickSpec = (val: any, index: number, specValue: any) => {
currentSelected.value[index] = specValue.value
let selectedSkuId = props.goodsSpec.find((i: any) => {
let matched = true
let specValues = i.specValues.filter((j: any) => j.specName !== 'images')
for (let n = 0; n < specValues.length; n++) {
if (specValues[n].specValue !== currentSelected.value[n]) {
matched = false
return
}
}
if (matched) {
return i
}
})
if (selectedSkuId?.skuId) {
currentSelected.value[index] = specValue.value
selectSkuList.value = {
spec: {
specName: val.name,
specValue: specValue.value
},
data: props.goodsDetail
}
selectName.value = specValue.value
emit('handleClickSku', {
skuId: selectedSkuId.skuId,
goodsId: props.goodsDetail.goodsId
})
} else {
uni.showToast({
title: '暂无该商品!',
duration: 2000,
icon: 'none'
})
}
}
/**
* 直接购买
*/
const buy = (data: any) => {
API_trade.addToCart(data).then(res => {
if (res.data.success) {
uni.navigateTo({
url: `/pages/order/fillorder?way=${data.cartType}&addr=${''}&parentOrder=${encodeURIComponent(JSON.stringify(parentOrder.value))}`
})
}
})
}
/**
* 添加到购物车或购买
*/
const addToCartOrBuy = (val: string) => {
if (!selectSkuList.value) {
uni.showToast({
title: '请选择规格商品',
icon: 'none'
})
return
}
let data = {
skuId: props.goodsDetail.id,
num: num.value
}
if (val == 'cart') {
API_trade.addToCart(data).then(res => {
if (res.data.code == 200) {
uni.showToast({
title: '商品已添加到购物车',
icon: 'none'
})
emit('queryCart')
closeMask()
}
})
} else {
// 判断是否拼团商品
if (buyType.value) {
data.cartType = 'PINTUAN'
} else if (props.goodsDetail.goodsType == 'VIRTUAL_GOODS') {
data.cartType = 'VIRTUAL'
} else {
data.cartType = 'BUY_NOW'
}
API_trade.addToCart(data).then(res => {
if (res.data.code == 200) {
uni.navigateTo({
url: `/pages/order/fillorder?way=${data.cartType}&addr=${props.addr?.id || ''}&parentOrder=${encodeURIComponent(JSON.stringify(parentOrder.value))}`
})
}
})
}
}
const formatSku = (list: any[]) => {
// 格式化数据
let arr: any[] = [{}]
if (!Array.isArray(list)) {
return false
}
list.forEach((_item, _index) => {
let item = list[_index]
item.specValues.forEach((spec: any, specIndex: number) => {
let name = spec.specName
let values = {
value: spec.specValue,
quantity: item.quantity,
skuId: item.skuId
}
if (name === 'images') {
return
}
arr.forEach((arrItem, arrIndex) => {
if (
arrItem.name == name &&
arrItem.values &&
!arrItem.values.find((i: any) => {
return i.value === values.value
})
) {
arrItem.values.push(values)
}
let keys = arr.map(key => {
return key.name
})
if (!keys.includes(name)) {
arr.push({
name: name,
values: [values]
})
}
})
})
})
arr.shift()
formatList.value = arr
list.forEach(item => {
// 默认选中
if (item.skuId === props.goodsDetail.id) {
item.specValues
.filter(i => i.specName !== 'images')
.forEach((value, _index) => {
currentSelected.value[_index] = value.specValue
selectName.value = value.specValue
selectSkuList.value = {
spec: value,
data: props.goodsDetail
}
})
}
})
skuList.value = list
}
onMounted(() => {
formatSku(props.goodsSpec)
})
</script>
<style lang="scss" scoped>
@import './popup.scss';

View File

@@ -17,168 +17,156 @@
</div>
</template>
<script>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance } from 'vue'
// 引入绘制插件
import DrawPoster from "@/js_sdk/u-draw-poster";
import logoImg from "@/pages/passport/static/logo-title.png";
import DrawPoster from "@/js_sdk/u-draw-poster"
import logoImg from "@/pages/passport/static/logo-title.png"
export default {
data: () => ({
imgUrl: "", //绘制出来的图片路径
show: false, //是否展示模态框
dp: {}, //绘制的dp对象用于存储绘制等一些方法。
logo: logoImg, //本地logo地址
}),
const instance = getCurrentInstance()
props: {
/**
* 父级传参的数据
*/
res: {
type: null,
default: "",
const props = defineProps<{
res?: any
}>()
const imgUrl = ref("")
const show = ref(false)
const dp = ref<any>({})
const logo = logoImg
/**
* 解决微信小程序中图片模糊问题
*/
// #ifdef MP-WEIXIN
const st2 = (size: number) => size * 2
// #endif
// #ifndef MP-WEIXIN
const st2 = (size: number) => size
// #endif
/**
* 保存图片
*/
const downLoad = () => {
uni.saveImageToPhotosAlbum({
filePath: imgUrl.value,
success: function () {
uni.showToast({
title: "保存成功!",
icon: "none",
})
},
},
onUnload() {},
methods: {
/**
* 解决微信小程序中图片模糊问题
*/
// #ifdef MP-WEIXIN
st2: (size) => size * 2,
// #endif
// #ifndef MP-WEIXIN
st2: (size) => size,
// #endif
/**
* 保存图片
*/
downLoad() {
uni.saveImageToPhotosAlbum({
filePath: this.imgUrl,
success: function () {
uni.showToast({
title: "保存成功!",
icon: "none",
});
},
fail: function () {
uni.showToast({
title: "保存失败,请稍后重试!",
icon: "none",
});
},
});
fail: function () {
uni.showToast({
title: "保存失败,请稍后重试!",
icon: "none",
})
},
})
}
/**
* 创建canvas
*/
async init() {
this.show = true;
this.dp = await DrawPoster.build({
selector: "canvas",
componentThis: this,
loading: true,
debugging: true,
});
let dp = this.dp;
// #ifdef MP-WEIXIN
// 用于微信小程序中画布错乱问题
dp.canvas.width = this.st2(600);
dp.canvas.height = this.st2(960);
// #endif
this.draw(dp);
},
/**
* 创建canvas
*/
const init = async () => {
show.value = true
dp.value = await DrawPoster.build({
selector: "canvas",
componentThis: instance?.proxy,
loading: true,
debugging: true,
})
let dpVal = dp.value
// #ifdef MP-WEIXIN
// 用于微信小程序中画布错乱问题
dpVal.canvas.width = st2(600)
dpVal.canvas.height = st2(960)
// #endif
await draw(dpVal)
}
async draw(dp) {
const { width, height, background, title } = this.res.container;
const { code, img, price } = this.res.bottom;
const draw = async (dp: any) => {
const { width, height, background, title } = props.res.container
const { code, img, price } = props.res.bottom
// /** 绘制背景 */
await dp.draw((ctx) => {
ctx.fillStyle = background;
ctx.fillRoundRect(
this.st2(0),
this.st2(0),
this.st2(width),
this.st2(height),
this.st2(12)
);
ctx.clip();
});
/** 绘制图片 */
dp.draw(async (ctx) => {
await Promise.all([
// 绘制Logo
ctx.drawImage(
this.logo,
this.st2(175),
this.st2(0),
this.st2(256),
this.st2(144)
),
// 中间图片
ctx.drawImage(
img,
this.st2(100),
this.st2(150),
this.st2(400),
this.st2(400)
),
// 二维码
ctx.drawImage(
code,
this.st2(39),
this.st2(750),
this.st2(150),
this.st2(150)
),
]);
});
// /** 绘制背景 */
await dp.draw((ctx: any) => {
ctx.fillStyle = background
ctx.fillRoundRect(
st2(0),
st2(0),
st2(width),
st2(height),
st2(12)
)
ctx.clip()
})
/** 绘制图片 */
dp.draw(async (ctx: any) => {
await Promise.all([
// 绘制Logo
ctx.drawImage(
logo,
st2(175),
st2(0),
st2(256),
st2(144)
),
// 中间图片
ctx.drawImage(
img,
st2(100),
st2(150),
st2(400),
st2(400)
),
// 二维码
ctx.drawImage(
code,
st2(39),
st2(750),
st2(150),
st2(150)
),
])
})
/** 绘制中间文字*/
await dp.draw((ctx) => {
ctx.fillStyle = "#333";
ctx.font = `bold ${this.st2(24)}px PingFang SC`;
ctx.textAlign = "center";
ctx.fillWarpText({
text: title,
maxWidth: this.st2(500),
x: this.st2(300),
y: this.st2(600),
layer: 1,
});
/** 绘制中间文字*/
await dp.draw((ctx: any) => {
ctx.fillStyle = "#333"
ctx.font = `bold ${st2(24)}px PingFang SC`
ctx.textAlign = "center"
ctx.fillWarpText({
text: title,
maxWidth: st2(500),
x: st2(300),
y: st2(600),
layer: 1,
})
ctx.fillStyle = "#ff3c2a";
ctx.font = `${this.st2(38)}px PingFang SC`;
ctx.textAlign = "center";
ctx.fillText(price, this.st2(300), this.st2(680));
});
ctx.fillStyle = "#ff3c2a"
ctx.font = `${st2(38)}px PingFang SC`
ctx.textAlign = "center"
ctx.fillText(price, st2(300), st2(680))
})
// /** 绘制底部文字 */
await dp.draw((ctx) => {
ctx.fillStyle = "#666";
ctx.font = `${this.st2(24)}px PingFang SC`;
ctx.fillText("长按图片,识别二维码", this.st2(200), this.st2(866));
ctx.fillStyle = "#666";
ctx.font = `${this.st2(24)}px PingFang SC`;
ctx.fillText("查看商品详情", this.st2(200), this.st2(900));
});
// /** 绘制底部文字 */
await dp.draw((ctx: any) => {
ctx.fillStyle = "#666"
ctx.font = `${st2(24)}px PingFang SC`
ctx.fillText("长按图片,识别二维码", st2(200), st2(866))
ctx.fillStyle = "#666"
ctx.font = `${st2(24)}px PingFang SC`
ctx.fillText("查看商品详情", st2(200), st2(900))
})
this.imgUrl = await dp.createImagePath();
imgUrl.value = await dp.createImagePath()
}
// console.log(posterImgUrl)
},
},
async mounted() {
this.init();
},
};
onMounted(() => {
init()
})
</script>
<style lang="scss" scoped>

View File

@@ -79,192 +79,182 @@
</view>
</template>
<script>
<script setup lang="ts">
import { ref, getCurrentInstance } from 'vue'
import { getRegionsById } from "@/api/common.js"
const instance = getCurrentInstance()
let windowWidth = 0
import { getRegionsById } from "@/api/common.js";
export default {
name: "UniCityNvue",
props: {
headTitle: {
//标题
type: String,
default: "区域选择",
},
pickerSize: {
// 使用多少个tab
type: [String, String],
default: "1",
},
provinceData: {
// 默认的省市区id如果不使用id的情况下则为[]
type: Array,
default: function () {
return [];
const props = withDefaults(defineProps<{
headTitle?: string
pickerSize?: string | string[]
provinceData?: any[]
}>(), {
headTitle: "区域选择",
pickerSize: "1",
provinceData: () => [],
})
const emit = defineEmits(['funcValue'])
const clearRightIcon = ref(true)
const scrollLeft = ref(500)
const scrollTop = ref(0)
const enableScroll = ref(true)
const tabCurrentIndex = ref(0)
const tabbars = ref<any[]>(props.provinceData)
const pickersize = ref(props.pickerSize)
const showPicker = ref(false)
/**
* 显示选择器
*/
const show = () => {
showPicker.value = true
if (tabbars.value[0].children.length == 0) {
getRegionsById(0).then((res: any) => {
tabbars.value[0].children = res.data.result
})
}
windowWidth = uni.getSystemInfoSync().windowWidth
}
/**
* 关闭选择器
*/
const hide = () => {
showPicker.value = false
}
/**
* tab切换
*/
const changeTab = (e: number) => {
let index = e
setScroll(index)
//延迟300ms,等待swiper动画结束再修改tabbar
tabCurrentIndex.value = index
setTimeout(() => {
getScroll("show" + index)
}, 10)
}
/**
* 获得元素的大小
*/
const getElSize = (id: string): Promise<any> => {
return new Promise((res) => {
let el = uni
.createSelectorQuery()
.in(instance!.proxy)
.select("#" + id)
el.fields(
{
size: true,
scrollOffset: true,
rect: true,
},
},
},
data() {
return {
clearRightIcon: true, //是否显示右侧关闭icon
scrollLeft: 500, //顶部选项卡左滑距离
scrollTop: 0, //默认滚动顶部为0
enableScroll: true, //是否启用滚动
tabCurrentIndex: 0, //当前选项卡索引
tabbars: this.provinceData, //默认的省市区id
pickersize: this.pickerSize, //多少个tab 推荐为4级
showPicker: false, //显示选取器
};
},
methods: {
/**
* 显示选择器
*/
show() {
this.showPicker = true;
if (this.tabbars[0].children.length == 0) {
getRegionsById(0).then((res) => {
this.tabbars[0].children = res.data.result;
});
(data: any) => {
res(data)
}
).exec()
})
}
windowWidth = uni.getSystemInfoSync().windowWidth;
},
/**
* 关闭选择器
*/
hide() {
this.showPicker = false;
},
/**
* tab切换
*/
changeTab(e) {
let index = e;
this.setScroll(index);
//延迟300ms,等待swiper动画结束再修改tabbar
this.tabCurrentIndex = index;
setTimeout(() => {
this.getScroll("show" + index);
}, 10);
},
/**
* 获得元素的大小
*/
getElSize(id) {
return new Promise((res, rej) => {
let el = uni
.createSelectorQuery()
.in(this)
.select("#" + id);
el.fields(
{
size: true,
scrollOffset: true,
rect: true,
},
(data) => {
res(data);
}
).exec();
});
},
/**
* 点击城市后回调
*/
async changCity(index, item) {
if (this.tabbars[index].id != item.id) {
this.tabbars[index].localName = item.name;
this.tabbars[index].id = item.id;
this.tabbars[index].center = item.center
if (index < this.tabbars.length - 1) {
this.tabbars.splice(index + 1, this.tabbars.length - index - 1);
}
if (this.tabbars.length < this.pickersize) {
uni.showLoading({
title: "加载中",
mask:true
});
try {
let data = await getRegionsById(item.id);
uni.hideLoading();
// 当前选项级为最后一级时回调,将选中的数据返回
if (data.data.result.length == 0) {
this.$emit("funcValue", this.tabbars);
this.hide();
} else {
// 将新的数据填充进下一级
var current = {
localName: "请选择",
id: "",
children: data.data.result,
};
this.tabbars.push(current);
this.tabCurrentIndex++;
// 当前距离重新为最上面
this['scrollTop'] = 0
}
} catch (error) {
uni.hideLoading();
}
/**
* 点击城市后回调
*/
const changCity = async (index: number, item: any) => {
if (tabbars.value[index].id != item.id) {
tabbars.value[index].localName = item.name
tabbars.value[index].id = item.id
tabbars.value[index].center = item.center
if (index < tabbars.value.length - 1) {
tabbars.value.splice(index + 1, tabbars.value.length - index - 1)
}
if (tabbars.value.length < pickersize.value as number) {
uni.showLoading({
title: "加载中",
mask: true
})
try {
let data = await getRegionsById(item.id)
uni.hideLoading()
// 当前选项级为最后一级时回调,将选中的数据返回
if (data.data.result.length == 0) {
emit("funcValue", tabbars.value)
hide()
} else {
this.$emit("funcValue", this.tabbars);
this.hide();
}
}
},
// 将新的数据填充进下一级
var current = {
localName: "请选择",
id: "",
children: data.data.result,
}
tabbars.value.push(current)
tabCurrentIndex.value++
/**
* 获取当前tab中滚动的距离
*/
async setScroll(index) {
let width = 0;
let nowWidth = 0;
for (let i = 0; i <= index; i++) {
let result = await this.getElSize("tab" + i);
width += result.width;
if (i === index) {
nowWidth = result.width;
// 当前距离重新为最上面
scrollTop.value = 0
}
} catch (error) {
uni.hideLoading()
}
if (width + nowWidth > windowWidth) {
this.scrollLeft = width + nowWidth;
} else {
this.scrollLeft = 0;
}
},
} else {
emit("funcValue", tabbars.value)
hide()
}
}
}
/**
* 计算当前的滚动距离
*/
getScroll(id) {
/**
* 获取当前tab中滚动距离
*/
const setScroll = async (index: number) => {
let width = 0
let nowWidth = 0
for (let i = 0; i <= index; i++) {
let result = await getElSize("tab" + i)
width += result.width
if (i === index) {
nowWidth = result.width
}
}
if (width + nowWidth > windowWidth) {
scrollLeft.value = width + nowWidth
} else {
scrollLeft.value = 0
}
}
/**
* 计算当前的滚动距离
*/
const getScroll = (id: string) => {
uni
.createSelectorQuery()
.in(instance!.proxy)
.select(".panel-scroll-box")
.boundingClientRect((data: any) => {
uni
.createSelectorQuery()
.in(this)
.select(".panel-scroll-box")
.boundingClientRect((data) => {
uni
.createSelectorQuery()
.in(this)
.select("#" + id)
.boundingClientRect((res) => {
if (res != undefined && res != null && res != "") {
this.scrollTop = res.top - data.top;
}
})
.exec();
.in(instance!.proxy)
.select("#" + id)
.boundingClientRect((res: any) => {
if (res != undefined && res != null && res != "") {
scrollTop.value = res.top - data.top
}
})
.exec();
},
},
};
.exec()
})
.exec()
}
// 暴露方法给父组件调用
defineExpose({ show, hide })
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template>
<div>
<view>
<!-- 一行两列商品展示 -->
<view class="goods-list" v-if="type == 'twoColumns'">
<view v-for="(item, index) in res" :key="index" class="goods-item">
@@ -10,7 +10,7 @@
height="330rpx"
mode="aspectFit"
>
<template #loading><u-loading></u-loading></template>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<view class="goods-detail">
@@ -57,7 +57,7 @@
</view>
<!-- 一行一列商品展示 -->
<div v-if="type == 'oneColumns'">
<div v-for="(item, index) in res" :key="index" class="goods-row">
div v-for="(item, index) in res" :key="index" class="goods-row">
<div class="flex goods-col">
<div class="goods-img" @click="navigateToDetailPage(item)">
<u-image
@@ -67,7 +67,7 @@
mode="aspectFit"
:src="item.goodsImage || item.thumbnail"
>
<template #loading><u-loading></u-loading></template>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</div>
<div class="goods-detail">
@@ -112,41 +112,26 @@
</div>
</div>
</div>
</div>
</view>
</template>
<script>
import commonTpl from "@/components/m-goods-list/common";
export default {
data() {
return {
lightColor: this.$mainColor,
};
},
mixins: [commonTpl],
props: {
// 展示的类型
type:{
type:String,
default:"oneColumns"
},
// 遍历的数据
res: {
type: Array,
default: () => {
return [];
},
},
},
methods: {
// 跳转到商品详情
navigateToDetailPage(item) {
uni.navigateTo({
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
});
},
},
};
<script setup lang="ts">
import { useGoodsListCommon } from './common'
import { goodsFormatPrice } from '@/utils/filters.js'
const { lightSearchStr, getPromotion, navigateToDetailPage, navigateToStoreDetailPage } = useGoodsListCommon()
const props = withDefaults(defineProps<{
type?: string
res?: any[]
storeName?: boolean
keyword?: string | null
}>(), {
type: 'oneColumns',
res: () => [],
storeName: true,
keyword: ''
})
</script>
<style lang="scss" scoped>

View File

@@ -0,0 +1,90 @@
import { useStore } from '@/store'
/**
* 商品列表公共逻辑(原 common.vue mixin
* 提供高亮搜索、unicode 转换、促销标签、导航跳转等通用方法
*/
export function useGoodsListCommon() {
const store = useStore()
const lightColor = store.getters.lightColor
/**
* 高亮显示搜索内容
*/
const lightSearchStr = (keyword: string, str: string) => {
if (!keyword) {
return str
} else {
let unicodes = ''
for (let i of Array.from(keyword)) {
unicodes += unicode(i) + "|"
}
const rule = '(' + unicodes + ')'
const reg = new RegExp(rule, 'gi')
return str ? str.replace(reg, (matchValue) =>
`<span style="color:${lightColor}">${matchValue}</span>`
) : ''
}
}
/**
* 转换为 unicode
*/
const unicode = (str: string) => {
var value = ''
for (var i = 0; i < str.length; i++) {
value += '\\u' + leftZero4(parseInt(str.charCodeAt(i).toString(16)))
}
return value
}
const leftZero4 = (str: string) => {
if (str != null && str != '' && str != 'undefined') {
if (str.length == 2) {
return '00' + str
}
}
return str
}
/**
* 数据去重 只显示一次 减免 劵等
*/
const getPromotion = (item: any) => {
if (item.promotionMap) {
let array: string[] = []
Object.keys(item.promotionMap).forEach((child) => {
if (!array.includes(child.split("-")[0])) {
array.push(child.split("-")[0])
}
})
return array
}
}
/**
* 跳转到商品详情
*/
const navigateToDetailPage = (item: any) => {
uni.navigateTo({
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
})
}
/**
* 跳转到店铺详情
*/
const navigateToStoreDetailPage = (item: any) => {
uni.navigateTo({
url: `/pages/product/shopPage?id=${item.storeId}`,
})
}
return {
lightSearchStr,
getPromotion,
navigateToDetailPage,
navigateToStoreDetailPage,
}
}

View File

@@ -1,69 +0,0 @@
<template>
</template>
<script>
export default {
methods: {
// 高亮显示搜索内容
lightSearchStr(keyword, str) {
if (!keyword) {
return str
} else {
let unicodes = '';
for (let i of Array.from(keyword)) {
unicodes += this.unicode(i) + "|"
}
const rule = '(' + unicodes + ')'
const reg = new RegExp(rule, 'gi');
return str ? str.replace(reg, matchValue =>
`<span style="color:${this.lightColor}">${matchValue}</span>`
) : ''
}
},
// 转换为unicode
unicode(str) {
var value = '';
for (var i = 0; i < str.length; i++) {
value += '\\u' + this.left_zero_4(parseInt(str.charCodeAt(i)).toString(16));
}
return value;
},
left_zero_4(str) {
if (str != null && str != '' && str != 'undefined') {
if (str.length == 2) {
return '00' + str;
}
}
return str;
},
// 数据去重一下 只显示一次 减免 劵 什么的
getPromotion(item) {
if (item.promotionMap) {
let array = [];
Object.keys(item.promotionMap).forEach((child) => {
if (!array.includes(child.split("-")[0])) {
array.push(child.split("-")[0]);
}
});
return array;
}
},
// 跳转到商品详情
navigateToDetailPage(item) {
uni.navigateTo({
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
});
},
// 跳转地址
navigateToStoreDetailPage(item) {
uni.navigateTo({
url: `/pages/product/shopPage?id=${item.storeId}`,
});
},
},
}
</script>
<style lang='scss' scoped>
</style>

View File

@@ -5,7 +5,7 @@
<view v-for="(item, index) in res" :key="index" class="goods-item">
<view class="image-wrapper" @click="navigateToDetailPage(item)">
<u-image :src="item.thumbnail" width="100%" height='330rpx' mode="aspectFit">
<template #loading><u-loading></u-loading></template>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</view>
<view class="goods-detail">
@@ -51,7 +51,7 @@
<div class="flex goods-col">
<div class="goods-img" @click="navigateToDetailPage(item)">
<u-image width="230rpx" mode="aspectFit" border-radius='16' height="230rpx" :src="item.thumbnail">
<template #loading><u-loading></u-loading></template>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</div>
<div class="goods-detail">
@@ -94,114 +94,32 @@
</view>
</template>
<script>
import commonTpl from '@/components/m-goods-list/common'
export default {
data() {
return {
lightColor: this.$mainColor
}
},
mixins: [commonTpl],
props: {
// 遍历的数据
res: {
type: Array,
default: () => {
return []
}
},
// 一行两列还是一行一列显示
type: {
type: String,
default: 'twoColumns',
validator() {
return ['twoColumns', 'oneColumns']
}
},
storeName: {
type: Boolean,
default: true
},
tabBarGap: {
type: Boolean,
default: true
},
keyword: {
type: null,
default: ''
}
<script setup lang="ts">
import { watch } from 'vue'
import { useGoodsListCommon } from './common'
import { goodsFormatPrice } from '@/utils/filters.js'
},
watch: {
keyword(val) {
if (val) {
this.lightSearchStr(val)
}
}
},
methods: {
const { lightSearchStr, getPromotion, navigateToDetailPage, navigateToStoreDetailPage } = useGoodsListCommon()
// 高亮显示搜索内容
lightSearchStr(keyword, str) {
if (!keyword) {
return str
} else {
let unicodes = '';
for (let i of Array.from(keyword)) {
unicodes += this.unicode(i) + "|"
}
const rule = '(' + unicodes + ')'
const reg = new RegExp(rule, 'gi');
return str ? str.replace(reg, matchValue =>
`<span style="color:${this.lightColor}">${matchValue}</span>`
) : ''
}
},
// 转换为unicode
unicode(str) {
var value = '';
for (var i = 0; i < str.length; i++) {
value += '\\u' + this.left_zero_4(parseInt(str.charCodeAt(i)).toString(16));
}
return value;
},
left_zero_4(str) {
if (str != null && str != '' && str != 'undefined') {
if (str.length == 2) {
return '00' + str;
}
}
return str;
},
// 数据去重一下 只显示一次 减免 劵 什么的
getPromotion(item) {
if (item ? item.promotionMap : item.promotionMap) {
const fieldList = item ? item.promotionMap : item.promotionMap
let array = [];
Object.keys(fieldList).forEach((child) => {
if (!array.includes(child.split("-")[0])) {
array.push(child.split("-")[0]);
}
});
return array;
}
},
// 跳转到商品详情
navigateToDetailPage(item) {
uni.navigateTo({
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
});
},
// 跳转地址
navigateToStoreDetailPage(item) {
uni.navigateTo({
url: `/pages/product/shopPage?id=${item.storeId}`,
});
},
}
const props = withDefaults(defineProps<{
res?: any[]
type?: string
storeName?: boolean
tabBarGap?: boolean
keyword?: string | null
}>(), {
res: () => [],
type: 'twoColumns',
storeName: true,
tabBarGap: true,
keyword: ''
})
watch(() => props.keyword, (val) => {
if (val) {
lightSearchStr(val as string, '')
}
})
</script>
<style lang='scss' scoped>
@@ -212,6 +130,7 @@
.goods-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin: 10rpx 20rpx 284rpx;
width: calc(100% - 40rpx);
box-sizing: border-box;
@@ -219,21 +138,17 @@
&.goods-list--embedded {
margin-bottom: 20rpx;
}
>.goods-item {
background-color: #ffffff;
display: flex;
border-radius: 16rpx;
flex-direction: column;
width: calc(50% - 30rpx);
width: calc(50% - 10rpx);
margin-bottom: 20rpx;
padding-bottom: 20rpx;
&:nth-child(2n + 1) {
margin-right: 20rpx;
}
.image-wrapper {
width: 100%;
height: 330rpx;
@@ -242,12 +157,12 @@
padding: 0;
}
}
.count-config,
.store-seller-name {
font-size: $font-sm;
}
.store-seller-name {
color: #666;
display: flex;
@@ -322,7 +237,7 @@
.goods-detail {
margin: 0 20rpx;
>.title {
font-size: $font-base;
color: $font-color-dark;
@@ -334,7 +249,7 @@
-webkit-line-clamp: 2;
overflow: hidden;
}
.promotion {
margin-top: 4rpx;
display: flex;
@@ -344,10 +259,10 @@
color: $light-color;
margin-right: 10rpx;
padding: 0 4rpx;
border-radius: 2rpx;
border-radius: 2px;
}
}
.store-seller-name {
color: #666;
display: flex;
@@ -409,7 +324,7 @@
padding-right: 10rpx;
font-size: 24rpx;
color: $font-color-light;
>.price {
font-size: 26rpx;
line-height: 1;
@@ -421,6 +336,5 @@
}
}
}
}
</style>

View File

@@ -4,7 +4,7 @@
<div class="flex goods-col">
<div class="goods-img">
<u-image width="230rpx" mode="aspectFit" border-radius='16' height="230rpx" :src="item.goodsImage || item.thumbnail">
<template #loading><u-loading></u-loading></template>
<template #loading><u-loading-icon></u-loading-icon></template>
</u-image>
</div>
<div class="goods-detail">
@@ -43,43 +43,33 @@
</div>
</template>
<script>
import commonTpl from '@/components/m-goods-list/common'
export default {
data() {
return {
lightColor: this.$mainColor,
}
},
mixins: [commonTpl],
props: {
// 遍历的数据
res: {
type: Array,
default: () => {
return []
}
},
type:{
type:null,
default:""
}
},
methods: {
// 跳转到商品详情
navigateToDetailPage(item) {
if(this.type == 'kanJia'){
uni.navigateTo({
url: `/pages/promotion/bargain/detail?id=${item.id}`,
});
return
}
uni.navigateTo({
url: `/pages/product/goods?id=${item.skuId}&goodsId=${item.goodsId}`,
});
},
}
<script setup lang="ts">
import { useGoodsListCommon } from './common'
import { goodsFormatPrice } from '@/utils/filters.js'
const { navigateToDetailPage } = useGoodsListCommon()
const props = withDefaults(defineProps<{
res?: any[]
type?: string | null
}>(), {
res: () => [],
type: ''
})
// 跳转到商品详情(覆盖公共方法,增加砍价跳转)
const navigateToDetailPageOverride = (item: any) => {
if(props.type == 'kanJia'){
uni.navigateTo({
url: `/pages/promotion/bargain/detail?id=${item.id}`,
})
return
}
navigateToDetailPage({
id: item.skuId,
goodsId: item.goodsId,
})
}
</script>
<style lang='scss' scoped>
@@ -87,72 +77,72 @@
width: 152rpx;
height: 108rpx;
}
.flex-j-sb {
.flex-j-sb {
width: 100%;
}
.goods-row {
background: #fff;
padding: 16rpx;
>.goods-col {
display: flex;
>.goods-img {
overflow: hidden;
flex: 4;
}
>.goods-detail {
flex: 7;
}
}
}
.goods-detail {
margin: 0 20rpx;
>.title {
font-size: $font-base;
color: $font-color-dark;
line-height: 1.5;
height: 86rpx;
padding: 10rpx 0 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.count-config {
padding: 5rpx 0;
color: #666;
display: flex;
font-size: 24rpx;
letter-spacing:2rpx;
padding-left: 10rpx;
.count-config-text {
font-size: 24rpx;
}
}
}
.price-box {
margin-top: 10rpx;
.goods-row {
background: #fff;
padding: 16rpx;
>.goods-col {
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 10rpx;
font-size: 24rpx;
color: $font-color-light;
>.price {
font-size: 26rpx;
line-height: 1;
color: $main-color;
font-weight: bold;
.price-int {
font-size: 48rpx;
}
>.goods-img {
overflow: hidden;
flex: 4;
}
>.goods-detail {
flex: 7;
}
}
}
.goods-detail {
margin: 0 20rpx;
>.title {
font-size: $font-base;
color: $font-color-dark;
line-height: 1.5;
height: 86rpx;
padding: 10rpx 0 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.count-config {
padding: 5rpx 0;
color: #666;
display: flex;
font-size: 24rpx;
letter-spacing:2rpx;
padding-left: 10rpx;
.count-config-text {
font-size: 24rpx;
}
}
}
.price-box {
margin-top: 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 10rpx;
font-size: 24rpx;
color: $font-color-light;
>.price {
font-size: 26rpx;
line-height: 1;
color: $main-color;
font-weight: bold;
.price-int {
font-size: 48rpx;
}
}
}
</style>

View File

@@ -1,73 +1,63 @@
<template>
<div>
<div class="goods-recommend">{{title ? `--${title}-- `:''}}</div>
<view>
<view class="goods-recommend">{{title ? `--${title}-- `:''}}</view>
<goodsTemplate :res='goodsList' />
</div>
</view>
</template>
<script>
import goodsTemplate from '@/components/m-goods-list/list'
import { getGoodsList } from "@/api/goods.js";
export default {
data() {
return {
goodsList: [],
params: {
pageNumber: 1,
},
};
},
props: {
title: {
type: String,
default: "",
},
pageSize: {
type: null,
default: 12,
},
categoryId: {
type: null,
default: "",
},
storeId: {
type: null,
default: "",
},
},
components:{goodsTemplate},
mounted() {
this.initGoods();
},
methods: {
/**
* 初始化商品
*/
async initGoods() {
let submit = JSON.parse(
JSON.stringify(
Object.assign(this.params, {
pageSize: this.pageSize,
categoryId: this.categoryId,
storeId: this.storeId,
})
)
);
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import goodsTemplate from '@/components/m-goods-list/list.vue'
import { getGoodsList } from "@/api/goods.js"
Object.keys(submit).map((key) => {
if (!submit[key] || submit[key].length == 0) {
delete submit[key];
}
});
let goodsList = await getGoodsList(submit);
this.goodsList.push(...goodsList.data.result.records);
},
handleClick(item) {
uni.navigateTo({
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
});
},
},
};
const props = withDefaults(defineProps<{
title?: string
pageSize?: number | null
categoryId?: string | null
storeId?: string | null
}>(), {
title: '',
pageSize: 12,
categoryId: '',
storeId: ''
})
const goodsList = ref<any[]>([])
const params = ref({
pageNumber: 1,
})
onMounted(() => {
initGoods()
})
/**
* 初始化商品
*/
const initGoods = async () => {
let submit = JSON.parse(
JSON.stringify(
Object.assign(params.value, {
pageSize: props.pageSize,
categoryId: props.categoryId,
storeId: props.storeId,
})
)
)
Object.keys(submit).map((key) => {
if (!submit[key] || (submit[key] as any).length == 0) {
delete submit[key]
}
})
let res = await getGoodsList(submit)
goodsList.value.push(...(res.data.result.records as any[]))
}
const handleClick = (item: any) => {
uni.navigateTo({
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
})
}
</script>
<style scoped lang="scss">
@@ -130,4 +120,4 @@ $w_94: 94%;
}
}
}
</style>
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="index">
<view class="index">
<view v-model="show" class="slot-content">
<image @click="downLoad()" class="img" :src="imgUrl" />
<div class="canvas-hide">
@@ -11,181 +11,153 @@
<!-- #endif -->
</div>
</view>
</div>
</view>
</template>
<script>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance } from 'vue'
// 引入绘制插件
import DrawPoster from "@/js_sdk/u-draw-poster";
import DrawPoster from "@/js_sdk/u-draw-poster"
// 生成二维码
import uQRCode from '@/components/Sansnn-uQRCode/uqrcode.js';
import logoImg from "@/pages/passport/static/logo-title.png";
import myFaceImg from "@/pages/passport/static/missing-face.png";
import uQRCode from '@/components/Sansnn-uQRCode/uqrcode.js'
import logoImg from "@/pages/passport/static/logo-title.png"
import myFaceImg from "@/pages/passport/static/missing-face.png"
export default {
data: () => ({
imgUrl: "", //绘制出来的图片路径
show: false, //是否展示模态框
dp: {}, //绘制的dp对象用于存储绘制等一些方法。
logo: logoImg, // 本地logo地址
myFace: myFaceImg, // 本地默认头像
sharingLink: '', // 二维码链接
}),
props: {
/**
* 父级传参的数据
*/
res: {
type: null,
default: "",
},
},
onUnload() {},
onReady() {},
methods: {
/** 解决微信小程序中图片模糊问题 */
// #ifdef MP-WEIXIN
st2: (size) => size * 2,
// #endif
const instance = getCurrentInstance()
// #ifndef MP-WEIXIN
st2: (size) => size,
// #endif
const props = defineProps<{
res?: any
}>()
/** 保存图片 */
downLoad() {
uni.saveImageToPhotosAlbum({
filePath: this.imgUrl,
success: function () {
uni.showToast({title: "保存成功!",icon: "none",});
},
fail: function () {
uni.showToast({title: "保存失败,请稍后重试!",icon: "none",});
},
});
},
/** 创建canvas */
async init() {
this.show = true;
this.dp = await DrawPoster.build({
selector: "canvas",
componentThis: this,
loading: true,
debugging: true,
});
let dp = this.dp;
// #ifdef MP-WEIXIN
// 用于微信小程序中画布错乱问题
dp.canvas.width = this.st2(560);
dp.canvas.height = this.st2(800);
// #endif
this.showQRCode(dp);
},
/** 生成二维码 */
async showQRCode(dp){
await uQRCode.make({
canvasId: 'canvas', // canvas画布
componentInstance: this,
text: this.res.bottom.code, // 二维码内容
size: 130, // 二维码大小
margin: 5, // 二维码内边距
backgroundColor: '#ffffff', // 二维码背景颜色
foregroundColor: '#000000', // !二维码色块颜色
fileType: 'jpg',
errorCorrectLevel: 0, // <== 关键 容错率M:0,L:1,H:2,Q:3,
// errorCorrectLevel: uQRCode.errorCorrectLevel.M, // <== 关键 容错率M:0,L:1,H:2,Q:3,
success: res => {
this.sharingLink = res; // res => 图片路径
// 对画布进行绘制
this.draw(dp);
},
fail: res => {
// console.log('失败',res)
}
const imgUrl = ref("")
const show = ref(false)
const dp = ref<any>({})
const logo = logoImg
const myFace = myFaceImg
const sharingLink = ref('')
/** 解决微信小程序中图片模糊问题 */
// #ifdef MP-WEIXIN
const st2 = (size: number) => size * 2
// #endif
// #ifndef MP-WEIXIN
const st2 = (size: number) => size
// #endif
/** 保存图片 */
const downLoad = () => {
uni.saveImageToPhotosAlbum({
filePath: imgUrl.value,
success: function () {
uni.showToast({title: "保存成功!",icon: "none"})
},
fail: function () {
uni.showToast({title: "保存失败,请稍后重试!",icon: "none"})
},
})
}
/** 创建canvas */
const init = async () => {
show.value = true
dp.value = await DrawPoster.build({
selector: "canvas",
componentThis: instance?.proxy,
loading: true,
debugging: true,
})
let dpVal = dp.value
// #ifdef MP-WEIXIN
// 用于微信小程序中画布错乱问题
dpVal.canvas.width = st2(560)
dpVal.canvas.height = st2(800)
// #endif
await showQRCode(dpVal)
}
/** 生成二维码 */
const showQRCode = async (dp: any) => {
await uQRCode.make({
canvasId: 'canvas',
componentInstance: instance?.proxy,
text: props.res.bottom.code,
size: 130,
margin: 5,
backgroundColor: '#ffffff',
foregroundColor: '#000000',
fileType: 'jpg',
errorCorrectLevel: 0,
success: (res: any) => {
sharingLink.value = res
draw(dp)
},
fail: (_res: any) => {
// console.log('失败',res)
}
})
}
const draw = async (dp: any) => {
const { face, nickName, desc } = props.res.memberInfo
const { width, height, background, title } = props.res.container
const { img, price } = props.res.bottom
/** 绘制背景 */
await dp.draw((ctx: any) => {
ctx.fillStyle = background
ctx.fillRoundRect(st2(0), st2(0), st2(width), st2(height), st2(12))
ctx.clip()
})
/** 绘制圆角矩形 */
await dp.draw(async (ctx: any) => {
ctx.fillStyle = "#ffffff"
ctx.strokeStyle = "#ffffff"
ctx.lineWidth = st2(1)
ctx.fillRoundRect(30, 150, 500, 620, 0)
})
/** 绘制图片 */
dp.draw(async (ctx: any) => {
await Promise.all([
ctx.drawImage(face ? face : myFace, st2(30), st2(30), st2(90), st2(90)),
ctx.drawImage(img, st2(60), st2(170), st2(440), st2(440)),
ctx.drawImage(sharingLink.value, st2(375), st2(625), st2(130), st2(130))
])
})
/** 绘制顶部文字(昵称 简述) */
await dp.draw((ctx: any) => {
ctx.fillStyle = "#666"
ctx.font = `${st2(24)}px PingFang SC`
ctx.fillText(nickName, st2(150), st2(65))
ctx.fillStyle = "#666"
ctx.font = `${st2(24)}px PingFang SC`
ctx.fillText(desc, st2(150), st2(105))
})
/** 绘制中间文字(商品名称 价格) */
await dp.draw((ctx: any) => {
ctx.fillStyle = "#333"
ctx.font = `bold ${st2(24)}px PingFang SC`
ctx.textAlign = "left"
ctx.fillWarpText({
text: title,
lineHeight: st2(32),
maxWidth: st2(280),
x: st2(60),
y: st2(710),
layer: 2,
})
},
async draw(dp) {
const { face, nickName, desc } = this.res.memberInfo;
const { width, height, background, title } = this.res.container;
const { code, img, price } = this.res.bottom;
ctx.fillStyle = "#ff3c2a"
ctx.font = `${st2(38)}px PingFang SC`
ctx.textAlign = "left"
ctx.fillText(price, st2(60), st2(665))
})
/** 绘制背景 */
await dp.draw((ctx) => {
ctx.fillStyle = background;
ctx.fillRoundRect( this.st2(0), this.st2(0), this.st2(width), this.st2(height), this.st2(12));
ctx.clip();
});
/** 绘制圆角矩形 */
await dp.draw(async (ctx)=>{
// 设置矩形色彩
ctx.fillStyle = "#ffffff";
// 设置图形轮廓的颜色。默认情况下线条和填充颜色都是黑色CSS 颜色值 #000000
ctx.strokeStyle = "#ffffff";
// 这个属性设置当前绘线的粗细。属性值必须为正数。描述线段宽度的数字。 0、 负数、 Infinity 和 NaN 会被忽略。默认值是1.0。
ctx.lineWidth = this.st2(1);
// 进行绘制
ctx.fillRoundRect(30, 150, 500, 620, 0);
// ctx.strokeRoundRect(30, 150, 500, 620, 0);
// ctx.strokeRect(30, 150, 500, 620, 0);
})
/** 绘制图片 */
dp.draw(async (ctx) => {
await Promise.all([
// 绘制头像face
ctx.drawImage(face?face:this.myFace, this.st2(30), this.st2(30), this.st2(90), this.st2(90)),
// 绘制Logo
// ctx.drawImage( this.logo, this.st2(175), this.st2(0), this.st2(256), this.st2(144)),
// 中间图片
ctx.drawImage(img, this.st2(60), this.st2(170), this.st2(440), this.st2(440)),
// 二维码
ctx.drawImage(this.sharingLink, this.st2(375), this.st2(625), this.st2(130), this.st2(130))
]);
});
/** 绘制顶部文字(昵称 简述) */
await dp.draw((ctx) => {
ctx.fillStyle = "#666";
ctx.font = `${this.st2(24)}px PingFang SC`;
ctx.fillText(nickName, this.st2(150), this.st2(65));
ctx.fillStyle = "#666";
ctx.font = `${this.st2(24)}px PingFang SC`;
ctx.fillText(desc, this.st2(150), this.st2(105));
});
/** 绘制中间文字(商品名称 价格) */
await dp.draw((ctx) => {
ctx.fillStyle = "#333";
ctx.font = `bold ${this.st2(24)}px PingFang SC`;
ctx.textAlign = "left";
ctx.fillWarpText({
text: title,
lineHeight: this.st2(32),
maxWidth: this.st2(280),
x: this.st2(60),
y: this.st2(710),
layer: 2,
});
ctx.fillStyle = "#ff3c2a";
ctx.font = `${this.st2(38)}px PingFang SC`;
ctx.textAlign = "left";
ctx.fillText(price, this.st2(60), this.st2(665));
});
/** 绘制底部文字 */
// await dp.draw((ctx) => {
// ctx.fillStyle = "#666";
// ctx.font = `${this.st2(24)}px PingFang SC`;
// ctx.fillText("长按图片,识别二维码", this.st2(200), this.st2(866));
// ctx.fillStyle = "#666";
// ctx.font = `${this.st2(24)}px PingFang SC`;
// ctx.fillText("查看商品详情", this.st2(200), this.st2(900));
// });
// 绘制生成本地地址
this.imgUrl = await dp.createImagePath();
},
},
// 绘制生成本地地址
imgUrl.value = await dp.createImagePath()
}
async mounted() {
this.init();
},
};
onMounted(() => {
init()
})
</script>
<style lang="scss" scoped>

View File

@@ -28,138 +28,122 @@
</view>
</template>
<script>
export default {
props: {
mode: {
value: Number,
default: 1,
},
//HM修改 定义默认搜索关键词(水印文字)
placeholder: {
value: String,
default: "请输入搜索内容",
},
value: {
type: String,
default: "",
},
// 默认半径为60
radius: {
value: String,
default: 60,
},
// 是否获取焦点
isFocusVal: {
value: Boolean,
default: true,
},
showClear: {
type: Boolean,
default: true,
},
},
data() {
return {
isShowSeachGoods: false, //是否显示查询的商品
active: false, //是否选中
inputVal: "", //Input中内容
isDelShow: false, //是否显示右侧删除icon
isFocus: false, //是否获取焦点
switchLayout: true, //切换当前商品的布局,默认为两列
};
},
mounted() {
this.isFocus = this.isFocusVal;
},
methods: {
//
out() {
uni.reLaunch({
url: "/pages/tabbar/home/index",
});
},
// 切换排列顺序
handelListClass() {
this.switchLayout = !this.switchLayout;
this.$emit("SwitchType");
},
//HM修改 触发组件confirm事件
triggerConfirm() {
this.$emit("confirm", false);
uni.hideKeyboard();
},
//HM修改 触发组件input事件
inputChange(event) {
var keyword = event.detail.value;
this.$emit("input", keyword);
if (this.inputVal) {
this.isDelShow = true;
}
},
focus() {
this.active = true;
//HM修改 增加获取焦点判断
if (this.inputVal) {
this.isDelShow = true;
}
},
blur() {
this.isFocus = false;
if (!this.inputVal) {
this.active = false;
}
},
clear() {
//HM修改 收起键盘
uni.hideKeyboard();
this.isFocus = false;
this.inputVal = "";
this.active = false;
//HM修改 清空内容时候触发组件input
this.$emit("input", "");
//this.$emit('search', '');//HM修改 清空内容时候不进行搜索
},
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
/**
* 回退到上一级
*/
onClickLeft() {
const paths = getCurrentPages();
console.log(paths)
if(paths.length > 1){
uni.navigateBack();
}else{
uni.switchTab({
url:"/pages/tabbar/home/index"
})
}
},
const props = withDefaults(defineProps<{
mode?: number
placeholder?: string
value?: string
radius?: string
isFocusVal?: boolean
showClear?: boolean
}>(), {
mode: 1,
placeholder: "请输入搜索内容",
value: "",
radius: "60",
isFocusVal: true,
showClear: true,
})
/**
* 内容为空时,输入默认关键字
*/
search() {
if (!this.inputVal) {
if (this.searchName == "取消") {
uni.hideKeyboard();
this.isFocus = false;
this.active = false;
return;
}
}
this.$emit("search", this.inputVal ? this.inputVal : this.placeholder);
},
},
watch: {
/**
* 监听当前是否有值 是否显示清除图标
*/
inputVal(newVal) {
newVal ? (this.isDelShow = true) : (this.isDelShow = false);
},
},
};
const emit = defineEmits(['confirm', 'input', 'search', 'SwitchType'])
const isShowSeachGoods = ref(false)
const active = ref(false)
const inputVal = ref("")
const isDelShow = ref(false)
const isFocus = ref(false)
const switchLayout = ref(true)
onMounted(() => {
isFocus.value = props.isFocusVal
})
//
const out = () => {
uni.reLaunch({
url: "/pages/tabbar/home/index",
})
}
// 切换排列顺序
const handelListClass = () => {
switchLayout.value = !switchLayout.value
emit("SwitchType")
}
//HM修改 触发组件confirm事件
const triggerConfirm = () => {
emit("confirm", false)
uni.hideKeyboard()
}
//HM修改 触发组件input事件
const inputChange = (event: any) => {
var keyword = event.detail.value
emit("input", keyword)
if (inputVal.value) {
isDelShow.value = true
}
}
const focus = () => {
active.value = true
//HM修改 增加获取焦点判断
if (inputVal.value) {
isDelShow.value = true
}
}
const blur = () => {
isFocus.value = false
if (!inputVal.value) {
active.value = false
}
}
const clear = () => {
//HM修改 收起键盘
uni.hideKeyboard()
isFocus.value = false
inputVal.value = ""
active.value = false
//HM修改 清空内容时候触发组件input
emit("input", "")
//this.$emit('search', '');//HM修改 清空内容时候不进行搜索
}
/**
* 回退到上一级
*/
const onClickLeft = () => {
const paths = getCurrentPages()
console.log(paths)
if(paths.length > 1){
uni.navigateBack()
}else{
uni.switchTab({
url:"/pages/tabbar/home/index"
})
}
}
/**
* 内容为空时,输入默认关键字
*/
const search = () => {
if (!inputVal.value) {
if (false) { // searchName == "取消" - 兼容旧逻辑
uni.hideKeyboard()
isFocus.value = false
active.value = false
return
}
}
emit("search", inputVal.value ? inputVal.value : props.placeholder)
}
/**
* 监听当前是否有值 是否显示清除图标
*/
watch(inputVal, (newVal) => {
newVal ? (isDelShow.value = true) : (isDelShow.value = false)
})
</script>
<style lang="scss" scoped>

View File

@@ -28,118 +28,125 @@
</view>
</u-popup>
</template>
<script>
import { h5Copy } from "@/js_sdk/h5-copy/h5-copy.js";
import configs from "@/config/config";
import mpShare from "@/utils/mpShare.js";
import { setClipboard } from "@/utils/filters.js";
export default {
mixins: [mpShare],
data() {
return {
configs,
show: true,
list: [
{
color: "#04BE02",
title: "微信好友",
icon: "weixin-fill",
type: 0,
},
{
color: "#04BE02",
title: "朋友圈",
icon: "weixin-circle-fill",
type: 1,
},
],
};
<script setup lang="ts">
import { ref } from 'vue'
import configs from "@/config/config"
import mpShare from "@/utils/mpShare.js"
import { setClipboard } from "@/utils/filters.js"
// mixin 处理mpShare 提供小程序分享能力
mpShare
const props = defineProps<{
thumbnail?: string
goodsName?: string
type?: string
goodsId?: string | number
link?: string
}>()
const emit = defineEmits(['close'])
const show = ref(true)
const config = configs
const list = ref([
{
color: "#04BE02",
title: "微信好友",
icon: "weixin-fill",
type: 0,
},
// 图片缩略图、 商品名称 、 typegoods,shop,pintuan) 拼团商品分享以及店铺分享
props: ["thumbnail", "goodsName", "type", "goodsId", "link"],
methods: {
close() {
this.$emit("close");
},
weChatShare(){
this.$u.mpShare = {
title: this.shareTitle(), // 默认为小程序名称,可自定义
path: '', // 默认为当前页面路径一般无需修改QQ小程序不支持
// 分享图标,路径可以是本地文件路径、代码包文件路径或者网络图片路径。
// 支持PNG及JPG默认为当前页面的截图
imageUrl: this.thumbnail ||''
}
},
// h5复制链接
// #ifdef H5
copyLink() {
let content;
if (this.link) {
content = this.configs.shareLink + this.link;
} else {
content =
this.configs.shareLink +
getCurrentPages()[getCurrentPages().length - 1].__page__.fullPath;
}
setClipboard(content)
},
// #endif
shareTitle() {
let shareTitle;
if (this.type == "goods") {
shareTitle = `[好友推荐]${this.goodsName}快来跟我一起看看吧`;
} else if (this.type == "shops") {
shareTitle = `[好友发现]${this.goodsName}快来跟我一起看看吧`;
} else if (this.type == "pintuan") {
shareTitle = `[好友邀请]${this.goodsName}快来跟我一起抢购吧!`;
} else if (this.type == "kanjia") {
shareTitle = `[好友邀请]请快来帮我砍一刀${this.goodsName}`;
}
return shareTitle;
},
// #ifdef APP-PLUS
handleShare(val) {
console.log("12312312")
if (val.type <= 1) {
let scene; // "WXSenceTimeline 朋友圈 WXSceneSession 微信好友"
val.type == 1
? (scene = "WXSenceTimeline")
: (scene = "WXSceneSession");
uni.share({
provider: "weixin",
scene: scene,
href: configs.shareLink + this.link,
imageUrl: this.thumbnail,
type: 0,
summary: this.goodsName,
title: this.shareTitle(),
success: function (res) {
uni.showToast({
title: "分享成功!",
duration: 2000,
icon: "none",
});
this.$emit("close");
},
fail: function (err) {
uni.showToast({
title: "分享失败!",
duration: 2000,
icon: "none",
});
this.$emit("close");
},
});
}
},
// #endif
{
color: "#04BE02",
title: "朋友圈",
icon: "weixin-circle-fill",
type: 1,
},
};
])
const close = () => {
emit("close")
}
const weChatShare = () => {
// @ts-ignore
const proxy = getCurrentInstance()?.proxy
if (proxy) {
// @ts-ignore
proxy.$u.mpShare = {
title: shareTitle(),
path: '',
imageUrl: props.thumbnail || ''
}
}
}
// h5复制链接
// #ifdef H5
const copyLink = () => {
let content: string
if (props.link) {
content = config.shareLink + props.link
} else {
content =
config.shareLink +
getCurrentPages()[getCurrentPages().length - 1].__page__.fullPath
}
setClipboard(content)
}
// #endif
const shareTitle = (): string => {
let shareTitle: string = ''
if (props.type == "goods") {
shareTitle = `[好友推荐]${props.goodsName}快来跟我一起看看吧`
} else if (props.type == "shops") {
shareTitle = `[好友发现]${props.goodsName}快来跟我一起看看吧`
} else if (props.type == "pintuan") {
shareTitle = `[好友邀请]${props.goodsName}快来跟我一起抢购吧!`
} else if (props.type == "kanjia") {
shareTitle = `[好友邀请]请快来帮我砍一刀${props.goodsName}`
}
return shareTitle
}
// #ifdef APP-PLUS
const handleShare = (val: any) => {
console.log("12312312")
if (val.type <= 1) {
let scene: string // "WXSenceTimeline 朋友圈 WXSceneSession 微信好友"
val.type == 1
? (scene = "WXSenceTimeline")
: (scene = "WXSceneSession")
uni.share({
provider: "weixin",
scene: scene,
href: config.shareLink + props.link,
imageUrl: props.thumbnail,
type: 0,
summary: props.goodsName,
title: shareTitle(),
success: function () {
uni.showToast({
title: "分享成功!",
duration: 2000,
icon: "none",
})
emit("close")
},
fail: function () {
uni.showToast({
title: "分享失败!",
duration: 2000,
icon: "none",
})
emit("close")
},
})
}
}
// #endif
</script>
<style lang="scss" scoped>
@import "./mp-share.scss";
@@ -175,4 +182,4 @@ export default {
}
}
}
</style>
</style>

View File

@@ -1,27 +1,21 @@
<template>
<div>
<view>
<u-popup v-model:show="show" mode="bottom" height="800rpx" border-radius="14">
<div class="wrapper">
<view class="wrapper">
<view class="down-goods-tips">该商品已下架</view>
<scroll-view scroll-y="true" style="height: 670rpx">
<goodsRecommend title="其他商品" />
</scroll-view>
</div>
</view>
</u-popup>
</div>
</view>
</template>
<script>
import goodsRecommend from "@/components/m-goods-recommend/index.vue";
<script setup lang="ts">
import { ref } from 'vue'
import goodsRecommend from "@/components/m-goods-recommend/index.vue"
export default {
data() {
return {
show: true, // 是否显示
};
},
components: { goodsRecommend },
};
const show = ref(true)
</script>
<style lang="scss" scoped>

View File

@@ -10,198 +10,175 @@
</view>
</template>
<script>
export default {
props: {
maskBg: {
type: String,
default: "rgba(0,0,0,0)",
},
placement: {
type: String,
default: "default", //default top-start top-end bottom-start bottom-end
},
direction: {
type: String,
default: "column", //column row
},
x: {
type: Number,
default: 0,
},
y: {
type: Number,
default: 0,
},
value: {
type: Boolean,
default: false,
},
popData: {
type: Array,
default: () => [],
},
theme: {
type: String,
default: "light", //light dark
},
dynamic: {
type: Boolean,
default: false,
},
gap: {
type: Number,
default: 20,
},
triangle: {
type: Boolean,
default: true,
},
},
data() {
return {
popupsTop: "0rpx",
popupsLeft: "0rpx",
show: false,
dynPlace: "",
};
},
mounted() {
this.popupsPosition();
},
methods: {
tapMask() {
this.$emit("input", !this.value);
},
tapItem(item) {
if (item.disabled) return;
this.$emit("tapPopup", item);
this.$emit("input", !this.value);
},
getStatusBar() {
const windowInfo = uni.getWindowInfo();
// #ifdef H5
return Promise.resolve(
(windowInfo.statusBarHeight || 0) + (windowInfo.windowTop || 0)
);
// #endif
// #ifndef H5
return Promise.resolve(windowInfo.statusBarHeight || 0);
// #endif
},
async popupsPosition() {
let statusBar = await this.getStatusBar();
let promise = new Promise((resolve, reject) => {
let popupsDom = uni.createSelectorQuery().in(this).select(".popups");
popupsDom
.fields(
{
size: true,
},
(data) => {
if (!data) {
resolve();
return;
}
let width = data.width;
let height = data.height;
<script setup lang="ts">
import { ref, watch, onMounted, getCurrentInstance } from 'vue'
const props = withDefaults(defineProps<{
maskBg?: string
placement?: string
direction?: string
x?: number
y?: number
modelValue?: boolean
popData?: any[]
theme?: string
dynamic?: boolean
gap?: number
triangle?: boolean
}>(), {
maskBg: "rgba(0,0,0,0)",
placement: "default",
direction: "column",
x: 0,
y: 0,
modelValue: false,
popData: () => [],
theme: "light",
dynamic: false,
gap: 20,
triangle: true,
})
let y = this.dynamic
? this.dynamicGetY(this.y, this.gap)
: this.transformRpx(this.y);
const emit = defineEmits(['update:modelValue', 'tapPopup'])
let x = this.dynamic
? this.dynamicGetX(this.x, this.gap)
: this.transformRpx(this.x);
const instance = getCurrentInstance()
// #ifdef H5
y = this.dynamic
? this.y + statusBar
: this.transformRpx(this.y + statusBar);
// #endif
const popupsTop = ref("0rpx")
const popupsLeft = ref("0rpx")
const show = ref(false)
const dynPlace = ref("")
this.dynPlace =
this.placement == "default"
? this.getPlacement(x, y)
: this.placement;
onMounted(() => {
popupsPosition()
})
switch (this.dynPlace) {
case "top-start":
this.popupsTop = `${y + 9}rpx`;
this.popupsLeft = `${x - 15}rpx`;
break;
case "top-end":
this.popupsTop = `${y + 9}rpx`;
this.popupsLeft = `${x + 15 - width}rpx`;
break;
case "bottom-start":
this.popupsTop = `${y - 18 - height}rpx`;
this.popupsLeft = `${x - 15}rpx`;
break;
case "bottom-end":
this.popupsTop = `${y - 9 - height}rpx`;
this.popupsLeft = `${x + 15 - width}rpx`;
break;
}
resolve();
}
)
.exec();
});
return promise;
},
getPlacement(x, y) {
const { windowWidth: width, windowHeight: height } = uni.getWindowInfo();
if (x > width / 2 && y > height / 2) {
return "bottom-end";
} else if (x < width / 2 && y < height / 2) {
return "top-start";
} else if (x > width / 2 && y < height / 2) {
return "top-end";
} else if (x < width / 2 && y > height / 2) {
return "bottom-start";
} else if (x > width / 2) {
return "top-end";
} else {
return "top-start";
}
},
dynamicGetY(y, gap) {
const { windowHeight: height } = uni.getWindowInfo();
y = y < gap ? gap : y;
y = height - y < gap ? height - gap : y;
const tapMask = () => {
emit("update:modelValue", !props.modelValue)
}
return y;
},
dynamicGetX(x, gap) {
const { windowWidth: width } = uni.getWindowInfo();
x = x < gap ? gap : x;
x = width - x < gap ? width - gap : x;
return x;
},
transformRpx(params) {
const { screenWidth } = uni.getWindowInfo();
return (params * screenWidth) / 375;
},
},
watch: {
value: {
immediate: true,
handler: async function (newVal, oldVal) {
if (newVal) await this.popupsPosition();
this.show = newVal;
},
},
placement: {
immediate: true,
handler(newVal, oldVal) {
this.dynPlace = newVal;
},
},
},
};
const tapItem = (item: any) => {
if (item.disabled) return
emit("tapPopup", item)
emit("update:modelValue", !props.modelValue)
}
const getStatusBar = () => {
const windowInfo = uni.getWindowInfo()
// #ifdef H5
return Promise.resolve(
(windowInfo.statusBarHeight || 0) + (windowInfo.windowTop || 0)
)
// #endif
// #ifndef H5
return Promise.resolve(windowInfo.statusBarHeight || 0)
// #endif
}
const popupsPosition = async () => {
let statusBar = await getStatusBar()
await new Promise<void>((resolve) => {
let popupsDom = uni.createSelectorQuery().in(instance!.proxy).select(".popups")
popupsDom
.fields(
{
size: true,
},
(data: any) => {
if (!data) {
resolve()
return
}
let width = data.width
let height = data.height
let y = props.dynamic
? dynamicGetY(props.y, props.gap)
: transformRpx(props.y)
let x = props.dynamic
? dynamicGetX(props.x, props.gap)
: transformRpx(props.x)
// #ifdef H5
y = props.dynamic
? props.y + statusBar
: transformRpx(props.y + statusBar)
// #endif
dynPlace.value =
props.placement == "default"
? getPlacement(x, y)
: props.placement
switch (dynPlace.value) {
case "top-start":
popupsTop.value = `${y + 9}rpx`
popupsLeft.value = `${x - 15}rpx`
break
case "top-end":
popupsTop.value = `${y + 9}rpx`
popupsLeft.value = `${x + 15 - width}rpx`
break
case "bottom-start":
popupsTop.value = `${y - 18 - height}rpx`
popupsLeft.value = `${x - 15}rpx`
break
case "bottom-end":
popupsTop.value = `${y - 9 - height}rpx`
popupsLeft.value = `${x + 15 - width}rpx`
break
}
resolve()
}
)
.exec()
})
}
const getPlacement = (x: number, y: number) => {
const { windowWidth: width, windowHeight: height } = uni.getWindowInfo()
if (x > width / 2 && y > height / 2) {
return "bottom-end"
} else if (x < width / 2 && y < height / 2) {
return "top-start"
} else if (x > width / 2 && y < height / 2) {
return "top-end"
} else if (x < width / 2 && y > height / 2) {
return "bottom-start"
} else if (x > width / 2) {
return "top-end"
} else {
return "top-start"
}
}
const dynamicGetY = (y: number, gap: number) => {
const { windowHeight: height } = uni.getWindowInfo()
y = y < gap ? gap : y
y = height - y < gap ? height - gap : y
return y
}
const dynamicGetX = (x: number, gap: number) => {
const { windowWidth: width } = uni.getWindowInfo()
x = x < gap ? gap : x
x = width - x < gap ? width - gap : x
return x
}
const transformRpx = (params: number) => {
const { screenWidth } = uni.getWindowInfo()
return (params * screenWidth) / 375
}
watch(() => props.modelValue, async (newVal) => {
if (newVal) await popupsPosition()
show.value = newVal
}, { immediate: true })
watch(() => props.placement, (newVal) => {
dynPlace.value = newVal
}, { immediate: true })
</script>
<style lang="scss" scoped>

View File

@@ -11,29 +11,21 @@
</view>
</template>
<script>
export default {
name: 'UTimeLineItem',
props: {
nodeTop: {
type: [String, Number],
default: ''
},
bgColor: {
type: String,
default: '#ffffff'
}
},
computed: {
nodeStyle() {
if (this.nodeTop === '' || this.nodeTop === null || this.nodeTop === undefined) {
return {}
}
const top = typeof this.nodeTop === 'number' ? `${this.nodeTop}rpx` : this.nodeTop
return { top }
}
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
nodeTop?: string | number
bgColor?: string
}>()
const nodeStyle = computed(() => {
if (props.nodeTop === '' || props.nodeTop === null || props.nodeTop === undefined) {
return {}
}
}
const top = typeof props.nodeTop === 'number' ? `${props.nodeTop}rpx` : props.nodeTop
return { top }
})
</script>
<style lang="scss" scoped>

View File

@@ -4,10 +4,8 @@
</view>
</template>
<script>
export default {
name: 'UTimeLine'
}
<script setup lang="ts">
// UTimeLine 组件
</script>
<style lang="scss" scoped>

View File

@@ -26,42 +26,28 @@
</view>
</template>
<script>
export default {
name: 'uni-load-more',
props: {
status: {
//上拉的状态more-loading前loading-loading中noMore-没有更多了
type: String,
default: 'more'
},
showIcon: {
type: Boolean,
default: true
},
showText: {
type: Boolean,
default: true
},
color: {
type: String,
default: '#777777'
},
contentText: {
type: Object,
default() {
return {
contentdown: '上拉显示更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了'
};
}
}
},
data() {
return {};
<script setup lang="ts">
withDefaults(defineProps<{
status?: string
showIcon?: boolean
showText?: boolean
color?: string
contentText?: {
contentdown: string
contentrefresh: string
contentnomore: string
}
};
}>(), {
status: 'more',
showIcon: true,
showText: true,
color: '#777777',
contentText: () => ({
contentdown: '上拉显示更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了',
}),
})
</script>
<style>

View File

@@ -10,211 +10,183 @@
</view>
</view>
</template>
<script>
/**
* NumberBox 数字输入框
* @description 带加减按钮的数字输入框
* @tutorial https://ext.dcloud.net.cn/plugin?id=31
* @property {Number} value 输入框当前值
* @property {Number} min 最小值
* @property {Number} max 最大值
* @property {Number} step 每次点击改变的间隔大小
* @property {String} background 背景色
* @property {String} color 字体颜色(前景色)
* @property {Boolean} disabled = [true|false] 是否为禁用状态
* @event {Function} change 输入框值改变时触发的事件,参数为输入框当前的 value
* @event {Function} focus 输入框聚焦时触发的事件,参数为 event 对象
* @event {Function} blur 输入框失焦时触发的事件,参数为 event 对象
*/
export default {
name: "UniNumberBox",
emits: ['change', 'input', 'update:modelValue', 'blur', 'focus'],
props: {
value: {
type: [Number, String],
default: 1
},
modelValue: {
type: [Number, String],
default: 1
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
step: {
type: Number,
default: 1
},
background: {
type: String,
default: '#f5f5f5'
},
color: {
type: String,
default: '#333'
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
inputValue: 0
};
},
watch: {
value(val) {
this.inputValue = +val;
},
modelValue(val) {
this.inputValue = +val;
}
},
created() {
if (this.value === 1) {
this.inputValue = +this.modelValue;
}
if (this.modelValue === 1) {
this.inputValue = +this.value;
}
},
methods: {
_calcValue(type) {
if (this.disabled) {
return;
}
const scale = this._getDecimalScale();
let value = this.inputValue * scale;
let step = this.step * scale;
if (type === "minus") {
value -= step;
if (value < (this.min * scale)) {
return;
}
if (value > (this.max * scale)) {
value = this.max * scale
}
}
<script setup lang="ts">
import { ref, watch } from 'vue'
if (type === "plus") {
value += step;
if (value > (this.max * scale)) {
return;
}
if (value < (this.min * scale)) {
value = this.min * scale
}
}
const props = withDefaults(defineProps<{
value?: number | string
modelValue?: number | string
min?: number
max?: number
step?: number
background?: string
color?: string
disabled?: boolean
}>(), {
value: 1,
modelValue: 1,
min: 0,
max: 100,
step: 1,
background: '#f5f5f5',
color: '#333',
disabled: false,
})
this.inputValue = (value / scale).toFixed(String(scale).length - 1);
this.$emit("change", +this.inputValue);
// TODO vue2 兼容
this.$emit("input", +this.inputValue);
// TODO vue3 兼容
this.$emit("update:modelValue", +this.inputValue);
},
_getDecimalScale() {
const emit = defineEmits<{
change: [value: number]
input: [value: number]
'update:modelValue': [value: number]
blur: [event: Event]
focus: [event: Event]
}>()
let scale = 1;
// 浮点型
if (~~this.step !== this.step) {
scale = Math.pow(10, String(this.step).split(".")[1].length);
}
return scale;
},
_onBlur(event) {
this.$emit('blur', event)
let value = event.detail.value;
if (!value) {
// this.inputValue = 0;
return;
}
value = +value;
if (value > this.max) {
value = this.max;
} else if (value < this.min) {
value = this.min;
}
const scale = this._getDecimalScale();
this.inputValue = value.toFixed(String(scale).length - 1);
this.$emit("change", +this.inputValue);
this.$emit("input", +this.inputValue);
},
_onFocus(event) {
this.$emit('focus', event)
}
const inputValue = ref(0)
watch(() => props.value, (val) => {
inputValue.value = +val
})
watch(() => props.modelValue, (val) => {
inputValue.value = +val
})
if (props.value === 1) {
inputValue.value = +props.modelValue
}
if (props.modelValue === 1) {
inputValue.value = +props.value
}
function _getDecimalScale() {
let scale = 1
if (~~props.step !== props.step) {
scale = Math.pow(10, String(props.step).split('.')[1].length)
}
return scale
}
function _emitValue() {
const val = +inputValue.value
emit('change', val)
emit('input', val)
emit('update:modelValue', val)
}
function _calcValue(type: 'minus' | 'plus') {
if (props.disabled) {
return
}
const scale = _getDecimalScale()
let value = inputValue.value * scale
const step = props.step * scale
if (type === 'minus') {
value -= step
if (value < props.min * scale) {
return
}
};
if (value > props.max * scale) {
value = props.max * scale
}
}
if (type === 'plus') {
value += step
if (value > props.max * scale) {
return
}
if (value < props.min * scale) {
value = props.min * scale
}
}
inputValue.value = Number((value / scale).toFixed(String(scale).length - 1))
_emitValue()
}
function _onBlur(event: any) {
emit('blur', event)
let value = event.detail.value
if (!value) {
return
}
value = +value
if (value > props.max) {
value = props.max
} else if (value < props.min) {
value = props.min
}
const scale = _getDecimalScale()
inputValue.value = Number(value.toFixed(String(scale).length - 1))
_emitValue()
}
function _onFocus(event: Event) {
emit('focus', event)
}
</script>
<style lang="scss" scoped>
$box-height: 48rpx;
$bg: #f5f5f5;
$br: 4rpx;
$color: #333;
$box-height: 48rpx;
$bg: #f5f5f5;
$br: 4rpx;
$color: #333;
.uni-numbox {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-numbox {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-numbox-btns {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 8px;
background-color: $bg;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-numbox-btns {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 8px;
background-color: $bg;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-numbox__value {
margin: 0 4rpx;
background-color: $bg;
width: 70rpx;
height: $box-height;
text-align: center;
font-size: 24rpx;
border-left-width: 0;
border-right-width: 0;
color: $color;
}
.uni-numbox__value {
margin: 0 4rpx;
background-color: $bg;
width: 70rpx;
height: $box-height;
text-align: center;
font-size: 24rpx;
border-left-width: 0;
border-right-width: 0;
color: $color;
}
.uni-numbox__minus {
border-top-left-radius: $br;
border-bottom-left-radius: $br;
}
.uni-numbox__minus {
border-top-left-radius: $br;
border-bottom-left-radius: $br;
}
.uni-numbox__plus {
border-top-right-radius: $br;
border-bottom-right-radius: $br;
}
.uni-numbox__plus {
border-top-right-radius: $br;
border-bottom-right-radius: $br;
}
.uni-numbox--text {
// fix nvue
line-height: 40rpx;
.uni-numbox--text {
line-height: 40rpx;
font-size: 40rpx;
font-weight: 300;
color: $color;
}
font-size: 40rpx;
font-weight: 300;
color: $color;
}
.uni-numbox .uni-numbox--disabled {
color: #c0c0c0 !important;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.uni-numbox .uni-numbox--disabled {
color: #c0c0c0 !important;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
</style>

View File

@@ -1,111 +1,99 @@
<template></template>
<script>
import { getAddressCode } from "@/api/address";
<script setup lang="ts">
import { onMounted } from 'vue'
import { getAddressCode } from '@/api/address'
import config from '@/config/config'
export default {
data() {
return {
config
};
},
mounted() {
this.init();
},
methods: {
// 初始化地图并且调用
initMap() {
let that = this;
uni.chooseLocation({
success: function (res) {
/**获取地址详情地址 */
that.posToCity(res.latitude, res.longitude).then((val) => {
/**获取地址code */
getAddressCode(
val.regeocode.addressComponent.citycode,
val.regeocode.addressComponent.township
).then((code) => {
that.$emit("callback", { ...val, ...res, ...code });
that.$emit("close");
});
});
},
fail(e) {
console.log(e)
that.$emit("close");
},
});
},
// 根据当前客户端判断展示不同类型数据
init() {
// #ifdef MP-WEIXIN
this.wechatMap();
// #endif
// #ifndef MP-WEIXIN
this.initMap();
// #endif
},
const emit = defineEmits<{
callback: [payload: Record<string, any>]
close: []
}>()
// 如果是微信小程序单独走微信小程序授权模式
wechatMap() {
let that = this;
uni.authorize({
scope: "scope.userLocation",
success() {
// 允许授权
that.initMap();
},
fail() {
//拒绝授权
uni.showModal({
content: "检测到您没打开获取地址功能权限,是否去设置打开?",
confirmText: "确认",
cancelText: "取消",
success: (res) => {
if (res.confirm) {
// 打开设置好后重新刷新地图
uni.openSetting({
success: (res) => {
that.initMap();
},
});
} else {
// 取消后关闭
that.$emit("close");
return false;
}
},
});
return false;
},
});
},
// 获取城市的数据
posToCity(latitude, longitude) {
return new Promise((resolve, reject) => {
uni.request({
url: `https://restapi.amap.com/v3/geocode/regeo`,
method: "GET",
data: {
key: config.aMapKey, //web服务的key
location: `${longitude},${latitude}`,
},
success: ({ data }) => {
const { status, info } = data;
if (status === "1") {
resolve(data);
} else {
reject(info);
}
},
fail: (err) => {
reject(err);
},
});
});
},
},
};
onMounted(() => {
init()
})
function initMap() {
uni.chooseLocation({
success(res) {
posToCity(res.latitude, res.longitude).then((val) => {
getAddressCode(
val.regeocode.addressComponent.citycode,
val.regeocode.addressComponent.township,
).then((code) => {
emit('callback', { ...val, ...res, ...code })
emit('close')
})
})
},
fail(e) {
console.log(e)
emit('close')
},
})
}
function init() {
// #ifdef MP-WEIXIN
wechatMap()
// #endif
// #ifndef MP-WEIXIN
initMap()
// #endif
}
function wechatMap() {
uni.authorize({
scope: 'scope.userLocation',
success() {
initMap()
},
fail() {
uni.showModal({
content: '检测到您没打开获取地址功能权限,是否去设置打开?',
confirmText: '确认',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
uni.openSetting({
success: () => {
initMap()
},
})
} else {
emit('close')
}
},
})
},
})
}
function posToCity(latitude: number, longitude: number) {
return new Promise<any>((resolve, reject) => {
uni.request({
url: 'https://restapi.amap.com/v3/geocode/regeo',
method: 'GET',
data: {
key: config.aMapKey,
location: `${longitude},${latitude}`,
},
success: ({ data }: any) => {
const { status, info } = data
if (status === '1') {
resolve(data)
} else {
reject(info)
}
},
fail: (err) => {
reject(err)
},
})
})
}
</script>
<style lang="scss" scoped>
</style>
</style>

View File

@@ -1,9 +1,9 @@
<template>
<view>
<view
v-if="!hid"
v-if="!state.hid"
class="flex-row-center"
:style="{ top: scHight }"
:style="{ top: state.scHight }"
style="width: 750rpx; position: fixed; z-index: 100; left: 0"
>
<view
@@ -14,35 +14,35 @@
class="flex"
style="width: 100%"
animation="false"
:style="{ height: originalHeight }"
:style="{ height: state.originalHeight }"
>
<movable-view
scale-value="1"
animation="false"
damping="5000"
:x="moveX"
:x="state.moveX"
:style="{
height: sliderHeight,
width: sliderWidth,
height: state.sliderHeight,
width: state.sliderWidth,
'z-index': 101,
}"
direction="horizontal"
>
<image
:src="imgbk"
:src="state.imgbk"
class="image"
mode="aspectFit"
:style="{
height: sliderHeight,
width: sliderWidth,
'margin-top': imgbKH,
height: state.sliderHeight,
width: state.sliderWidth,
'margin-top': state.imgbKH,
}"
></image>
</movable-view>
<image
:src="img"
:src="state.img"
mode="aspectFit"
:style="{ height: originalHeight, width: originalWidth }"
:style="{ height: state.originalHeight, width: state.originalWidth }"
style="border-radius: 10rpx"
></image>
</movable-area>
@@ -61,7 +61,7 @@
scale-value="1"
animation="false"
damping="50"
:x="movePv"
:x="state.movePv"
class="flex-row-center"
style="
border-radius: 50%;
@@ -78,14 +78,14 @@
<u-icon
:color="mainColor"
size="40"
v-if="endLoad"
v-if="state.endLoad"
name="arrow-right"
></u-icon>
<u-icon :color="mainColor" size="40" v-else name="reload"></u-icon>
</movable-view>
<text style="padding-left: 140rpx" :style="{ color: col }">{{
hasImg
<text style="padding-left: 140rpx" :style="{ color: state.col }">{{
state.hasImg
}}</text>
</movable-area>
<view class="flex-row-around padding-top" style="width: 100%">
@@ -106,173 +106,178 @@
</view>
</template>
<script>
import api from "@/config/api.js";
import storage from "@/utils/storage.js";
import uuid from "@/utils/uuid.modified.js";
const phone = uni.getSystemInfoSync();
const l = phone.screenWidth / 750;
export default {
name: "verification",
created() {
// 可自行调整
this.scHight = phone.screenHeight / 2 - 200 + "px";
this.getCode();
},
props: {
height: {
type: String,
default: "80rpx",
<script setup lang="ts">
import { reactive, computed } from 'vue'
import { useStore } from '@/store'
import api from '@/config/api.js'
import storage from '@/utils/storage.js'
import uuid from '@/utils/uuid.modified.js'
const emit = defineEmits(['send'])
const props = withDefaults(defineProps<{
height?: string
width?: string
left?: string
top?: string
business?: string
}>(), {
height: '80rpx',
width: '350rpx',
left: '180rpx',
top: '30rpx',
business: 'LOGIN'
})
const store = useStore()
const phone = uni.getSystemInfoSync()
const l = phone.screenWidth / 750
const mainColor = computed(() => store.getters.mainColor)
const state = reactive({
flage: false,
key: '', // key
vsrtx: '点击进行验证', // 按钮提示语
vsr: false,
hid: true,
col: '#838383',
movePv: 0,
hasImg: '拖动滑块已完成拼图',
spcode: '',
tl: 0,
moveCode: 0,
// X轴移动距离
moveX: 0,
// 模版高度
originalHeight: '',
// 模版宽度
originalWidth: '',
// 拼图高度
sliderHeight: '',
// 平涂宽度
sliderWidth: '',
scHight: 0,
// 原图
img: '',
// 拼图
imgbk: '',
endLoad: true,
imgbKH: ''
})
// 可自行调整
state.scHight = phone.screenHeight / 2 - 200 + 'px'
getCode()
function show() {
state.hid = false
}
function hide() {
if (!state.vsr) {
// vsr判断是否验证成功成功隐藏验证框
state.hid = !state.hid
}
}
function error() {
state.vsr = false
state.hid = false
state.moveX = 0
state.moveCode = 0
}
// 获取验证图片
function getCode() {
state.col = '#b3afae'
state.hasImg = '图片加载中...'
if (!storage.getUuid()) {
storage.setUuid(uuid.v1())
}
uni.request({
url: api.common + '/common/slider/' + props.business,
header: {
uuid: storage.getUuid()
},
width: {
type: String,
default: "350rpx",
success: (res: any) => {
state.col = '#838383'
state.hasImg = '拖动滑块以完成拼图'
const data = res.data.result
// base64的图片
state.img = data.backImage
state.imgbk = data.slidingImage
// 根据参数动态适应验证图片的高宽
state.imgbKH = data.randomY * 1.8 + 'rpx'
state.originalHeight = data.originalHeight * 1.8 + 'rpx'
state.originalWidth = data.originalWidth * 1.8 + 'rpx'
state.sliderHeight = data.sliderHeight * 1.8 + 'rpx'
state.sliderWidth = data.sliderWidth * 1.8 + 'rpx'
// 适应比率,用来适应滑动距离
state.tl = 1 / (1.8 * l)
// 无用信息
state.spcode = data.capcode
// 验证令牌
state.key = data.key
store.state.verificationKey = data.key
}
})
}
function end() {
state.endLoad = false
// 验证拼图位置是否正确
uni.request({
method: 'POST',
url:
api.common +
'/common/slider/' +
props.business +
'?xPos=' +
parseInt(String(state.moveCode * state.tl)),
header: {
uuid: storage.getUuid()
},
left: {
type: String,
default: "180rpx",
},
top: {
type: String,
default: "30rpx",
},
business: {
type: String,
default: "LOGIN",
},
},
data() {
return {
mainColor: this.$mainColor,
flage: false,
key: "", //key
vsrtx: "点击进行验证", //按钮提示语
vsr: false, //
hid: true,
col: "#838383",
movePv: 0,
hasImg: "拖动滑块已完成拼图",
spcode: "",
tl: 0,
moveCode: 0,
//X轴移动距离
moveX: 0,
//模版高度
originalHeight: "",
//模版宽度
originalWidth: "",
//拼图高度
sliderHeight: "",
//平涂宽度
sliderWidth: "",
scHight: 0,
//原图
img: "",
//拼图
imgbk: "",
endLoad: true,
imgbKH: "",
};
},
methods: {
show() {
this.hid = false;
},
hide() {
if (!this.vsr) {
// vsr判断是否验证成功成功隐藏验证框
this.hid = !this.hid;
success: (res: any) => {
state.endLoad = true
if (res.data.result == false) {
res.data.result = false
} else {
res.data.result = true
}
if (res.data && res.data.result) {
// 验证成功后把key发送出去,后端会把验证信息存在缓存里
emit('send', state.key)
hide()
state.vsr = true
state.vsrtx = '已通过验证'
} else {
getCode() // 让滑块回到起始位置
if (state.movePv == 1) {
state.movePv = 0
} else {
state.movePv = 1
}
}
},
error() {
this.vsr = false;
this.hid = false;
this.moveX = 0;
this.moveCode = 0;
},
// 获取验证图片
getCode() {
this.col = "#b3afae";
this.hasImg = "图片加载中...";
if (!storage.getUuid()) {
storage.setUuid(uuid.v1());
}
uni.request({
url: api.common + "/common/slider/" + this.business,
header: {
uuid: storage.getUuid(),
},
success: (res) => {
this.col = "#838383";
this.hasImg = "拖动滑块以完成拼图";
var data = res.data.result;
fail: () => {
uni.showToast({ title: '连接服务器失败', icon: 'none' })
}
})
}
// base64的图片
this.img = data.backImage;
this.imgbk = data.slidingImage;
// 根据参数动态适应验证图片的高宽
this.imgbKH = data.randomY * 1.8 + "rpx";
this.originalHeight = data.originalHeight * 1.8 + "rpx";
this.originalWidth = data.originalWidth * 1.8 + "rpx";
this.sliderHeight = data.sliderHeight * 1.8 + "rpx";
this.sliderWidth = data.sliderWidth * 1.8 + "rpx";
// 适应比率,用来适应滑动距离
this.tl = 1 / (1.8 * l);
// 无用信息
this.spcode = data.capcode;
// 验证令牌
this.key = data.key;
this.$store.state.verificationKey = data.key;
},
});
},
end(e) {
this.endLoad = false;
// 验证拼图位置是否正确
uni.request({
method: "POST",
url:
api.common +
"/common/slider/" +
this.business +
"?xPos=" +
parseInt(this.moveCode * this.tl),
header: {
uuid: storage.getUuid(),
},
success: (res) => {
this.endLoad = true;
res.data.result == false
? (res.data.result = false)
: (res.data.result = true);
// 绑定拼图位置
function moveChange(e: any) {
state.moveX = e.detail.x
state.moveCode = e.detail.x
}
if (res.data && res.data.result) {
//验证成功后把key发送出去,后端会把验证信息存在缓存里
this.$emit("send", this.key);
this.hide();
this.vsr = true;
this.vsrtx = "已通过验证";
} else {
this.getCode(); // 让滑块回到起始位置
if (this.movePv == 1) {
this.movePv = 0;
} else {
this.movePv = 1;
}
}
},
fail: (res) => {
this.$msg("连接服务器失败");
},
});
},
// 绑定拼图位置
moveChange(e) {
this.moveX = e.detail.x;
this.moveCode = e.detail.x;
},
},
};
defineExpose({
show,
hide,
error,
getCode,
})
</script>
<style lang="scss" scoped>

View File

@@ -10,7 +10,7 @@
:password="isPassword"
:type="inputType"
:maxlength="size"
@input="input"
@input="handleInput"
@focus="inputFocus"
@blur="inputBlur"
/>
@@ -37,7 +37,7 @@
</view>
</view>
</template>
<script>
<script setup lang="ts">
/**
* @description 输入验证码组件
* @property {string} type = [box|middle|bottom] - 显示类型 默认box -eg:bottom
@@ -50,175 +50,155 @@
* @property {string} boxActiveColor - 光标聚焦到的框的颜色 默认:#000000
* @event {Function(data)} confirm - 输入完成
*/
export default {
name: 'xt-verify-code',
emits: ['update:modelValue', 'input', 'confirm'],
props: {
modelValue: {
type: String,
default: () => ''
},
value: {
type: String,
default: () => ''
},
type: {
type: String,
default: () => 'box'
},
inputType: {
type: String,
default: () => 'number'
},
size: {
type: Number,
default: () => 6
},
isFocus: {
type: Boolean,
default: () => true
},
isPassword: {
type: Boolean,
default: () => false
},
cursorColor: {
type: String,
default: () => '#cccccc'
},
boxNormalColor: {
type: String,
default: () => '#cccccc'
},
boxActiveColor: {
type: String,
default: () => '#000000'
}
},
data() {
return {
focused: false,
cursorVisible: false,
cursorHeight: 35,
code: '', // 输入的验证码
codeCursorLeft: [] // 向左移动的距离数组
};
},
created() {
this.code = this.modelValue || this.value || '';
this.focused = this.isFocus;
this.cursorVisible = this.isFocus;
},
mounted() {
this.init();
if (this.isFocus) {
this.$nextTick(() => {
this.focusInput();
});
}
},
methods: {
focusInput() {
this.focused = false;
this.$nextTick(() => {
this.focused = true;
// #ifdef H5
const inputEl = this.$refs.codeInput;
if (inputEl && typeof inputEl.focus === 'function') {
inputEl.focus();
}
// #endif
});
},
/**
* @description 初始化
*/
init() {
this.getCodeCursorLeft();
this.setCursorHeight();
},
/**
* @description 获取元素节点
* @param {string} elm - 节点的id、class 相当于 document.querySelect的参数 -eg: #id
* @param {string} type = [single|array] - 单个元素获取多个元素 默认是单个元素
* @param {Function} callback - 回调函数
*/
getElement(elm, type = 'single', callback) {
uni
.createSelectorQuery()
.in(this)
[type === 'array' ? 'selectAll' : 'select'](elm)
.boundingClientRect()
.exec(data => {
callback(data[0]);
});
},
/**
* @description 计算光标的高度
*/
setCursorHeight() {
this.getElement('.xt__box', 'single', boxElm => {
this.cursorHeight = boxElm.height * 0.6;
});
},
/**
* @description 获取光标在每一个box的left位置
*/
getCodeCursorLeft() {
// 获取父级框的位置信息
this.getElement('#xt__input-ground', 'single', parentElm => {
const parentLeft = parentElm.left;
// 获取各个box信息
this.getElement('.xt__box', 'array', elms => {
this.codeCursorLeft = [];
elms.forEach(elm => {
this.codeCursorLeft.push(elm.left - parentLeft + elm.width / 2);
});
});
});
},
import { ref, watch, onMounted, nextTick, getCurrentInstance } from 'vue'
// 输入框输入变化的回调
input(e) {
const value = e.detail.value;
this.code = value;
this.cursorVisible = value.length !== this.size;
this.$emit('update:modelValue', value);
this.$emit('input', value);
this.inputSuccess(value);
},
const emit = defineEmits(['update:modelValue', 'input', 'confirm'])
const props = withDefaults(defineProps<{
modelValue?: string
value?: string
type?: string
inputType?: string
size?: number
isFocus?: boolean
isPassword?: boolean
cursorColor?: string
boxNormalColor?: string
boxActiveColor?: string
}>(), {
modelValue: '',
value: '',
type: 'box',
inputType: 'number',
size: 6,
isFocus: true,
isPassword: false,
cursorColor: '#cccccc',
boxNormalColor: '#cccccc',
boxActiveColor: '#000000'
})
// 输入完成回调
inputSuccess(value) {
if (value.length === this.size) {
this.$emit('confirm', value);
}
},
// 输入聚焦
inputFocus() {
this.cursorVisible = this.code.length !== this.size;
},
// 输入失去焦点
inputBlur() {
this.cursorVisible = false;
},
codeFormat(val, isPassword) {
let value = '';
if (val) {
value = isPassword ? '*' : val;
}
return value;
}
},
watch: {
modelValue(val) {
this.code = val || '';
},
value(val) {
this.code = val || '';
}
const instance = getCurrentInstance()
const codeInput = ref<any>(null)
const focused = ref(false)
const cursorVisible = ref(false)
const cursorHeight = ref(35)
const code = ref('') // 输入的验证码
const codeCursorLeft = ref<number[]>([]) // 向左移动的距离数组
// 初始化
watch([() => props.modelValue, () => props.value], ([modelVal, val]) => {
code.value = modelVal || val || ''
}, { immediate: true })
onMounted(() => {
focused.value = props.isFocus
cursorVisible.value = props.isFocus
init()
if (props.isFocus) {
nextTick(() => {
focusInput()
})
}
};
})
function focusInput() {
focused.value = false
nextTick(() => {
focused.value = true
// #ifdef H5
const inputEl = codeInput.value
if (inputEl && typeof inputEl.focus === 'function') {
inputEl.focus()
}
// #endif
})
}
/**
* @description 初始化
*/
function init() {
getCodeCursorLeft()
setCursorHeight()
}
/**
* @description 获取元素节点
* @param {string} elm - 节点的id、class 相当于 document.querySelect的参数 -eg: #id
* @param {string} type = [single|array] - 单个元素获取多个元素 默认是单个元素
* @param {Function} callback - 回调函数
*/
function getElement(elm: string, type: string = 'single', callback: Function) {
uni
.createSelectorQuery()
.in(instance?.proxy)
[type === 'array' ? 'selectAll' : 'select'](elm)
.boundingClientRect()
.exec((data: any) => {
callback(data[0])
})
}
/**
* @description 计算光标的高度
*/
function setCursorHeight() {
getElement('.xt__box', 'single', (boxElm: any) => {
cursorHeight.value = boxElm.height * 0.6
})
}
/**
* @description 获取光标在每一个box的left位置
*/
function getCodeCursorLeft() {
// 获取父级框的位置信息
getElement('#xt__input-ground', 'single', (parentElm: any) => {
const parentLeft = parentElm.left
// 获取各个box信息
getElement('.xt__box', 'array', (elms: any) => {
codeCursorLeft.value = []
elms.forEach((elm: any) => {
codeCursorLeft.value.push(elm.left - parentLeft + elm.width / 2)
})
})
})
}
// 输入框输入变化的回调
function handleInput(e: any) {
const value = e.detail.value
code.value = value
cursorVisible.value = value.length !== props.size
emit('update:modelValue', value)
emit('input', value)
inputSuccess(value)
}
// 输入完成回调
function inputSuccess(value: string) {
if (value.length === props.size) {
emit('confirm', value)
}
}
// 输入聚焦
function inputFocus() {
cursorVisible.value = code.value.length !== props.size
}
// 输入失去焦点
function inputBlur() {
cursorVisible.value = false
}
function codeFormat(val: string | undefined, isPassword: boolean): string {
let value = ''
if (val) {
value = isPassword ? '*' : val
}
return value
}
</script>
<style lang="scss" scoped>
.xt__verify-code {