付晓文。 1 місяць тому
батько
коміт
7a888114bc

+ 14 - 0
common/router/modules/routes.js

@@ -861,6 +861,20 @@ const routes = [{
 			title: '明细详情',
 		},
 	},
+	{
+		path: '/pages/orderingSet/index',
+		name: 'orderingSet',
+		meta: {
+			title: '点餐管理',
+		},
+	},
+	{
+		path: '/pages/orderingSet/download',
+		name: 'orderingSetDownload',
+		meta: {
+			title: '下载链接',
+		},
+	},
 	{
 		path: '/pages/merchantDish/home/index',
 		name: 'merchantDishHome',

+ 6 - 0
components/index_nav.vue

@@ -52,6 +52,12 @@
 						path: '/pages/index/shop',
 						needTopUp: true
 					},
+					{
+						name: '点餐管理',
+						url: '/static/newIndex/productShop.png',
+						path: '/pages/orderingSet/index',
+						needTopUp: true
+					},
 					{
 						name: '储值管理',
 						url: '/static/newIndex/toup.png',

+ 14 - 0
pages.json

@@ -619,6 +619,20 @@
 			}
 		},
 
+		// 点餐管理---------------
+		{
+			"path": "pages/orderingSet/index",
+			"style": {
+				"navigationBarTitleText": "点餐管理"
+			}
+		},
+		{
+			"path": "pages/orderingSet/download",
+			"style": {
+				"navigationBarTitleText": "下载链接"
+			}
+		},
+
 		{
 			"path": "pages/merchantDish/home/index",
 			"style": {

+ 381 - 0
pages/orderingSet/download.vue

@@ -0,0 +1,381 @@
+<template>
+	<view class="page">
+		<u-navbar title="下载链接" :autoBack="true" :placeholder="true" :border-bottom="false"></u-navbar>
+
+		<view class="card">
+			<view class="card-title">桌号·共{{ tableList.length }}桌</view>
+			<view class="list">
+				<view
+					class="row"
+					v-for="item in tableList"
+					:key="item.no"
+					@click="toggleSelect(item.no)"
+				>
+					<view class="left">
+						<view class="check" :class="{ on: selectedMap[item.no] }">
+							<u-icon v-if="selectedMap[item.no]" name="checkmark" color="#fff" size="20"></u-icon>
+						</view>
+						<text class="name">{{ item.no }}-桌号</text>
+					</view>
+					<view class="qr" @click.stop="openQr(item)">
+						<image class="qr-ico" src="/static/scan/qrcode.png" mode="aspectFit"></image>
+						<text>二维码</text>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<view class="footer">
+			<view class="all" @click="toggleSelectAll">
+				<view class="check" :class="{ on: isAllSelected }">
+					<u-icon v-if="isAllSelected" name="checkmark" color="#fff" size="20"></u-icon>
+				</view>
+				<text>全选</text>
+			</view>
+			<view
+				class="dl-btn"
+				:class="{ on: selectedCount > 0 }"
+				@click="onDownload"
+			>下载链接</view>
+		</view>
+
+		<!-- 链接弹窗 -->
+		<u-popup v-model="linkVisible" mode="center" border-radius="24" :mask-close-able="true">
+			<view class="link-dialog">
+				<view class="title">下载链接</view>
+				<view class="url">{{ downloadLink }}</view>
+				<view class="copy-btn" @click="copyLink">复制链接</view>
+			</view>
+		</u-popup>
+
+		<!-- 二维码弹层 -->
+		<u-popup v-model="qrVisible" mode="bottom" border-radius="24" :mask-close-able="true">
+			<view class="qr-sheet">
+				<view class="title">{{ currentTable.no }}桌号二维码</view>
+				<view class="qr-block">
+					<image class="qr-img" :src="miniQrUrl" mode="aspectFit"></image>
+					<text class="qr-label">小程序二维码</text>
+				</view>
+				<view class="qr-block" v-if="officialQrUrl">
+					<image class="qr-img" :src="officialQrUrl" mode="aspectFit"></image>
+					<text class="qr-label">公众号二维码</text>
+				</view>
+				<view class="save-btn" @click="saveQr">保存二维码</view>
+			</view>
+		</u-popup>
+	</view>
+</template>
+
+<script>
+	import { USER_INFO } from '@/common/util/constants.js'
+
+	const STORAGE_LIST = 'ordering_table_list'
+
+	function buildQrContent(merchantId, tableNo) {
+		return `${merchantId || ''}桌号${tableNo}`
+	}
+
+	function buildQrImageUrl(content) {
+		return `https://api.qrserver.com/v1/create-qr-code/?size=280x280&data=${encodeURIComponent(content)}`
+	}
+
+	export default {
+		data() {
+			return {
+				merchantId: '',
+				tableList: [],
+				selectedMap: {},
+				linkVisible: false,
+				downloadLink: '',
+				qrVisible: false,
+				currentTable: { no: '' },
+				miniQrUrl: '',
+				officialQrUrl: ''
+			}
+		},
+		computed: {
+			selectedCount() {
+				return Object.keys(this.selectedMap).filter((k) => this.selectedMap[k]).length
+			},
+			isAllSelected() {
+				return this.tableList.length > 0 && this.selectedCount === this.tableList.length
+			}
+		},
+		onShow() {
+			const info = uni.getStorageSync(USER_INFO) || {}
+			this.merchantId = info.merchantId || ''
+			const key = `${STORAGE_LIST}_${this.merchantId || 'default'}`
+			const list = uni.getStorageSync(key)
+			this.tableList = Array.isArray(list) ? list : []
+			this.selectedMap = {}
+		},
+		methods: {
+			toggleSelect(no) {
+				const next = { ...this.selectedMap }
+				if (next[no]) delete next[no]
+				else next[no] = true
+				this.selectedMap = next
+			},
+			toggleSelectAll() {
+				if (this.isAllSelected) {
+					this.selectedMap = {}
+					return
+				}
+				const map = {}
+				this.tableList.forEach((item) => {
+					map[item.no] = true
+				})
+				this.selectedMap = map
+			},
+			onDownload() {
+				if (!this.selectedCount) return
+				const nos = this.tableList.filter((i) => this.selectedMap[i.no]).map((i) => i.no).join(',')
+				const mid = this.merchantId || 'merchant'
+				this.downloadLink = `https://www.124.123.123123.124.124?merchantId=${encodeURIComponent(mid)}&tables=${encodeURIComponent(nos)}`
+				this.linkVisible = true
+			},
+			copyLink() {
+				uni.setClipboardData({
+					data: this.downloadLink,
+					success: () => {
+						uni.showToast({ title: '复制成功', icon: 'none' })
+					}
+				})
+			},
+			openQr(item) {
+				this.currentTable = item
+				this.miniQrUrl = buildQrImageUrl(buildQrContent(this.merchantId, item.no))
+				this.officialQrUrl = ''
+				this.qrVisible = true
+			},
+			saveQr() {
+				if (!this.miniQrUrl) {
+					uni.showToast({ title: '暂无二维码', icon: 'none' })
+					return
+				}
+				uni.showLoading({ title: '保存中' })
+				uni.downloadFile({
+					url: this.miniQrUrl,
+					success: (res) => {
+						if (res.statusCode !== 200) {
+							uni.hideLoading()
+							uni.showToast({ title: '下载失败', icon: 'none' })
+							return
+						}
+						uni.saveImageToPhotosAlbum({
+							filePath: res.tempFilePath,
+							success: () => {
+								uni.hideLoading()
+								uni.showToast({ title: '已保存到相册', icon: 'success' })
+							},
+							fail: () => {
+								uni.hideLoading()
+								uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' })
+							}
+						})
+					},
+					fail: () => {
+						uni.hideLoading()
+						uni.showToast({ title: '下载失败', icon: 'none' })
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	$theme: #5b9eff;
+	$theme-disabled: #c5e0ff;
+
+	.page {
+		min-height: 100vh;
+		background: #f5f6f8;
+		padding: 24rpx 28rpx calc(160rpx + env(safe-area-inset-bottom));
+		box-sizing: border-box;
+	}
+
+	.card {
+		background: #fff;
+		border-radius: 20rpx;
+		overflow: hidden;
+		box-shadow: 0 4rpx 16rpx rgba(91, 158, 255, 0.06);
+	}
+
+	.card-title {
+		padding: 32rpx 32rpx 16rpx;
+		font-size: 30rpx;
+		font-weight: 600;
+		color: #1d2129;
+	}
+
+	.row {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 36rpx 32rpx;
+		border-top: 1rpx solid #f2f3f5;
+
+		.left {
+			display: flex;
+			align-items: center;
+			gap: 20rpx;
+		}
+
+		.name {
+			font-size: 30rpx;
+			color: #1d2129;
+		}
+
+		.qr {
+			display: flex;
+			align-items: center;
+			gap: 8rpx;
+			font-size: 26rpx;
+			color: #86909c;
+
+			.qr-ico {
+				width: 32rpx;
+				height: 32rpx;
+			}
+		}
+	}
+
+	.check {
+		width: 40rpx;
+		height: 40rpx;
+		border-radius: 50%;
+		border: 2rpx solid #c9cdd4;
+		box-sizing: border-box;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-shrink: 0;
+
+		&.on {
+			background: $theme;
+			border-color: $theme;
+		}
+	}
+
+	.footer {
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		z-index: 20;
+		display: flex;
+		align-items: center;
+		gap: 24rpx;
+		padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
+		background: #fff;
+
+		.all {
+			display: flex;
+			align-items: center;
+			gap: 12rpx;
+			font-size: 28rpx;
+			color: #1d2129;
+			flex-shrink: 0;
+			padding-right: 8rpx;
+		}
+	}
+
+	.dl-btn {
+		flex: 1;
+		height: 88rpx;
+		border-radius: 44rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 30rpx;
+		font-weight: 500;
+		background: $theme-disabled;
+		color: #fff;
+
+		&.on {
+			background: $theme;
+		}
+	}
+
+	.link-dialog {
+		width: 600rpx;
+		padding: 40rpx 36rpx 36rpx;
+		background: #fff;
+		box-sizing: border-box;
+
+		.title {
+			text-align: center;
+			font-size: 32rpx;
+			font-weight: 600;
+			color: #1d2129;
+			margin-bottom: 28rpx;
+		}
+
+		.url {
+			word-break: break-all;
+			font-size: 26rpx;
+			color: #4e5969;
+			line-height: 1.6;
+			text-align: center;
+			margin-bottom: 36rpx;
+		}
+
+		.copy-btn {
+			height: 88rpx;
+			border-radius: 44rpx;
+			background: $theme;
+			color: #fff;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			font-size: 30rpx;
+		}
+	}
+
+	.qr-sheet {
+		padding: 36rpx 40rpx calc(40rpx + env(safe-area-inset-bottom));
+		background: #fff;
+
+		.title {
+			text-align: center;
+			font-size: 34rpx;
+			font-weight: 600;
+			color: #1d2129;
+		}
+
+		.qr-block {
+			margin-top: 28rpx;
+			border: 1rpx solid #e5e6eb;
+			border-radius: 16rpx;
+			padding: 32rpx 24rpx;
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+
+			.qr-img {
+				width: 280rpx;
+				height: 280rpx;
+				background: #f5f6f8;
+			}
+
+			.qr-label {
+				margin-top: 20rpx;
+				font-size: 26rpx;
+				color: #4e5969;
+			}
+		}
+
+		.save-btn {
+			margin-top: 40rpx;
+			height: 88rpx;
+			border-radius: 44rpx;
+			background: $theme;
+			color: #fff;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			font-size: 30rpx;
+		}
+	}
+</style>

+ 549 - 0
pages/orderingSet/index.vue

@@ -0,0 +1,549 @@
+<template>
+	<view class="page">
+		<u-navbar title="点餐管理" :autoBack="true" :placeholder="true" :border-bottom="false"></u-navbar>
+
+		<!-- 下单类型 -->
+		<view class="mode-card">
+			<text class="mode-label">下单类型</text>
+			<u-radio-group v-model="orderMode" active-color="#5B9EFF" @change="onModeChange">
+				<u-radio name="orderOnly" active-color="#5B9EFF">仅下单</u-radio>
+				<u-radio name="orderPay" active-color="#5B9EFF">下单和支付</u-radio>
+			</u-radio-group>
+		</view>
+
+		<view class="table-card" :style="{ paddingBottom: tableList.length ? '24rpx' : '0' }">
+			<view class="table-head">
+				<text class="table-title">桌号·共{{ tableList.length }}桌</text>
+				<view v-if="tableList.length" class="add-link" @click="openAdd">
+					<u-icon name="plus" color="#5B9EFF" size="26"></u-icon>
+					<text>添加桌号</text>
+				</view>
+			</view>
+
+			<!-- 空态 -->
+			<view v-if="!tableList.length" class="empty-box">
+				<view class="empty-illust">
+					<view class="box-lid"></view>
+					<view class="box-body"></view>
+				</view>
+				<text class="empty-text">您的门店暂无桌号哦~</text>
+				<view class="empty-btn" @click="openAdd">去添加桌号</view>
+			</view>
+
+			<!-- 列表 -->
+			<view v-else class="table-list">
+				<view class="list-row" v-for="item in tableList" :key="item.no">
+					<text class="no-text">{{ item.no }}-桌号</text>
+					<view class="qr-entry" @click="openQr(item)">
+						<image class="qr-ico" src="/static/scan/qrcode.png" mode="aspectFit"></image>
+						<text>二维码</text>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<view class="footer" v-if="tableList.length">
+			<view class="btn soft" @click="goDownload">下载链接</view>
+		</view>
+
+		<!-- 添加桌号:底部弹层 -->
+		<u-popup v-model="addVisible" mode="bottom" border-radius="24" :mask-close-able="true">
+			<view class="add-sheet">
+				<view class="sheet-title">门店桌号</view>
+				<view class="sheet-sub">桌号·共{{ tableList.length }}桌</view>
+				<view class="range-row">
+					<input
+						class="range-input"
+						type="number"
+						v-model="startNo"
+						placeholder="请输入开始桌号"
+						placeholder-class="ph"
+					/>
+					<text class="range-dash">-</text>
+					<input
+						class="range-input"
+						type="number"
+						v-model="endNo"
+						placeholder="请输入结束桌号"
+						placeholder-class="ph"
+					/>
+				</view>
+				<view class="btn primary sheet-btn" @click="confirmAdd">确认添加</view>
+			</view>
+		</u-popup>
+
+		<!-- 校验提示弹窗 -->
+		<u-popup v-model="tipVisible" mode="center" border-radius="24" :mask-close-able="false">
+			<view class="tip-dialog">
+				<view class="tip-title">提示</view>
+				<view class="tip-content">{{ tipMessage }}</view>
+				<view class="tip-ok" @click="tipVisible = false">确定</view>
+			</view>
+		</u-popup>
+
+		<!-- 查看二维码:底部弹层 -->
+		<u-popup v-model="qrVisible" mode="bottom" border-radius="24" :mask-close-able="true">
+			<view class="qr-sheet">
+				<view class="sheet-title">{{ currentTable.no }}桌号二维码</view>
+				<view class="qr-block">
+					<image class="qr-img" :src="miniQrUrl" mode="aspectFit"></image>
+					<text class="qr-label">小程序二维码</text>
+				</view>
+				<view class="qr-block" v-if="officialQrUrl">
+					<image class="qr-img" :src="officialQrUrl" mode="aspectFit"></image>
+					<text class="qr-label">公众号二维码</text>
+				</view>
+				<view class="btn primary sheet-btn" @click="saveQr">保存二维码</view>
+			</view>
+		</u-popup>
+	</view>
+</template>
+
+<script>
+	import { USER_INFO } from '@/common/util/constants.js'
+
+	const STORAGE_LIST = 'ordering_table_list'
+	const STORAGE_MODE = 'ordering_order_mode'
+
+	function padTableNo(num) {
+		const intVal = parseInt(String(num).trim(), 10)
+		if (Number.isNaN(intVal) || intVal < 1) return ''
+		if (intVal >= 1 && intVal <= 99) return String(intVal).padStart(3, '0')
+		return String(intVal)
+	}
+
+	function buildQrContent(merchantId, tableNo) {
+		return `${merchantId || ''}桌号${tableNo}`
+	}
+
+	function buildQrImageUrl(content) {
+		return `https://api.qrserver.com/v1/create-qr-code/?size=280x280&data=${encodeURIComponent(content)}`
+	}
+
+	export default {
+		data() {
+			return {
+				orderMode: 'orderOnly',
+				tableList: [],
+				addVisible: false,
+				startNo: '',
+				endNo: '',
+				qrVisible: false,
+				currentTable: { no: '' },
+				miniQrUrl: '',
+				officialQrUrl: '',
+				merchantId: '',
+				tipVisible: false,
+				tipMessage: ''
+			}
+		},
+		onShow() {
+			const info = uni.getStorageSync(USER_INFO) || {}
+			this.merchantId = info.merchantId || ''
+			this.loadData()
+		},
+		methods: {
+			storageKey(base) {
+				return `${base}_${this.merchantId || 'default'}`
+			},
+			loadData() {
+				const mode = uni.getStorageSync(this.storageKey(STORAGE_MODE))
+				if (mode === 'orderOnly' || mode === 'orderPay') this.orderMode = mode
+				const list = uni.getStorageSync(this.storageKey(STORAGE_LIST))
+				this.tableList = Array.isArray(list) ? list : []
+			},
+			saveList() {
+				uni.setStorageSync(this.storageKey(STORAGE_LIST), this.tableList)
+			},
+			showTip(msg) {
+				this.tipMessage = msg
+				this.tipVisible = true
+			},
+			onModeChange(val) {
+				this.orderMode = val
+				uni.setStorageSync(this.storageKey(STORAGE_MODE), val)
+			},
+			openAdd() {
+				this.startNo = ''
+				this.endNo = ''
+				this.addVisible = true
+			},
+			confirmAdd() {
+				const startRaw = String(this.startNo).trim()
+				const endRaw = String(this.endNo).trim()
+				if (!startRaw || !endRaw) {
+					this.showTip('桌号不能为空')
+					return
+				}
+				const start = parseInt(startRaw, 10)
+				const end = parseInt(endRaw, 10)
+				if (Number.isNaN(start) || Number.isNaN(end) || start < 1 || end < 1) {
+					this.showTip('桌号不能为空')
+					return
+				}
+				if (start > end) {
+					this.showTip('开始桌号不能大于结束桌号')
+					return
+				}
+				if (end - start + 1 > 200) {
+					this.showTip('单次最多添加200个桌号')
+					return
+				}
+
+				const existSet = new Set(this.tableList.map((i) => i.no))
+				const toAdd = []
+				for (let i = start; i <= end; i++) {
+					const no = padTableNo(i)
+					if (!no) continue
+					if (existSet.has(no)) {
+						this.showTip(`桌号${parseInt(no, 10)}已存在,请重新输入`)
+						return
+					}
+					toAdd.push({ no, createTime: Date.now() })
+				}
+				this.tableList = this.tableList.concat(toAdd).sort((a, b) => parseInt(a.no, 10) - parseInt(b.no, 10))
+				this.saveList()
+				this.addVisible = false
+				uni.showToast({ title: '添加成功', icon: 'success' })
+			},
+			goDownload() {
+				uni.navigateTo({ url: '/pages/orderingSet/download' })
+			},
+			openQr(item) {
+				this.currentTable = item
+				const content = buildQrContent(this.merchantId, item.no)
+				this.miniQrUrl = buildQrImageUrl(content)
+				this.officialQrUrl = ''
+				this.qrVisible = true
+			},
+			saveQr() {
+				const url = this.miniQrUrl
+				if (!url) {
+					uni.showToast({ title: '暂无二维码', icon: 'none' })
+					return
+				}
+				uni.showLoading({ title: '保存中' })
+				uni.downloadFile({
+					url,
+					success: (res) => {
+						if (res.statusCode !== 200) {
+							uni.hideLoading()
+							uni.showToast({ title: '下载失败', icon: 'none' })
+							return
+						}
+						uni.saveImageToPhotosAlbum({
+							filePath: res.tempFilePath,
+							success: () => {
+								uni.hideLoading()
+								uni.showToast({ title: '已保存到相册', icon: 'success' })
+							},
+							fail: () => {
+								uni.hideLoading()
+								uni.showToast({ title: '保存失败,请检查相册权限', icon: 'none' })
+							}
+						})
+					},
+					fail: () => {
+						uni.hideLoading()
+						uni.showToast({ title: '下载失败', icon: 'none' })
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	$theme: #5b9eff;
+	$theme-soft: #eaf3ff;
+	$page-bg: #f5f6f8;
+
+	.page {
+		min-height: 100vh;
+		background: $page-bg;
+		padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
+		box-sizing: border-box;
+	}
+
+	.mode-card {
+		margin: 24rpx 28rpx 0;
+		padding: 32rpx 28rpx;
+		background: #fff;
+		border-radius: 20rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+
+		.mode-label {
+			font-size: 30rpx;
+			color: #1d2129;
+			font-weight: 500;
+		}
+	}
+
+	.table-card {
+		margin: 24rpx 28rpx 0;
+		background: #fff;
+		border-radius: 20rpx;
+		overflow: hidden;
+		background-image: linear-gradient(180deg, #eef5ff 0%, #ffffff 120rpx);
+	}
+
+	.table-head {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 28rpx 28rpx 12rpx;
+
+		.table-title {
+			font-size: 30rpx;
+			color: #1d2129;
+			font-weight: 600;
+		}
+
+		.add-link {
+			display: flex;
+			align-items: center;
+			gap: 4rpx;
+			font-size: 28rpx;
+			color: $theme;
+		}
+	}
+
+	.empty-box {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		padding: 80rpx 40rpx 72rpx;
+
+		.empty-illust {
+			width: 180rpx;
+			height: 140rpx;
+			position: relative;
+			margin-bottom: 28rpx;
+
+			.box-lid {
+				position: absolute;
+				left: 20rpx;
+				top: 8rpx;
+				width: 140rpx;
+				height: 36rpx;
+				border: 8rpx solid #d0d5dd;
+				border-radius: 8rpx 8rpx 0 0;
+				border-bottom: none;
+				transform: rotate(-8deg);
+				transform-origin: left bottom;
+			}
+
+			.box-body {
+				position: absolute;
+				left: 24rpx;
+				bottom: 10rpx;
+				width: 132rpx;
+				height: 88rpx;
+				border: 8rpx solid #d0d5dd;
+				border-radius: 8rpx;
+				background: linear-gradient(180deg, #f7f8fa 0%, #eef0f3 100%);
+			}
+		}
+
+		.empty-text {
+			font-size: 28rpx;
+			color: #86909c;
+			margin-bottom: 40rpx;
+		}
+
+		.empty-btn {
+			min-width: 280rpx;
+			height: 72rpx;
+			padding: 0 40rpx;
+			border-radius: 40rpx;
+			border: 2rpx solid #a8cbff;
+			color: $theme;
+			font-size: 28rpx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			background: #fff;
+		}
+	}
+
+	.table-list {
+		padding: 0 8rpx 8rpx;
+	}
+
+	.list-row {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 34rpx 20rpx;
+		margin: 0 12rpx;
+		border-bottom: 1rpx solid #f0f2f5;
+
+		&:last-child {
+			border-bottom: none;
+		}
+
+		.row-left {
+			display: flex;
+			align-items: center;
+			gap: 20rpx;
+		}
+
+		.no-text {
+			font-size: 30rpx;
+			color: #1d2129;
+		}
+
+		.qr-entry {
+			display: flex;
+			align-items: center;
+			gap: 8rpx;
+			font-size: 26rpx;
+			color: #86909c;
+
+			.qr-ico {
+				width: 32rpx;
+				height: 32rpx;
+			}
+		}
+	}
+
+	.footer {
+		position: fixed;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		padding: 20rpx 40rpx calc(20rpx + env(safe-area-inset-bottom));
+		background: #fff;
+		z-index: 20;
+	}
+
+	.btn {
+		height: 88rpx;
+		border-radius: 44rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 30rpx;
+		font-weight: 500;
+
+		&.soft {
+			background: $theme-soft;
+			color: $theme;
+		}
+
+		&.primary {
+			background: $theme;
+			color: #fff;
+		}
+	}
+
+	.add-sheet,
+	.qr-sheet {
+		padding: 36rpx 40rpx calc(40rpx + env(safe-area-inset-bottom));
+		background: #fff;
+	}
+
+	.sheet-title {
+		text-align: center;
+		font-size: 34rpx;
+		font-weight: 600;
+		color: #1d2129;
+	}
+
+	.sheet-sub {
+		margin-top: 28rpx;
+		font-size: 28rpx;
+		color: #1d2129;
+		font-weight: 500;
+	}
+
+	.range-row {
+		margin-top: 28rpx;
+		display: flex;
+		align-items: center;
+		gap: 16rpx;
+
+		.range-input {
+			flex: 1;
+			height: 80rpx;
+			padding: 0 24rpx;
+			background: #f5f6f8;
+			border-radius: 12rpx;
+			font-size: 28rpx;
+			color: #333;
+			text-align: center;
+		}
+
+		.range-dash {
+			color: #86909c;
+			font-size: 32rpx;
+		}
+
+		.ph {
+			color: #c0c4cc;
+			font-size: 26rpx;
+		}
+	}
+
+	.sheet-btn {
+		margin-top: 48rpx;
+	}
+
+	.tip-dialog {
+		width: 560rpx;
+		background: #fff;
+		border-radius: 24rpx;
+		overflow: hidden;
+
+		.tip-title {
+			text-align: center;
+			padding-top: 40rpx;
+			font-size: 32rpx;
+			font-weight: 600;
+			color: #1d2129;
+		}
+
+		.tip-content {
+			text-align: center;
+			padding: 28rpx 40rpx 36rpx;
+			font-size: 28rpx;
+			color: #4e5969;
+			line-height: 1.5;
+		}
+
+		.tip-ok {
+			height: 96rpx;
+			border-top: 1rpx solid #f0f2f5;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			font-size: 30rpx;
+			color: $theme;
+			font-weight: 500;
+		}
+	}
+
+	.qr-block {
+		margin-top: 28rpx;
+		border: 1rpx solid #e5e6eb;
+		border-radius: 16rpx;
+		padding: 32rpx 24rpx;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+
+		.qr-img {
+			width: 280rpx;
+			height: 280rpx;
+			background: #f5f6f8;
+		}
+
+		.qr-label {
+			margin-top: 20rpx;
+			font-size: 26rpx;
+			color: #4e5969;
+		}
+	}
+</style>