Merge branch 'feat/appointment-goods'

This commit is contained in:
pikachu1995@126.com
2026-10-21 08:25:10 +08:00
94 changed files with 6672 additions and 6293 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
### 说明 https://ext.dcloud.net.cn/plugin?id=3237

View File

@@ -1,200 +0,0 @@
<template>
<div class="index">
<u-modal v-model:show="show" :show-title="false" :show-confirm-button="false" mask-close-able>
<view class="slot-content">
<image @click="downLoad()" class="img" :src="imgUrl" />
<div class="canvas-hide">
<!-- #ifdef MP-WEIXIN -->
<canvas id="canvas" type="2d" style="width: 600px; height: 960px" />
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<canvas canvas-id="canvas" id="canvas" style="width: 600px; height: 960px" />
<!-- #endif -->
</div>
</view>
</u-modal>
</div>
</template>
<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"
const instance = getCurrentInstance()
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",
})
},
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(600)
dpVal.canvas.height = st2(960)
// #endif
await draw(dpVal)
}
const draw = async (dp: any) => {
const { width, height, background, title } = props.res.container
const { code, 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()
})
/** 绘制图片 */
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: 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 = `${st2(38)}px PingFang SC`
ctx.textAlign = "center"
ctx.fillText(price, st2(300), st2(680))
})
// /** 绘制底部文字 */
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))
})
imgUrl.value = await dp.createImagePath()
}
onMounted(() => {
init()
})
</script>
<style lang="scss" scoped>
page,
.index {
height: 100%;
}
.canvas-hide {
/* 1 */
position: fixed;
right: 100vw;
bottom: 100vh;
/* 2 */
z-index: -9999;
/* 3 */
opacity: 0;
}
.index {
position: relative;
text-align: center;
background: rgba($color: grey, $alpha: 0.2);
}
image {
display: block;
}
.img {
width: 600rpx;
height: 960rpx;
}
</style>

View File

@@ -1,199 +0,0 @@
<template>
<view class="index">
<view v-model="show" class="slot-content">
<image @click="downLoad()" class="img" :src="imgUrl" />
<div class="canvas-hide">
<!-- #ifdef MP-WEIXIN -->
<canvas id="canvas" type="2d" style="width: 560px; height: 800px" />
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<canvas canvas-id="canvas" id="canvas" style="width: 560px; height: 800px" />
<!-- #endif -->
</div>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance } from 'vue'
// 引入绘制插件
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"
const instance = getCurrentInstance()
const props = defineProps<{
res?: any
}>()
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,
})
ctx.fillStyle = "#ff3c2a"
ctx.font = `${st2(38)}px PingFang SC`
ctx.textAlign = "left"
ctx.fillText(price, st2(60), st2(665))
})
// 绘制生成本地地址
imgUrl.value = await dp.createImagePath()
}
onMounted(() => {
init()
})
</script>
<style lang="scss" scoped>
page,
.index {
height: 100%;
}
.canvas-hide {
/* 1 */
position: fixed;
right: 100vw;
bottom: 100vh;
/* 2 */
z-index: -9999;
/* 3 */
opacity: 0;
}
.index {
position: relative;
text-align: center;
background: rgba($color: grey, $alpha: 0.2);
}
image {
display: block;
}
.img {
width: 560rpx;
height: 800rpx;
}
.qrcode-content {
width: 100rpx;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,331 +0,0 @@
<template>
<view class="shadow" :class="!show?'':'shadow-show'" :style="{backgroundColor:show?maskBg:'rgba(0,0,0,0)'}" @tap="tapMask">
<view class="popups" :class="[theme]" :style="{top: popupsTop ,left: popupsLeft,flexDirection:direction}">
<text :class="dynPlace" :style="{width:'0px',height:'0px'}" v-if="triangle"></text>
<view v-for="(item,index) in popData" :key="index" @tap.stop="tapItem(item)" class="itemChild view" :class="[direction=='row'?'solid-right':'solid-bottom',item.disabled?'disabledColor':'']">
<view class="popup-icon" v-if="item.icon">
<u-icon size="35rpx" :name="item.icon"></u-icon>
</view>
<span class="title">{{item.title}}</span>
</view>
<slot></slot>
</view>
</view>
</template>
<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,
})
const emit = defineEmits(['update:modelValue', 'tapPopup'])
const instance = getCurrentInstance()
const popupsTop = ref("0rpx")
const popupsLeft = ref("0rpx")
const show = ref(false)
const dynPlace = ref("")
onMounted(() => {
popupsPosition()
})
const tapMask = () => {
emit("update:modelValue", !props.modelValue)
}
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>
.title {
margin-left: 16rpx;
}
.shadow {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
z-index: 9999;
transition: background 0.3s ease-in-out;
visibility: hidden;
&.shadow-show {
visibility: visible;
}
}
.popups {
position: absolute;
padding: 20rpx;
border-radius: 5px;
display: flex;
.view {
display: flex;
align-items: center;
padding: 15rpx 10rpx;
font-size: 25rpx;
}
.popup-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
line-height: 40rpx;
}
.image {
display: inline-block;
vertical-align: middle;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
}
.dark {
background-color: #4c4c4c;
color: #fff;
.top-start:after {
content: "";
position: absolute;
top: -18rpx;
left: 10rpx;
border-width: 0 20rpx 20rpx;
border-style: solid;
border-color: transparent transparent #4c4c4c;
}
.top-end:after {
content: "";
position: absolute;
top: -18rpx;
right: 10rpx;
border-width: 0 20rpx 20rpx;
border-style: solid;
border-color: transparent transparent #4c4c4c;
}
.bottom-start:after {
content: "";
position: absolute;
bottom: -18rpx;
left: 10rpx;
border-width: 20rpx 20rpx 0;
border-style: solid;
border-color: #4c4c4c transparent transparent;
}
.bottom-end:after {
content: "";
position: absolute;
bottom: -18rpx;
right: 10rpx;
border-width: 20rpx 20rpx 0;
border-style: solid;
border-color: #4c4c4c transparent transparent;
}
.disabledColor {
color: #c5c8ce;
}
}
.light {
color: #515a6e;
box-shadow: 0upx 0upx 30upx rgba(0, 0, 0, 0.2);
background: #fff;
.top-start:after {
content: "";
position: absolute;
top: -18rpx;
left: 10rpx;
border-width: 0 20rpx 20rpx;
border-style: solid;
border-color: transparent transparent #fff;
}
.top-end:after {
content: "";
position: absolute;
top: -18rpx;
right: 10rpx;
border-width: 0 20rpx 20rpx;
border-style: solid;
border-color: transparent transparent #fff;
}
.bottom-start:after {
content: "";
position: absolute;
bottom: -18rpx;
left: 10rpx;
border-width: 20rpx 20rpx 0;
border-style: solid;
border-color: #fff transparent transparent;
}
.bottom-end:after {
content: "";
position: absolute;
bottom: -18rpx;
right: 10rpx;
border-width: 20rpx 20rpx 0;
border-style: solid;
border-color: #fff transparent transparent;
}
.disabledColor {
color: #c5c8ce;
}
}
.solid-bottom {
border-bottom: 1px solid #f3f5f7;
}
.solid-right {
border-right: 1px solid #ccc;
}
.popups .itemChild:last-child {
border: none;
}
</style>

View File

@@ -4,12 +4,9 @@
v-if="!state.hid"
class="flex-row-center"
:style="{ top: state.scHight }"
style="width: 750rpx; position: fixed; z-index: 100; left: 0"
style="width: 750rpx; position: fixed; z-index: 100; left: 0; background-color: #ffffff; padding: 40rpx 0"
>
<view
class="flex-column-center"
style="background-color: #fcfcfc; padding: 30rpx; border-radius: 10rpx"
>
<view class="flex-column-center">
<movable-area
class="flex"
style="width: 100%"
@@ -43,63 +40,47 @@
:src="state.img"
mode="aspectFit"
:style="{ height: state.originalHeight, width: state.originalWidth }"
style="border-radius: 10rpx"
style="border-radius: 24rpx"
></image>
</movable-area>
<movable-area
class="flex-row-start"
style="
width: 100%;
background-color: #efefef;
height: 80rpx;
border-radius: 40rpx;
margin-top: 30rpx;
"
>
<movable-area class="v-track" animation="false">
<view class="v-fill" :style="{ width: fillWidth }"></view>
<text class="v-track-text" :style="{ color: state.col }">{{
state.hasImg
}}</text>
<movable-view
scale-value="1"
animation="false"
damping="50"
:x="state.movePv"
class="flex-row-center"
style="
border-radius: 50%;
height: 100rpx;
width: 100rpx;
background-color: #ffffff;
border: 2rpx solid #e3e3e3;
margin-top: -13rpx;
"
class="v-handle"
:style="handleStyle"
direction="horizontal"
@change="moveChange"
@touchend="end"
>
<u-icon
:color="mainColor"
size="40"
color="#ffffff"
size="38"
v-if="state.endLoad"
name="arrow-right"
></u-icon>
<u-icon :color="mainColor" size="40" v-else name="reload"></u-icon>
<u-icon color="#ffffff" size="38" v-else name="reload"></u-icon>
</movable-view>
<text style="padding-left: 140rpx" :style="{ color: state.col }">{{
state.hasImg
}}</text>
</movable-area>
<view class="flex-row-around padding-top" style="width: 100%">
<u-icon
@click="hide"
:color="mainColor"
size="40"
name="close"
></u-icon>
<text class="cu-tag round" @click="getCode">刷新拼图</text>
<text class="my-neirong-sm cuIcon-safe" style="color: #c1c1c1"
>Lili-FRAMEWORK</text
>
<view class="v-foot">
<view class="v-icon-btn" @click="getCode">
<u-icon color="#888888" size="32" name="reload"></u-icon>
</view>
<view class="v-icon-btn" @click="hide">
<u-icon color="#888888" size="30" name="close"></u-icon>
</view>
</view>
<view class="v-mark">
<text class="cuIcon-safe v-mark-icon"></text>
<text>Lili-FRAMEWORK 安全验证</text>
</view>
</view>
</view>
@@ -133,6 +114,13 @@ const phone = uni.getSystemInfoSync()
const l = phone.screenWidth / 750
const mainColor = computed(() => store.getters.mainColor)
const fillWidth = computed(() => `${state.moveX + 42 * l}px`)
const handleStyle = computed(() => {
const glow = /^#[0-9a-fA-F]{6}$/.test(mainColor.value)
? mainColor.value + '59'
: 'rgba(0, 0, 0, 0.18)'
return { background: mainColor.value, boxShadow: `0 6rpx 20rpx ${glow}` }
})
const state = reactive({
flage: false,
key: '', // key
@@ -284,6 +272,79 @@ defineExpose({
@import "./animation.css";
@import "./icon.css";
// @import './main.css';
.v-track {
position: relative;
width: 100%;
height: 84rpx;
margin-top: 36rpx;
background-color: #f7f7f8;
border-radius: 42rpx;
}
.v-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
background-color: #ffe9e5;
border-radius: 42rpx;
}
.v-track-text {
position: absolute;
left: 0;
right: 0;
top: 0;
line-height: 84rpx;
text-align: center;
font-size: 26rpx;
letter-spacing: 2rpx;
}
.v-handle {
width: 84rpx;
height: 84rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.v-foot {
display: flex;
flex-direction: row;
justify-content: flex-end;
width: 100%;
margin-top: 28rpx;
}
.v-icon-btn {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
background-color: #f4f4f5;
display: flex;
align-items: center;
justify-content: center;
margin-left: 24rpx;
}
.v-mark {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-top: 32rpx;
font-size: 22rpx;
color: #d2d2d2;
}
.v-mark-icon {
margin-right: 8rpx;
font-size: 22rpx;
color: #d9d9d9;
}
.dh-wt {
animation: at 1.1s ease;
animation-iteration-count: infinite;