瀏覽代碼

first commit

LiuShu_0203 4 月之前
當前提交
e48d140019

+ 2 - 0
.env.development

@@ -0,0 +1,2 @@
+VITE_API_BASE_URL=http://192.168.50.127:18001
+NODE_ENV=development

+ 2 - 0
.env.production

@@ -0,0 +1,2 @@
+VITE_API_BASE_URL=https://api.yourdomain.com
+NODE_ENV=production

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+.DS_Store
+node_modules
+uni_modules
+unpackage
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw*

+ 17 - 0
App.vue

@@ -0,0 +1,17 @@
+<script>
+	export default {
+		onLaunch: function() {
+			// console.log('App Launch')
+		},
+		onShow: function() {
+			// console.log('App Show')
+		},
+		onHide: function() {
+			// console.log('App Hide')
+		}
+	}
+</script>
+
+<style>
+	/*每个页面公共css */
+</style>

+ 395 - 0
components/l-echart/canvas.js

@@ -0,0 +1,395 @@
+import {getDeviceInfo} from './utils';
+
+const cacheChart = {}
+const fontSizeReg = /([\d\.]+)px/;
+class EventEmit {
+	constructor() {
+		this.__events = {};
+	}
+	on(type, listener) {
+		if (!type || !listener) {
+			return;
+		}
+		const events = this.__events[type] || [];
+		events.push(listener);
+		this.__events[type] = events;
+	}
+	emit(type, e) {
+		if (type.constructor === Object) {
+			e = type;
+			type = e && e.type;
+		}
+		if (!type) {
+			return;
+		}
+		const events = this.__events[type];
+		if (!events || !events.length) {
+			return;
+		}
+		events.forEach((listener) => {
+			listener.call(this, e);
+		});
+	}
+	off(type, listener) {
+		const __events = this.__events;
+		const events = __events[type];
+		if (!events || !events.length) {
+			return;
+		}
+		if (!listener) {
+			delete __events[type];
+			return;
+		}
+		for (let i = 0, len = events.length; i < len; i++) {
+			if (events[i] === listener) {
+				events.splice(i, 1);
+				i--;
+			}
+		}
+	}
+}
+class Image {
+	constructor() {
+		this.currentSrc = null
+		this.naturalHeight = 0
+		this.naturalWidth = 0
+		this.width = 0
+		this.height = 0
+		this.tagName = 'IMG'
+	}
+	set src(src) {
+		this.currentSrc = src
+		uni.getImageInfo({
+			src,
+			success: (res) => {
+				this.naturalWidth = this.width = res.width
+				this.naturalHeight = this.height = res.height
+				this.onload()
+			},
+			fail: () => {
+				this.onerror()
+			}
+		})
+	}
+	get src() {
+		return this.currentSrc
+	}
+}
+class OffscreenCanvas {
+	constructor(ctx, com, canvasId) {
+		this.tagName = 'canvas'
+		this.com = com
+		this.canvasId = canvasId
+		this.ctx = ctx
+	}
+	set width(w) {
+		this.com.offscreenWidth = w
+	}
+	set height(h) {
+		this.com.offscreenHeight = h
+	}
+	get width() {
+		return this.com.offscreenWidth || 0
+	}
+	get height() {
+		return this.com.offscreenHeight || 0
+	}
+	getContext(type) {
+		return this.ctx
+	}
+	getImageData() {
+		return new Promise((resolve, reject) => {
+			this.com.$nextTick(() => {
+				uni.canvasGetImageData({
+					x:0,
+					y:0,
+					width: this.com.offscreenWidth,
+					height: this.com.offscreenHeight,
+					canvasId: this.canvasId,
+					success: (res) => {
+						resolve(res)
+					},
+					fail: (err) => {
+						reject(err)
+					},
+				}, this.com)
+			})
+		})
+	}
+}
+export class Canvas {
+	constructor(ctx, com, isNew, canvasNode={}) {
+		cacheChart[com.canvasId] = {ctx}
+		this.canvasId = com.canvasId;
+		this.chart = null;
+		this.isNew = isNew
+		this.tagName = 'canvas'
+		this.canvasNode = canvasNode;
+		this.com = com;
+		if (!isNew) {
+			this._initStyle(ctx)
+		}
+		this._initEvent();
+		this._ee = new EventEmit()
+	}
+	getContext(type) {
+		if (type === '2d') {
+			return this.ctx;
+		}
+	}
+	setAttribute(key, value) {
+		if(key === 'aria-label') {
+			this.com['ariaLabel'] = value
+		}
+	}
+	setChart(chart) {
+		this.chart = chart;
+	}
+	createOffscreenCanvas(param){
+		if(!this.children) {
+			this.com.isOffscreenCanvas = true
+			this.com.offscreenWidth = param.width||300
+			this.com.offscreenHeight = param.height||300
+			const com = this.com
+			const canvasId = this.com.offscreenCanvasId
+			const context = uni.createCanvasContext(canvasId, this.com)
+			this._initStyle(context)
+			this.children = new OffscreenCanvas(context, com, canvasId)
+		} 
+		return this.children
+	}
+	appendChild(child) {
+		console.log('child', child)
+	}
+	dispatchEvent(type, e) {
+		if(typeof type == 'object') {
+			this._ee.emit(type.type, type);
+		} else {
+			this._ee.emit(type, e);
+		}
+		return true
+	}
+	attachEvent() {
+	}
+	detachEvent() {
+	}
+	addEventListener(type, listener) {
+		this._ee.on(type, listener)
+	}
+	removeEventListener(type, listener) {
+		this._ee.off(type, listener)
+	}
+	_initCanvas(zrender, ctx) {
+		// zrender.util.getContext = function() {
+		// 	return ctx;
+		// };
+		// zrender.util.$override('measureText', function(text, font) {
+		// 	ctx.font = font || '12px sans-serif';
+		// 	return ctx.measureText(text, font);
+		// });
+	}
+	_initStyle(ctx, child) {
+		const styles = [
+			'fillStyle',
+			'strokeStyle',
+			'fontSize',
+			'globalAlpha',
+			'opacity',
+			'textAlign',
+			'textBaseline',
+			'shadow',
+			'lineWidth',
+			'lineCap',
+			'lineJoin',
+			'lineDash',
+			'miterLimit',
+			// #ifdef H5
+			'font',
+			// #endif
+		];
+		const colorReg = /#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])\b/g;
+		styles.forEach(style => {
+			Object.defineProperty(ctx, style, {
+				set: value => {
+					// #ifdef H5
+					if (style === 'font' && fontSizeReg.test(value)) {
+						const match = fontSizeReg.exec(value);
+						ctx.setFontSize(match[1]);
+						return;
+					}
+					// #endif
+					
+					if (style === 'opacity') {
+						ctx.setGlobalAlpha(value)
+						return;
+					}
+					if (style !== 'fillStyle' && style !== 'strokeStyle' || value !== 'none' && value !== null) {
+						// #ifdef H5 || APP-PLUS || MP-BAIDU
+						if(typeof value == 'object') {
+							if (value.hasOwnProperty('colorStop') || value.hasOwnProperty('colors')) {
+								ctx['set' + style.charAt(0).toUpperCase() + style.slice(1)](value);
+							}
+							return
+						} 
+						// #endif
+						// #ifdef MP-TOUTIAO
+						if(colorReg.test(value)) {
+							value = value.replace(colorReg, '#$1$1$2$2$3$3')
+						}
+						// #endif
+						ctx['set' + style.charAt(0).toUpperCase() + style.slice(1)](value);
+					}
+				}
+			});
+		});
+		if(!this.isNew && !child) {
+			ctx.uniDrawImage = ctx.drawImage
+			ctx.drawImage = (...a) => {
+				a[0] = a[0].src
+				ctx.uniDrawImage(...a)
+			}
+		}
+		if(!ctx.createRadialGradient) {
+			ctx.createRadialGradient = function() {
+				return ctx.createCircularGradient(...[...arguments].slice(-3))
+			};
+		}
+		// 字节不支持
+		if (!ctx.strokeText) {
+			ctx.strokeText = (...a) => {
+				ctx.fillText(...a)
+			}
+		}
+		
+		// 钉钉不支持 , 鸿蒙是异步
+		if (!ctx.measureText || getDeviceInfo().osName === 'harmonyos') {
+			ctx._measureText = ctx.measureText
+			const strLen = (str) => {
+				let len = 0;
+				for (let i = 0; i < str.length; i++) {
+					if (str.charCodeAt(i) > 0 && str.charCodeAt(i) < 128) {
+						len++;
+					} else {
+						len += 2;
+					}
+				}
+				return len;
+			}
+			ctx.measureText = (text, font) => {
+				let fontSize = ctx?.state?.fontSize || 12;
+				if (font) {
+					fontSize = parseInt(font.match(/([\d\.]+)px/)[1])
+				}
+				fontSize /= 2;
+				let isBold = fontSize >= 16;
+				const widthFactor = isBold ? 1.3 : 1;
+				// ctx._measureText(text, (res) => {})
+				return {
+					width: strLen(text) * fontSize * widthFactor
+				};
+			}
+		}
+	}
+
+	_initEvent(e) {
+		this.event = {};
+		const eventNames = [{
+			wxName: 'touchStart',
+			ecName: 'mousedown'
+		}, {
+			wxName: 'touchMove',
+			ecName: 'mousemove'
+		}, {
+			wxName: 'touchEnd',
+			ecName: 'mouseup'
+		}, {
+			wxName: 'touchEnd',
+			ecName: 'click'
+		}];
+
+		eventNames.forEach(name => {
+			this.event[name.wxName] = e => {
+				const touch = e.touches[0];
+				this.chart.getZr().handler.dispatch(name.ecName, {
+					zrX: name.wxName === 'tap' ? touch.clientX : touch.x,
+					zrY: name.wxName === 'tap' ? touch.clientY : touch.y
+				});
+			};
+		});
+	}
+
+	set width(w) {
+		this.canvasNode.width = w
+	}
+	set height(h) {
+		this.canvasNode.height = h
+	}
+
+	get width() {
+		return this.canvasNode.width || 0
+	}
+	get height() {
+		return this.canvasNode.height || 0
+	}
+	get ctx() {
+		return cacheChart[this.canvasId]['ctx'] || null
+	}
+	set chart(chart) {
+		cacheChart[this.canvasId]['chart'] = chart
+	}
+	get chart() {
+		return cacheChart[this.canvasId]['chart'] || null
+	}
+}
+
+export function dispatch(name, {x,y, wheelDelta}) {
+	this.dispatch(name, {
+		zrX: x,
+		zrY: y,
+		zrDelta: wheelDelta,
+		preventDefault: () => {},
+		stopPropagation: () =>{}
+	});
+}
+export function setCanvasCreator(echarts, {canvas, node}) {
+	// echarts.setCanvasCreator(() => canvas);
+	if(echarts && !echarts.registerPreprocessor) {
+		return console.warn('echarts 版本不对或未传入echarts,vue3请使用esm格式')
+	}
+	echarts.registerPreprocessor(option => {
+		if (option && option.series) {
+			if (option.series.length > 0) {
+				option.series.forEach(series => {
+					series.progressive = 0;
+				});
+			} else if (typeof option.series === 'object') {
+				option.series.progressive = 0;
+			}
+		}
+	});
+	function loadImage(src, onload, onerror) {
+		let img = null
+		if(node && node.createImage) {
+			img = node.createImage()
+			img.onload = onload.bind(img);
+			img.onerror = onerror.bind(img);
+			img.src = src;
+			return img
+		} else {
+			img = new Image()
+			img.onload = onload.bind(img)
+			img.onerror = onerror.bind(img);
+			img.src = src
+			return img
+		}
+	}
+	if(echarts.setPlatformAPI) {
+		echarts.setPlatformAPI({
+			loadImage: canvas.setChart ? loadImage : null,
+			createCanvas(){
+				const key = 'createOffscreenCanvas'
+				return uni.canIUse(key) && uni[key] ? uni[key]({type: '2d'}) : canvas
+			}
+		})
+	}
+}

+ 252 - 0
components/l-echart/l-echart.uvue

@@ -0,0 +1,252 @@
+<template>
+	<!-- #ifdef APP -->
+	<web-view class="lime-echart" ref="chartRef" @load="loaded" :style="[customStyle]" 
+		:webview-styles="[webviewStyles]" src="/uni_modules/lime-echart/static/uvue.html?v=10112">
+	</web-view>
+	<!-- #endif -->
+	<!-- #ifdef H5 -->
+	<div class="lime-echart" ref="chartRef"></div>
+	<!-- #endif -->
+</template>
+
+<script lang="uts" setup>
+	// @ts-nocheck
+	import { Echarts } from './uvue';
+	type EchartsResolve = (value : Echarts) => void
+	defineOptions({
+		name: 'l-echart'
+	})
+	const emits = defineEmits(['finished'])
+	const props = defineProps({
+		// #ifdef APP
+		webviewStyles: {
+			type: Object
+		},
+		customStyle: {
+			type: Object
+		},
+		// #endif
+		// #ifndef APP
+		webviewStyles: {
+			type: Object
+		},
+		customStyle: {
+			type: [String, Object]
+		},
+		// #endif
+		isDisableScroll: {
+			type: Boolean,
+			default: false
+		},
+		isClickable: {
+			type: Boolean,
+			default: true
+		},
+		enableHover: {
+			type: Boolean,
+			default: false
+		},
+		beforeDelay: {
+			type: Number,
+			default: 30
+		}
+	})
+
+	const finished = ref(false)
+	const map = [] as EchartsResolve[]
+	const callbackMap = [] as EchartsResolve[]
+	// let context = null as UniWebViewElement | null
+	let chart = null as Echarts | null
+	let chartRef = ref<UniWebViewElement | null>(null)
+	
+	const trigger = () => {
+		// #ifdef APP
+		if (finished.value) {
+			if (chart == null) {
+				chart = new Echarts(chartRef.value!)
+			}
+			while (map.length > 0) {
+				const resolve = map.pop() as EchartsResolve
+				resolve(chart!)
+			}
+		}
+		// #endif
+		// #ifdef H5
+		while (map.length > 0) {
+			if(chart != null){
+				const resolve = map.pop() as EchartsResolve
+				resolve(chart!)
+			}
+		}
+		// #endif
+		
+		if(chart != null){
+			while(callbackMap.length > 0){
+				const callback = callbackMap.pop() as EchartsResolve
+				callback(chart!)
+			}
+		}
+	}
+	
+	// #ifdef APP
+	const loaded = (event : UniWebViewLoadEvent) => {
+		event.stopPropagation()
+		event.preventDefault()
+		finished.value = true
+		trigger()
+		emits('finished')
+	}
+	// #endif
+	
+	
+	const _next = () : boolean => {
+		if (chart == null) {
+			console.warn(`组件还未初始化,请先使用 init`)
+			return true
+		}
+		return false
+	}
+	const setOption = (option : UTSJSONObject) => {
+		if (_next()) return
+		chart!.setOption(option);
+	}
+	const showLoading = () => {
+		if (_next()) return
+		chart!.showLoading();
+	}
+	const hideLoading = () => {
+		if (_next()) return
+		chart!.hideLoading();
+	}
+	const clear = () => {
+		if (_next()) return
+		chart!.clear();
+	}
+	const dispose = () => {
+		if (_next()) return
+		chart!.dispose();
+	}
+	const resize = (size : UTSJSONObject) => {
+		if (_next()) return
+		chart!.resize(size);
+	}
+	const canvasToTempFilePath = (opt : UTSJSONObject) => {
+		if (_next()) return
+		chart!.canvasToTempFilePath(opt);
+	}
+	// function init() : Promise<Echarts> {
+	// 	return new Promise((resolve) => {
+	// 		map.push(resolve)
+	// 		trigger()
+	// 	})
+	// }
+	// #ifdef APP
+	function init(callback : ((chart : Echarts) => void) | null) : Promise<Echarts> {
+		// if (chart !== null && callback != null) {
+		// 	callback(chart!)
+		// } else {
+		// 	console.warn('echarts 未加载完成,您可以延时一下')
+		// }
+		if(callback!=null){
+			callbackMap.push(callback)
+		}
+		return new Promise<Echarts>((resolve) => {
+			map.push(resolve)
+			trigger()
+		})
+	}
+	// #endif
+	// #ifdef H5
+	const touchstart = (e) => {
+		if(chart == null) return
+		const handler = chart.getZr().handler;
+		const rect = chart.getZr().dom.getBoundingClientRect()
+		handler.dispatch('mousedown', {
+			zrX: e.touches[0].clientX - rect.left,
+			zrY: e.touches[0].clientY - rect.top
+		})
+		handler.dispatch('click', {
+			zrX: e.touches[0].clientX - rect.left,
+			zrY: e.touches[0].clientY - rect.top
+		})
+	}
+	const touchmove = (e) => {
+		if(chart == null) return
+		const handler = chart.getZr().handler;
+		const rect = chart.getZr().dom.getBoundingClientRect()
+		handler.dispatch('mousemove', {
+			zrX: e.touches[0].clientX - rect.left,
+			zrY: e.touches[0].clientY - rect.top
+		})
+	}
+	const mouseup = (e) => {
+		if(chart == null) return
+		const handler = chart.getZr().handler;
+		handler.dispatch('mousemove', {
+			zrX: 999999999,
+			zrY: 999999999
+		})
+		handler.dispatch('mouseup', {
+			zrX: 999999999,
+			zrY: 999999999
+		})
+	}
+	function init(echarts: any, ...args: any[]): Promise<Echarts>{
+		if(echarts == null){
+			console.error('请确保已经引入了 ECharts 库');
+			return Promise.reject('请确保已经引入了 ECharts 库');
+		}
+		let theme:string|null=null
+		let opts={}
+		let callback:Function|null=null;
+		
+		args.forEach(item =>{
+			if(typeof item === 'function') {
+				callback = item
+			} else if(['string'].includes(typeof item)){
+				theme = item
+			} else if(typeof item === 'object'){
+				opts = item
+			}
+		})
+		chart = echarts.init(chartRef.value, theme, opts)
+		window.addEventListener('touchstart', touchstart)
+		window.addEventListener('touchmove', touchmove)
+		window.addEventListener('touchend', mouseup)
+		
+		if(callback!=null && typeof callback == 'function'){
+			callbackMap.push(callback)
+		}
+		return new Promise<Echarts>((resolve) => {
+			map.push(resolve)
+			trigger()
+		})
+	}
+	onMounted(()=>{
+		finished.value = true
+		trigger()
+		emits('finished')
+	})
+	onUnmounted(()=>{
+		window.removeEventListener('touchstart', touchstart)
+		window.removeEventListener('touchmove', touchmove)
+		window.removeEventListener('touchend', mouseup)
+	})
+	// #endif
+	
+	defineExpose({
+		init,
+		setOption,
+		showLoading,
+		hideLoading,
+		clear,
+		dispose,
+		resize,
+		canvasToTempFilePath
+	})
+</script>
+<style lang="scss">
+	.lime-echart {
+		flex: 1;
+	}
+</style>

+ 514 - 0
components/l-echart/l-echart.vue

@@ -0,0 +1,514 @@
+<template>
+	<view class="lime-echart" :style="customStyle" v-if="canvasId" ref="limeEchart" :aria-label="ariaLabel">
+		<!-- #ifndef APP-NVUE -->
+		<canvas
+			class="lime-echart__canvas"
+			v-if="use2dCanvas"
+			type="2d"
+			:id="canvasId"
+			:style="canvasStyle"
+			:disable-scroll="isDisableScroll"
+			@touchstart="touchStart"
+			@touchmove="touchMove"
+			@touchend="touchEnd"
+		/>
+		<canvas
+			class="lime-echart__canvas"
+			v-else
+			:width="nodeWidth"
+			:height="nodeHeight"
+			:style="canvasStyle"
+			:canvas-id="canvasId"
+			:id="canvasId"
+			:disable-scroll="isDisableScroll"
+			@touchstart="touchStart"
+			@touchmove="touchMove"
+			@touchend="touchEnd"
+		/>
+		<view class="lime-echart__mask"
+			v-if="isPC"
+			@mousedown="touchStart"
+			@mousemove="touchMove"
+			@mouseup="touchEnd"
+			@touchstart="touchStart"
+			@touchmove="touchMove"
+			@touchend="touchEnd">
+		</view>
+		<canvas v-if="isOffscreenCanvas" :style="offscreenStyle" :canvas-id="offscreenCanvasId"></canvas>
+		<!-- #endif -->
+		<!-- #ifdef APP-NVUE -->
+		<web-view
+			class="lime-echart__canvas"
+			:id="canvasId"
+			:style="canvasStyle"
+			:webview-styles="webviewStyles"
+			ref="webview"
+			src="/uni_modules/lime-echart/static/uvue.html?v=1"
+			@pagefinish="finished = true"
+			@onPostMessage="onMessage"
+		></web-view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+// #ifndef APP-NVUE
+import {Canvas, setCanvasCreator, dispatch} from './canvas';
+import {wrapTouch, convertTouchesToArray, devicePixelRatio ,sleep, canIUseCanvas2d, getRect, getDeviceInfo} from './utils';
+// #endif
+// #ifdef APP-NVUE
+import { base64ToPath, sleep } from './utils';
+import {Echarts} from './nvue'
+// #endif
+const charts = {}
+const echartsObj = {}
+
+
+/**
+ * LimeChart 图表
+ * @description 全端兼容的eCharts
+ * @tutorial https://ext.dcloud.net.cn/plugin?id=4899
+
+ * @property {String} customStyle 自定义样式
+ * @property {String} type 指定 canvas 类型
+ * @value 2d 使用canvas 2d,部分小程序支持
+ * @value '' 使用原生canvas,会有层级问题
+ * @value bottom right	不缩放图片,只显示图片的右下边区域
+ * @property {Boolean} isDisableScroll	 
+ * @property {number} beforeDelay = [30]  延迟初始化 (毫秒)
+ * @property {Boolean} enableHover PC端使用鼠标悬浮
+
+ * @event {Function} finished 加载完成触发
+ */
+export default {
+	name: 'lime-echart',
+	props: {
+		// #ifdef MP-WEIXIN || MP-TOUTIAO
+		type: {
+			type: String,
+			default: '2d'
+		},
+		// #endif
+		// #ifdef APP-NVUE
+		webviewStyles: Object,
+		// hybrid: Boolean,
+		// #endif
+		customStyle: String,
+		isDisableScroll: Boolean,
+		isClickable: {
+			type: Boolean,
+			default: true
+		},
+		enableHover: Boolean,
+		beforeDelay: {
+			type: Number,
+			default: 30
+		}
+	},
+	data() {
+		return {
+			// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
+			use2dCanvas: true,
+			// #endif
+			// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
+			use2dCanvas: false,
+			// #endif
+			ariaLabel: '图表',
+			width: null,
+			height: null,
+			nodeWidth: null,
+			nodeHeight: null,
+			// canvasNode: null,
+			config: {},
+			inited: false,
+			finished: false,
+			file: '',
+			platform: '',
+			isPC: false,
+			isDown: false,
+			isOffscreenCanvas: false,
+			offscreenWidth: 0,
+			offscreenHeight: 0
+		};
+	},
+	computed: {
+		canvasId() {
+			return `lime-echart${this._ && this._.uid || this._uid}`
+		},
+		offscreenCanvasId() {
+			return `${this.canvasId}_offscreen`
+		},
+		offscreenStyle() {
+			return `width:${this.offscreenWidth}px;height: ${this.offscreenHeight}px; position: fixed; left: 99999px; background: red`
+		},
+		canvasStyle() {
+			return  this.width && this.height ? ('width:' + this.width + 'px;height:' + this.height + 'px') : ''
+		}
+	},
+	// #ifndef VUE3
+	beforeDestroy() {
+		this.clear()
+		this.dispose()
+		// #ifdef H5
+		if(this.isPC) {
+			document.removeEventListener('mousewheel', this.mousewheel)
+		}
+		// #endif
+	},
+	// #endif
+	// #ifdef VUE3
+	beforeUnmount() {
+		this.clear()
+		this.dispose()
+		// #ifdef H5
+		if(this.isPC) {
+			document.removeEventListener('mousewheel', this.mousewheel)
+		}
+		// #endif
+	},
+	// #endif
+	created() {
+		// #ifdef H5
+		if(!('ontouchstart' in window)) {
+			this.isPC = true
+			document.addEventListener('mousewheel', this.mousewheel)
+		}
+		// #endif
+		// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
+		const { platform } = getDeviceInfo();
+		this.isPC = /windows/i.test(platform)
+		// #endif
+		this.use2dCanvas = this.type === '2d' && canIUseCanvas2d()
+	},
+	mounted() {
+		this.$nextTick(() => {
+			this.$emit('finished')
+		})
+	},
+	methods: {
+		// #ifdef APP-NVUE
+		onMessage(e) {
+			const detail = e?.detail?.data[0] || null;
+			const data = detail?.data
+			const key = detail?.event
+			const options = data?.options
+			const event = data?.event
+			const file = detail?.file
+			if (key == 'log' && data) {
+				console.log(data)
+			}
+			if(event) {
+				this.chart.dispatchAction(event.replace(/"/g,''), options)
+			}
+			if(file) {
+				thie.file = file
+			}
+		},
+		// #endif
+		setChart(callback) {
+			if(!this.chart) {
+				console.warn(`组件还未初始化,请先使用 init`)
+				return
+			}
+			if(typeof callback === 'function' && this.chart) {
+				callback(this.chart);
+			}
+			// #ifdef APP-NVUE
+			if(typeof callback === 'function') {
+				this.$refs.webview.evalJs(`setChart(${JSON.stringify(callback.toString())}, ${JSON.stringify(this.chart.options)})`);
+			}
+			// #endif
+		},
+		setOption() {
+			if (!this.chart || !this.chart.setOption) {
+				console.warn(`组件还未初始化,请先使用 init`)
+				return
+			}
+			this.chart.setOption(...arguments);
+		},
+		showLoading() {
+			if(this.chart) {
+				this.chart.showLoading(...arguments)
+			}
+		},
+		hideLoading() {
+			if(this.chart) {
+				this.chart.hideLoading()
+			}
+		},
+		clear() {
+			if(this.chart) {
+				this.chart.clear()
+			}
+		},
+		dispose() {
+			if(this.chart) {
+				this.chart.dispose()
+			}
+		},
+		resize(size) {
+			if(size && size.width && size.height) {
+				this.height = size.height
+				this.width = size.width
+				if(this.chart) {this.chart.resize(size)}
+			} else {
+				this.$nextTick(() => {
+					uni.createSelectorQuery()
+						.in(this)
+						.select(`.lime-echart`)
+						.boundingClientRect()
+						.exec(res => {
+							if (res) {
+								let { width, height } = res[0];
+								this.width = width = width || 300;
+								this.height = height = height || 300;
+								this.chart.resize({width, height})
+							}
+						});
+				})
+				
+			}
+			
+		},
+		canvasToTempFilePath(args = {}) {
+			// #ifndef APP-NVUE
+			const { use2dCanvas, canvasId } = this;
+			return new Promise((resolve, reject) => {
+				const copyArgs = Object.assign({
+					canvasId,
+					success: resolve,
+					fail: reject
+				}, args);
+				if (use2dCanvas) {
+					delete copyArgs.canvasId;
+					copyArgs.canvas = this.canvasNode;
+				}
+				uni.canvasToTempFilePath(copyArgs, this);
+			});
+			// #endif
+			// #ifdef APP-NVUE
+			this.file = ''
+			this.$refs.webview.evalJs(`canvasToTempFilePath()`);
+			return new Promise((resolve, reject) => {
+				this.$watch('file', async (file) => {
+					if(file) {
+						const tempFilePath = await base64ToPath(file)
+						resolve(args.success({tempFilePath}))
+					} else {
+						reject(args.fail({error: ``}))
+					}
+				})
+			})
+			// #endif
+		},
+		async init(echarts, ...args) {
+			// #ifndef APP-NVUE
+			if(args && args.length == 0 && !echarts) {
+				console.error('缺少参数:init(echarts, theme?:string, opts?: object, callback?: function)')
+				return
+			}
+			// #endif
+			let theme=null,opts={},callback;
+			
+			Array.from(arguments).forEach(item => {
+				if(typeof item === 'function') {
+					callback = item
+				}
+				if(['string'].includes(typeof item)) {
+					theme = item
+				}
+				if(typeof item === 'object') {
+					opts = item
+				}
+			})
+			
+			if(this.beforeDelay) {
+				await sleep(this.beforeDelay)
+			}
+			let config = await this.getContext();
+			// #ifndef APP-NVUE
+			setCanvasCreator(echarts, config)
+			try {
+				this.chart = echarts.init(config.canvas, theme, Object.assign({}, config, opts))
+				if(typeof callback === 'function') {
+					callback(this.chart)
+				} else {
+					return this.chart
+				}
+			} catch(e) {
+				console.error(e.messges)
+				return null
+			}
+			// #endif
+			// #ifdef APP-NVUE
+			this.chart = new Echarts(this.$refs.webview)
+			this.$refs.webview.evalJs(`init(null, null, ${JSON.stringify(opts)}, ${theme})`)
+			if(callback) {
+				callback(this.chart)
+			} else {
+				return this.chart
+			}
+			// #endif
+		},
+		getContext() {
+			// #ifdef APP-NVUE
+			if(this.finished) {
+				return Promise.resolve(this.finished)
+			}
+			return new Promise(resolve => {
+				this.$watch('finished', (val) => {
+					if(val) {
+						resolve(this.finished)
+					}
+				})
+			})
+			// #endif
+			// #ifndef APP-NVUE
+			return getRect(`#${this.canvasId}`, {context: this, type: this.use2dCanvas ? 'fields': 'boundingClientRect'}).then(res => {
+				if(res) {
+					let dpr = devicePixelRatio
+					let {width, height, node} = res
+					let canvas;
+					this.width = width = width || 300;
+					this.height = height = height || 300;
+					if(node) {
+						const ctx = node.getContext('2d');
+						canvas = new Canvas(ctx, this, true, node);
+						this.canvasNode = node
+					} else {
+						// #ifdef MP-TOUTIAO
+						dpr = !this.isPC ? devicePixelRatio : 1// 1.25
+						// #endif
+						// #ifndef MP-ALIPAY || MP-TOUTIAO
+						dpr = this.isPC ? devicePixelRatio : 1
+						// #endif
+						// #ifdef MP-ALIPAY || MP-LARK
+						dpr = devicePixelRatio
+						// #endif
+						// #ifdef WEB
+						dpr = 1
+						// #endif
+						this.rect = res
+						this.nodeWidth = width * dpr;
+						this.nodeHeight = height * dpr;
+						const ctx = uni.createCanvasContext(this.canvasId, this);
+						canvas =  new Canvas(ctx, this, false);
+					}
+					return { canvas, width, height, devicePixelRatio: dpr, node };
+				} else {
+					return {}
+				}
+			})
+			// #endif
+		},
+		// #ifndef APP-NVUE
+		getRelative(e, touches) {
+			let { clientX, clientY } = e
+			if(!(clientX && clientY) && touches && touches[0]) {
+				clientX = touches[0].clientX
+				clientY = touches[0].clientY
+			}
+			return {x: clientX - this.rect.left, y: clientY - this.rect.top, wheelDelta: e.wheelDelta || 0}
+		},
+		getTouch(e, touches) {
+			const {x} = touches && touches[0] || {}
+			return x ? touches[0] : this.getRelative(e, touches);
+		},
+		touchStart(e) {
+			this.isDown = true
+			const next = () => {
+				const touches = convertTouchesToArray(e.touches)
+				if(this.chart) {
+					const touch = this.getTouch(e, touches)
+					this.startX = touch.x
+					this.startY = touch.y
+					this.startT = new Date()
+					const handler = this.chart.getZr().handler;
+					dispatch.call(handler, 'mousedown', touch)
+					dispatch.call(handler, 'mousemove', touch)
+					handler.processGesture(wrapTouch(e), 'start');
+					clearTimeout(this.endTimer);
+				}
+				
+			}
+			if(this.isPC) {
+				getRect(`#${this.canvasId}`, {context: this}).then(res => {
+					this.rect = res
+					next()
+				})
+				return
+			}
+			next()
+		},
+		touchMove(e) {
+			if(this.isPC && this.enableHover && !this.isDown) {this.isDown = true}
+			const touches = convertTouchesToArray(e.touches)
+			if (this.chart && this.isDown) {
+				const handler = this.chart.getZr().handler;
+				dispatch.call(handler, 'mousemove', this.getTouch(e, touches))
+				handler.processGesture(wrapTouch(e), 'change');
+			}
+			
+		},
+		touchEnd(e) {
+			this.isDown = false
+			if (this.chart) {
+				const touches = convertTouchesToArray(e.changedTouches)
+				const {x} = touches && touches[0] || {}
+				const touch = (x ? touches[0] : this.getRelative(e, touches)) || {};
+				const handler = this.chart.getZr().handler;
+				const isClick = Math.abs(touch.x - this.startX) < 10 && new Date() - this.startT < 200;
+				dispatch.call(handler, 'mouseup', touch)
+				handler.processGesture(wrapTouch(e), 'end');
+				if(isClick) {
+					dispatch.call(handler, 'click', touch)
+				} else {
+					this.endTimer = setTimeout(() => {
+						dispatch.call(handler, 'mousemove', {x: 999999999,y: 999999999});
+						dispatch.call(handler, 'mouseup', {x: 999999999,y: 999999999});
+					},50)
+				}
+			}
+		},
+		// #endif
+		// #ifdef H5
+		mousewheel(e){
+			if(this.chart) {
+				dispatch.call(this.chart.getZr().handler, 'mousewheel', this.getTouch(e))
+			}
+		}
+		// #endif
+	}
+};
+</script>
+<style>	
+.lime-echart {
+	position: relative;
+	/* #ifndef APP-NVUE */
+	width: 100%;
+	height: 100%;
+	/* #endif */
+	/* #ifdef APP-NVUE */
+	flex: 1;
+	/* #endif */
+}
+.lime-echart__canvas {
+	/* #ifndef APP-NVUE */
+	width: 100%;
+	height: 100%;
+	/* #endif */
+	/* #ifdef APP-NVUE */
+	flex: 1;
+	/* #endif */
+}
+/* #ifndef APP-NVUE */
+.lime-echart__mask {
+	position: absolute;
+	width: 100%;
+	height: 100%;
+	left: 0;
+	top: 0;
+	z-index: 1;
+}
+/* #endif */
+</style>

+ 51 - 0
components/l-echart/nvue.js

@@ -0,0 +1,51 @@
+export class Echarts {
+	eventMap = new Map()
+	constructor(webview) {
+		this.webview = webview
+		this.options = null
+	}
+	setOption() {
+		this.options = arguments
+		this.webview.evalJs(`setOption(${JSON.stringify(arguments)})`);
+	}
+	getOption() {
+		return this.options
+	}
+	showLoading() {
+		this.webview.evalJs(`showLoading(${JSON.stringify(arguments)})`);
+	}
+	hideLoading() {
+		this.webview.evalJs(`hideLoading()`);
+	}
+	clear() {
+		this.webview.evalJs(`clear()`);
+	}
+	dispose() {
+		this.webview.evalJs(`dispose()`);
+	}
+	resize(size) {
+		if(size) {
+			this.webview.evalJs(`resize(${JSON.stringify(size)})`);
+		} else {
+			this.webview.evalJs(`resize()`);
+		}
+	}
+	on(type, ...args) {
+		const query = args[0]
+		const useQuery = query && typeof query != 'function'
+		const param = useQuery ? [type, query] : [type]
+		const key = `${type}${useQuery ? JSON.stringify(query): '' }`
+		const callback = useQuery ? args[1]: args[0]
+		if(typeof callback == 'function'){
+			this.eventMap.set(key, callback)
+		}
+		this.webview.evalJs(`on(${JSON.stringify(param)})`);
+		console.warn('nvue 暂不支持事件')
+	}
+	dispatchAction(type, options){
+		const handler = this.eventMap.get(type)
+		if(handler){
+			handler(options)
+		}
+	}
+}

+ 183 - 0
components/l-echart/utils.js

@@ -0,0 +1,183 @@
+/**
+ * 获取设备基础信息
+ *
+ * @see [uni.getDeviceInfo](https://uniapp.dcloud.net.cn/api/system/getDeviceInfo.html)
+ */
+export function getDeviceInfo() {
+	if (uni.canIUse('getDeviceInfo') || uni.getDeviceInfo) {
+		return uni.getDeviceInfo();
+	} else {
+		return uni.getSystemInfoSync();
+	}
+}
+
+/**
+ * 获取窗口信息
+ *
+ * @see [uni.getWindowInfo](https://uniapp.dcloud.net.cn/api/system/getWindowInfo.html)
+ */
+export function getWindowInfo() {
+	if (uni.canIUse('getWindowInfo') || uni.getWindowInfo) {
+		return uni.getWindowInfo();
+	} else {
+		return uni.getSystemInfoSync();
+	}
+}
+
+/**
+ * 获取APP基础信息
+ *
+ * @see [uni.getAppBaseInfo](https://uniapp.dcloud.net.cn/api/system/getAppBaseInfo.html)
+ */
+export function getAppBaseInfo() {
+	if (uni.canIUse('getAppBaseInfo') || uni.getAppBaseInfo) {
+		return uni.getAppBaseInfo();
+	} else {
+		return uni.getSystemInfoSync();
+	}
+}
+
+// #ifndef APP-NVUE
+// 计算版本
+export function compareVersion(v1, v2) {
+	v1 = v1.split('.')
+	v2 = v2.split('.')
+	const len = Math.max(v1.length, v2.length)
+	while (v1.length < len) {
+		v1.push('0')
+	}
+	while (v2.length < len) {
+		v2.push('0')
+	}
+	for (let i = 0; i < len; i++) {
+		const num1 = parseInt(v1[i], 10)
+		const num2 = parseInt(v2[i], 10)
+
+		if (num1 > num2) {
+			return 1
+		} else if (num1 < num2) {
+			return -1
+		}
+	}
+	return 0
+}
+
+function gte(version) {
+	// 截止 2023-03-22 mac pc小程序不支持 canvas 2d
+	const { platform } = getDeviceInfo();
+	let { SDKVersion } = getAppBaseInfo();
+	// #ifdef MP-ALIPAY
+	SDKVersion = my.SDKVersion;
+	// #endif
+	// #ifdef MP-WEIXIN
+	return platform !== 'mac' && compareVersion(SDKVersion, version) >= 0;
+	// #endif
+	return compareVersion(SDKVersion, version) >= 0;
+}
+
+
+export function canIUseCanvas2d() {
+	// #ifdef MP-WEIXIN
+	return gte('2.9.0');
+	// #endif
+	// #ifdef MP-ALIPAY
+	return gte('2.7.0');
+	// #endif
+	// #ifdef MP-TOUTIAO
+	return gte('1.78.0');
+	// #endif
+	return false
+}
+
+export function convertTouchesToArray(touches) {
+	// 如果 touches 是一个数组,则直接返回它
+	if (Array.isArray(touches)) {
+		return touches;
+	}
+	// 如果touches是一个对象,则转换为数组
+	if (typeof touches === 'object' && touches !== null) {
+		return Object.values(touches);
+	}
+	// 对于其他类型,直接返回它
+	return touches;
+}
+
+export function wrapTouch(event) {
+	for (let i = 0; i < event.touches.length; ++i) {
+		const touch = event.touches[i];
+		touch.offsetX = touch.x;
+		touch.offsetY = touch.y;
+	}
+	return event;
+}
+
+export const devicePixelRatio = getWindowInfo().pixelRatio;
+
+// #endif
+// #ifdef APP-NVUE
+export function base64ToPath(base64) {
+	return new Promise((resolve, reject) => {
+		const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
+		const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
+		bitmap.loadBase64Data(base64, () => {
+			if (!format) {
+				reject(new Error('ERROR_BASE64SRC_PARSE'))
+			}
+			const time = new Date().getTime();
+			const filePath = `_doc/uniapp_temp/${time}.${format}`
+
+			bitmap.save(filePath, {},
+				() => {
+					bitmap.clear()
+					resolve(filePath)
+				},
+				(error) => {
+					bitmap.clear()
+					console.error(`${JSON.stringify(error)}`)
+					reject(error)
+				})
+		}, (error) => {
+			bitmap.clear()
+			console.error(`${JSON.stringify(error)}`)
+			reject(error)
+		})
+	})
+}
+// #endif
+
+
+export function sleep(time) {
+	return new Promise((resolve) => {
+		setTimeout(() => {
+			resolve(true)
+		}, time)
+	})
+}
+
+
+export function getRect(selector, options = {}) {
+	const typeDefault = 'boundingClientRect'
+	const {
+		context,
+		type = typeDefault
+	} = options
+	return new Promise((resolve, reject) => {
+		const dom = uni.createSelectorQuery().in(context).select(selector);
+		const result = (rect) => {
+			if (rect) {
+				resolve(rect)
+			} else {
+				reject()
+			}
+		}
+		if (type == typeDefault) {
+			dom[type](result).exec()
+		} else {
+			dom[type]({
+				node: true,
+				size: true,
+				rect: true
+			}, result).exec()
+		}
+	});
+};

+ 133 - 0
components/l-echart/uvue.uts

@@ -0,0 +1,133 @@
+// @ts-nocheck
+// #ifdef APP
+type EchartsEventHandler = (event: UTSJSONObject)=>void
+// type EchartsTempResolve = (obj : UTSJSONObject) => void
+// type EchartsTempOptions = UTSJSONObject
+export class Echarts {
+	options: UTSJSONObject = {} as UTSJSONObject
+	context: UniWebViewElement
+	eventMap: Map<string, EchartsEventHandler> = new Map()
+	private temp: UTSJSONObject[] = []
+	constructor(context: UniWebViewElement){
+		this.context = context
+		this.init()
+	}
+	init(){
+		this.context.evalJS(`init(null, null, ${JSON.stringify({})})`)
+		
+		this.context.addEventListener('message', (e : UniWebViewMessageEvent) => {
+			// event.stopPropagation()
+			// event.preventDefault()
+			
+			const detail = e.detail.data[0]
+			const file = detail.getString('file')
+			const data = detail.get('data')
+			const key = detail.getString('event')
+			const options = typeof data == 'object' ? (data as UTSJSONObject).getJSON('options'): null
+			const event = typeof data == 'object' ? (data as UTSJSONObject).getString('event'): null
+			if (key == 'log' && data != null) {
+				console.log(data)
+			}
+			if (event != null && options != null) {
+				this.dispatchAction(event.replace(/"/g,''), options)
+			}
+			if(file != null){
+				while (this.temp.length > 0) {
+					const opt = this.temp.pop()
+					const success = opt?.get('success')
+					if(typeof success == 'function'){
+						success as (res: UTSJSONObject) => void
+						success({tempFilePath: file})
+					}
+				}
+			}
+			
+		})
+	}
+	setOption(option: UTSJSONObject){
+		this.options = option;
+		this.context.evalJS(`setOption(${JSON.stringify([option])})`)
+	}
+	setOption(option: UTSJSONObject, notMerge: boolean = false, lazyUpdate: boolean = false){
+		this.options = option;
+		this.context.evalJS(`setOption(${JSON.stringify([option, notMerge, lazyUpdate])})`)
+	}
+	setOption(option: UTSJSONObject, notMerge: UTSJSONObject){
+		this.options = option;
+		this.context.evalJS(`setOption(${JSON.stringify([option, notMerge])})`)
+	}
+	getOption(): UTSJSONObject {
+		return this.options
+	}
+	showLoading(){
+		this.context.evalJS(`showLoading(${JSON.stringify([] as any[])})`);
+	}
+	showLoading(type: string, opts: UTSJSONObject){
+		this.context.evalJS(`showLoading(${JSON.stringify([type, opts])})`);
+	}
+	hideLoading(){
+		this.context.evalJS(`hideLoading()`);
+	}
+	clear(){
+		this.context.evalJS(`clear()`);
+	}
+	dispose(){
+		this.context.evalJS(`dispose()`);
+	}
+	resize(size:UTSJSONObject){
+		setTimeout(()=>{
+			this.context.evalJS(`resize(${JSON.stringify(size)})`);
+		},0)
+	}
+	resize(){
+		setTimeout(()=>{
+			this.context.evalJS(`resize()`);
+		},10)
+		
+	}
+	on(type:string, query: any, callback: EchartsEventHandler) {
+		const key = `${type}${JSON.stringify(query)}`
+		if(typeof callback == 'function'){
+			this.eventMap.set(key, callback)
+		}
+		this.context.evalJS(`on(${JSON.stringify([type, query])})`);
+		console.warn('uvue 暂不支持事件')
+	}
+	on(type:string, callback: EchartsEventHandler) {
+		const key = `${type}`
+		if(typeof callback == 'function'){
+			this.eventMap.set(key, callback)
+		}
+		this.context.evalJS(`on(${JSON.stringify([type])})`);
+		console.warn('uvue 暂不支持事件')
+	}
+	dispatchAction(type:string, options: UTSJSONObject){
+		const handler = this.eventMap.get(type)
+		if(handler!=null){
+			handler(options)
+		}
+	}
+	canvasToTempFilePath(opt: UTSJSONObject){
+		// this.context.evalJS(`on(${JSON.stringify(opt)})`);
+		this.context.evalJS(`canvasToTempFilePath(${JSON.stringify(opt)})`);
+		this.temp.push(opt)
+	}
+}
+
+// #endif
+// #ifndef APP
+export class Echarts {
+	constructor() {}
+	setOption(option: UTSJSONObject): void
+	isDisposed(): boolean;
+	clear(): void;
+	resize(size:UTSJSONObject): void;
+	resize(): void;
+	canvasToTempFilePath(opt : UTSJSONObject): void;
+	dispose(): void;
+	showLoading(cfg?: UTSJSONObject): void;
+	showLoading(name?: string, cfg?: UTSJSONObject): void;
+	hideLoading(): void;
+	getZr(): any
+}
+// #endif

+ 159 - 0
components/lime-echart/lime-echart.nvue

@@ -0,0 +1,159 @@
+<template>
+	<view style="width: 100%; height: 408px;">
+		<l-echart ref="chartRef" @finished="init"></l-echart>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				showTip: false,
+				option: {
+					tooltip: {
+						trigger: 'axis',
+						// shadowBlur: 0,
+						textStyle: {
+							textShadowBlur: 0
+						},
+						renderMode: 'richText',
+					},
+					legend: {
+						data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
+					},
+					grid: {
+						left: '3%',
+						right: '4%',
+						bottom: '3%',
+						containLabel: true
+					},
+					xAxis: {
+						type: 'category',
+						boundaryGap: false,
+						data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+					},
+					yAxis: {
+						type: 'value'
+					},
+					series: [
+						{
+							name: '邮件营销',
+							type: 'line',
+							stack: '总量',
+							data: [120, 132, 101, 134, 90, 230, 210]
+						},
+						{
+							name: '联盟广告',
+							type: 'line',
+							stack: '总量',
+							data: [220, 182, 191, 234, 290, 330, 310]
+						},
+						{
+							name: '视频广告',
+							type: 'line',
+							stack: '总量',
+							data: [150, 232, 201, 154, 190, 330, 410]
+						},
+						{
+							name: '直接访问',
+							type: 'line',
+							stack: '总量',
+							data: [320, 332, 301, 334, 390, 330, 320]
+						},
+						{
+							name: '搜索引擎',
+							type: 'line',
+							stack: '总量',
+							data: [820, 932, 901, 934, 1290, 1330, 1320]
+						}
+					]
+				}
+			}
+		},
+		mounted() {
+			console.log('lime echarts nvue')
+		},
+		methods: {
+			init() {
+				const chartRef = this.$refs['chartRef']
+				chartRef.init(chart => {
+					chart.setOption(this.option);
+					
+					
+					setTimeout(()=>{
+						const option = {
+							tooltip: {
+								trigger: 'axis',
+								// shadowBlur: 0,
+								textStyle: {
+									textShadowBlur: 0
+								},
+								renderMode: 'richText',
+							},
+							legend: {
+								data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
+							},
+							grid: {
+								left: '3%',
+								right: '4%',
+								bottom: '3%',
+								containLabel: true
+							},
+							xAxis: {
+								type: 'category',
+								boundaryGap: false,
+								data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+							},
+							yAxis: {
+								type: 'value'
+							},
+							series: [
+								{
+									name: '邮件营销',
+									type: 'line',
+									stack: '总量',
+									data: [120, 132, 101, 134, 90, 230, 210]
+								},
+								{
+									name: '联盟广告',
+									type: 'line',
+									stack: '总量',
+									data: [220, 182, 191, 234, 290, 330, 310]
+								},
+								{
+									name: '视频广告',
+									type: 'line',
+									stack: '总量',
+									data: [150, 232, 201, 154, 190, 330, 410]
+								},
+								{
+									name: '直接访问',
+									type: 'line',
+									stack: '总量',
+									data: [320, 332, 301, 334, 390, 330, 320]
+								},
+								{
+									name: '搜索引擎',
+									type: 'line',
+									stack: '总量',
+									data: [820, 932, 901, 934, 1290, 1330, 1320]
+								}
+							]
+						}
+						chart.setOption(option);
+					},1000)
+				})
+			},
+			save() {
+				// this.$refs.chart.canvasToTempFilePath({
+				// 	success(res) {
+				// 		console.log('res::::', res)
+				// 	}
+				// })
+			}
+		}
+	}
+</script>
+<style>
+
+</style>

+ 160 - 0
components/lime-echart/lime-echart.uvue

@@ -0,0 +1,160 @@
+<template>
+	<view style="width: 100%; height: 408px;background-color: aqua;">
+		<l-echart ref="chartRef" @finished="init"></l-echart>
+	</view>
+</template>
+
+<script lang="uts" setup>
+	// @ts-nocheck
+	// #ifdef H5
+	import * as echarts from 'echarts/dist/echarts.esm.js'
+	// #endif
+	const chartRef = ref<LEchartComponentPublicInstance|null>(null)
+	const option = {
+		tooltip: {
+			trigger: 'axis',
+			// shadowBlur: 0,
+			textStyle: {
+				textShadowBlur: 0
+			},
+			renderMode: 'richText',
+		},
+		// formatter: async (params: any) => {
+		// 	console.log('params', params)
+		// 	return 1
+		// },
+		legend: {
+			data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
+		},
+		grid: {
+			left: '3%',
+			right: '4%',
+			bottom: '3%',
+			containLabel: true
+		},
+		xAxis: {
+			type: 'category',
+			boundaryGap: false,
+			data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+		},
+		yAxis: {
+			type: 'value'
+		},
+		series: [
+			{
+				name: '邮件营销',
+				type: 'line',
+				stack: '总量',
+				data: [120, 132, 101, 134, 90, 230, 210]
+			},
+			{
+				name: '联盟广告',
+				type: 'line',
+				stack: '总量',
+				data: [220, 182, 191, 234, 290, 330, 310]
+			},
+			{
+				name: '视频广告',
+				type: 'line',
+				stack: '总量',
+				data: [150, 232, 201, 154, 190, 330, 410]
+			},
+			{
+				name: '直接访问',
+				type: 'line',
+				stack: '总量',
+				data: [320, 332, 301, 334, 390, 330, 320]
+			},
+			{
+				name: '搜索引擎',
+				type: 'line',
+				stack: '总量',
+				data: [820, 932, 901, 934, 1290, 1330, 1320]
+			}
+		]
+	}
+
+	const init = async () =>{
+		if(chartRef.value== null) return
+		// #ifdef APP
+		const chart = await chartRef.value!.init(null)
+		// #endif
+		// #ifdef H5
+		const chart = await chartRef.value!.init(echarts, null)
+		// #endif
+		chart.setOption(option)
+		chart.on('mouseover', function (params) {
+		    console.log('params', params);
+		});
+		
+		
+		// setTimeout(()=> {
+		// 	const option1 = {
+		// 		tooltip: {
+		// 			trigger: 'axis',
+		// 			// shadowBlur: 0,
+		// 			textStyle: {
+		// 				textShadowBlur: 0
+		// 			},
+		// 			renderMode: 'richText',
+		// 		},
+		// 		legend: {
+		// 			data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
+		// 		},
+		// 		grid: {
+		// 			left: '3%',
+		// 			right: '4%',
+		// 			bottom: '3%',
+		// 			containLabel: true
+		// 		},
+		// 		xAxis: {
+		// 			type: 'category',
+		// 			boundaryGap: false,
+		// 			data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+		// 		},
+		// 		yAxis: {
+		// 			type: 'value'
+		// 		},
+		// 		series: [
+		// 			{
+		// 				name: '邮件营销',
+		// 				type: 'line',
+		// 				stack: '总量',
+		// 				data: [820, 132, 101, 134, 90, 230, 210]
+		// 			},
+		// 			{
+		// 				name: '联盟广告',
+		// 				type: 'line',
+		// 				stack: '总量',
+		// 				data: [220, 182, 191, 234, 290, 330, 310]
+		// 			},
+		// 			{
+		// 				name: '视频广告',
+		// 				type: 'line',
+		// 				stack: '总量',
+		// 				data: [950, 232, 201, 154, 190, 330, 410]
+		// 			},
+		// 			{
+		// 				name: '直接访问',
+		// 				type: 'line',
+		// 				stack: '总量',
+		// 				data: [320, 332, 301, 334, 390, 330, 320]
+		// 			},
+		// 			{
+		// 				name: '搜索引擎',
+		// 				type: 'line',
+		// 				stack: '总量',
+		// 				data: [820, 932, 901, 934, 1290, 1330, 1320]
+		// 			}
+		// 		]
+		// 	}
+		// 	chart.setOption(option1)
+		// },1000)
+	}
+	
+	
+	
+</script>
+<style>
+
+</style>

+ 226 - 0
components/lime-echart/lime-echart.vue

@@ -0,0 +1,226 @@
+<template>
+	<view >
+		<view style="height: 750rpx; position: relative">
+			<l-echart ref="chart" @finished="init"></l-echart>
+			<view class="customTooltips" :style="{left: position[0] + 'px',top: position[1] + 'px'}" v-if="params.length && position.length && showTip">
+				<view>这是个自定的tooltips</view>
+				<view>{{params[0]['axisValue']}}</view>
+				<view v-for="item in params">
+					<view>
+						<text>{{item.seriesName}}</text>
+						<text>{{item.value}}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>		
+</template>
+
+<script>
+	// nvue 不需要引入
+	// #ifdef VUE2
+	import * as echarts from '@/uni_modules/lime-echart/static/echarts.min';
+	// #endif
+	// #ifdef VUE3
+	// #ifdef MP
+	// 由于vue3 使用vite 不支持umd格式的包,小程序依然可以使用,但需要使用require
+	const echarts = require('../../static/echarts.min');
+	// #endif
+	// #ifndef MP
+	// 由于 vue3 使用vite 不支持umd格式的包,故引入npm的包
+	import * as echarts from 'echarts/dist/echarts.esm';
+	// #endif
+	// #endif
+	export default {
+		data() {
+			return {
+				showTip: false,
+				position: [],
+				params: [],
+				option:  {
+					tooltip: {
+						trigger: 'axis',
+						// shadowBlur: 0,
+						textStyle: {
+							textShadowBlur : 0
+						},
+						renderMode: 'richText',
+						position: (point, params, dom, rect, size) => {
+							// 假设自定义的tooltips尺寸
+							const box = [170, 170]
+							// 偏移
+							const offsetX = point[0] < size.viewSize[0] / 2 ? 20 : -box[0] - 20;
+							const offsetY = point[1] < size.viewSize[1] / 2 ? 20 : -box[1] - 20;
+							const x = point[0] + offsetX;
+							const y = point[1] + offsetY;
+							
+							this.position = [x, y]
+							this.params = params
+						},
+						formatter: (params, ticket, callback) => {
+							
+						}
+					},
+					legend: {
+						data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
+					},
+					grid: {
+						left: '3%',
+						right: '4%',
+						bottom: '3%',
+						containLabel: true
+					},
+					xAxis: {
+						type: 'category',
+						boundaryGap: false,
+						data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+					},
+					yAxis: {
+						type: 'value'
+					},
+					series: [
+						{
+							name: '邮件营销',
+							type: 'line',
+							stack: '总量',
+							data: [120, 132, 101, 134, 90, 230, 210]
+						},
+						{
+							name: '联盟广告',
+							type: 'line',
+							stack: '总量',
+							data: [220, 182, 191, 234, 290, 330, 310]
+						},
+						{
+							name: '视频广告',
+							type: 'line',
+							stack: '总量',
+							data: [150, 232, 201, 154, 190, 330, 410]
+						},
+						{
+							name: '直接访问',
+							type: 'line',
+							stack: '总量',
+							data: [320, 332, 301, 334, 390, 330, 320]
+						},
+						{
+							name: '搜索引擎',
+							type: 'line',
+							stack: '总量',
+							data: [820, 932, 901, 934, 1290, 1330, 1320]
+						}
+					]
+				}
+			}
+		},
+		
+		methods: {
+			init() {
+				// init(echarts, theme?:string, opts?:{}, chart => {})
+				// echarts 必填, 非nvue必填,nvue不用填
+				// theme 可选,应用的主题,目前只支持名称,如:'dark'
+				// opts = { // 可选
+				//	locale?: string  // 从 `5.0.0` 开始支持
+				// }
+				// chart => {} , callback 返回图表实例
+				// setTimeout(()=>{
+				// 	this.$refs.chart.init(echarts, chart => {
+				// 		chart.setOption(this.option);
+				// 	});
+				// },300)
+				this.$refs.chart.init(echarts, chart => {
+					chart.setOption(this.option);
+					
+					// 监听tooltip显示事件
+					chart.on('showTip', (params) => {
+					  this.showTip = true
+					  console.log('showTip::')
+					});
+					chart.on('hideTip', (params) => {
+						setTimeout(() => {
+							 this.showTip = false
+						},300)
+					});
+					
+					setTimeout(()=>{
+						const option = {
+							tooltip: {
+								trigger: 'axis',
+								// shadowBlur: 0,
+								textStyle: {
+									textShadowBlur: 0
+								},
+								renderMode: 'richText',
+							},
+							legend: {
+								data: ['邮件营销', '联盟广告', '视频广告', '直接访问', '搜索引擎']
+							},
+							grid: {
+								left: '3%',
+								right: '4%',
+								bottom: '3%',
+								containLabel: true
+							},
+							xAxis: {
+								type: 'category',
+								boundaryGap: false,
+								data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+							},
+							yAxis: {
+								type: 'value'
+							},
+							series: [
+								{
+									name: '邮件营销',
+									type: 'line',
+									stack: '总量',
+									data: [1120, 132, 101, 134, 90, 230, 210]
+								},
+								{
+									name: '联盟广告',
+									type: 'line',
+									stack: '总量',
+									data: [220, 182, 191, 234, 290, 330, 310]
+								},
+								{
+									name: '视频广告',
+									type: 'line',
+									stack: '总量',
+									data: [150, 632, 201, 154, 190, 330, 410]
+								},
+								{
+									name: '直接访问',
+									type: 'line',
+									stack: '总量',
+									data: [820, 332, 301, 334, 390, 330, 320]
+								},
+								{
+									name: '搜索引擎',
+									type: 'line',
+									stack: '总量',
+									data: [820, 932, 901, 934, 1290, 1330, 1320]
+								}
+							]
+						}
+						chart.setOption(option);
+					},1000)
+					
+				});
+			},
+			save() {
+				this.$refs.chart.canvasToTempFilePath({
+					success(res) {
+						console.log('res::::', res)
+					}
+				})
+			}
+		}
+	}
+</script>
+<style>
+	.customTooltips {
+		position: absolute;
+		background-color: rgba(255, 255, 255, 0.8);
+		padding: 20rpx;
+	}
+</style>

+ 20 - 0
index.html

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

+ 22 - 0
main.js

@@ -0,0 +1,22 @@
+import App from './App'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+  ...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+export function createApp() {
+  const app = createSSRApp(App)
+  return {
+    app
+  }
+}
+// #endif

+ 73 - 0
manifest.json

@@ -0,0 +1,73 @@
+{
+    "name" : "water-uniApp",
+    "appid" : "",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wx06f2b1b09ac5684f",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true,
+        "appid" : "wx06f2b1b09ac5684f"
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3"
+}

+ 90 - 0
package-lock.json

@@ -0,0 +1,90 @@
+{
+  "name": "water-uniapp",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@dcloudio/uni-ui": {
+      "version": "1.5.7"
+    },
+    "cross-env": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz",
+      "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+      "dev": true,
+      "requires": {
+        "cross-spawn": "^7.0.1"
+      }
+    },
+    "cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "requires": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      }
+    },
+    "echarts": {
+      "version": "5.6.0",
+      "requires": {
+        "tslib": "2.3.0",
+        "zrender": "5.6.1"
+      }
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true
+    },
+    "path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true
+    },
+    "qrcode-generator": {
+      "version": "1.4.4"
+    },
+    "shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^3.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true
+    },
+    "tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+    },
+    "which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "zrender": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz",
+      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+      "requires": {
+        "tslib": "2.3.0"
+      }
+    }
+  }
+}

+ 24 - 0
package.json

@@ -0,0 +1,24 @@
+{
+  "name": "water-uniapp",
+  "version": "1.0.0",
+  "main": "main.js",
+  "scripts": {
+    "dev:h5": "vite",
+    "build:h5": "vite build",
+    "dev:mp-weixin": "cross-env NODE_ENV=development vite",
+    "build:mp-weixin": "cross-env NODE_ENV=production vite build",
+    "serve": "vite preview"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "description": "",
+  "dependencies": {
+    "@dcloudio/uni-ui": "^1.5.7",
+    "echarts": "^5.6.0",
+    "qrcode-generator": "^1.4.4"
+  },
+  "devDependencies": {
+    "cross-env": "^7.0.3"
+  }
+}

+ 66 - 0
pages.json

@@ -0,0 +1,66 @@
+{
+	"easycom": {
+		"autoscan": true,
+		"custom": {
+			// uni-ui 规则如下配置
+			"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
+		}
+	},
+	"pages": [
+		{
+		  "path": "pages/login/login",
+		  "style": {
+		    "navigationBarTitleText": "登录",
+		    "navigationStyle": "custom"
+		  }
+		},
+		{
+			"path": "pages/bindPhone/bindPhone",
+			"style": {
+				"navigationBarTitleText": "绑定手机号",
+				"navigationBarBackgroundColor": "#FFFFFF",
+				"navigationBarTextStyle": "black"
+			}
+		},
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "缴费账单",
+				"navigationBarBackgroundColor": "#FFFFFF",
+				"navigationBarTextStyle": "black"
+			}
+		},
+		{
+			"path": "pages/agreement/user",
+			"style": {
+				"navigationBarTitleText": "用户协议",
+				"navigationBarBackgroundColor": "#FFFFFF",
+				"navigationBarTextStyle": "black"
+			}
+		},
+		{
+			"path": "pages/agreement/privacy",
+			"style": {
+				"navigationBarTitleText": "隐私声明",
+				"navigationBarBackgroundColor": "#FFFFFF",
+				"navigationBarTextStyle": "black"
+			}
+		},
+		{
+			"path": "pages/agreement/payfees",
+			"style": {
+				"navigationBarTitleText": "缴费协议",
+				"navigationBarBackgroundColor": "#FFFFFF",
+				"navigationBarTextStyle": "black"
+			}
+		}
+	],
+	"window": {
+		"backgroundTextStyle": "light",
+		"navigationBarBackgroundColor": "#ffffff",
+		"navigationBarTitleText": "物联网智能抄表系统",
+		"navigationBarTextStyle": "black"
+	},
+	"style": "v3",
+	"sitemapLocation": "sitemap.json"
+}

+ 42 - 0
pages/agreement/payfees.vue

@@ -0,0 +1,42 @@
+<template>
+	<view style="line-height: 26px;padding: 32upx;" class="home1">
+		<view style="font-size: 28upx;" v-html="content"></view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+	import {
+		onLoad
+	} from '@dcloudio/uni-app'
+	
+	import { post, get } from '@/utils/request'
+	
+	const content = ref('')
+	
+	const getGuize = async () => {
+		try {
+			const res = await get('/api/protocolConfig/findByType',{
+				type: 3
+			})
+			
+			if (res.code === '200') {
+				content.value = res.data.text
+			} else {
+			  uni.showToast({ title: res.message || '获取失败', icon: 'none' })
+			}
+		} catch (err) {
+			// console.log('请求失败:', err)
+			uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+		}
+	}
+	
+	onLoad(() => {
+		getGuize()
+	})
+</script>
+
+<style>
+</style>

+ 42 - 0
pages/agreement/privacy.vue

@@ -0,0 +1,42 @@
+<template>
+	<view style="line-height: 26px;padding: 32upx;" class="home1">
+		<view style="font-size: 28upx;" v-html="content"></view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+	import {
+		onLoad
+	} from '@dcloudio/uni-app'
+	
+	import { post, get } from '@/utils/request'
+	
+	const content = ref('')
+	
+	const getGuize = async () => {
+		try {
+			const res = await get('/api/protocolConfig/findByType',{
+				type: 2
+			})
+			
+			if (res.code === '200') {
+				content.value = res.data.text
+			} else {
+			  uni.showToast({ title: res.message || '获取失败', icon: 'none' })
+			}
+		} catch (err) {
+			// console.log('请求失败:', err)
+			uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+		}
+	}
+	
+	onLoad(() => {
+		getGuize()
+	})
+</script>
+
+<style>
+</style>

+ 42 - 0
pages/agreement/user.vue

@@ -0,0 +1,42 @@
+<template>
+	<view style="line-height: 26px;padding: 32upx;" class="home1">
+		<view style="font-size: 28upx;" v-html="content"></view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+	import {
+		onLoad
+	} from '@dcloudio/uni-app'
+	
+	import { post, get } from '@/utils/request'
+	
+	const content = ref('')
+	
+	const getGuize = async () => {
+		try {
+			const res = await get('/api/protocolConfig/findByType',{
+				type: 1
+			})
+			
+			if (res.code === '200') {
+				content.value = res.data.text
+			} else {
+			  uni.showToast({ title: res.message || '获取失败', icon: 'none' })
+			}
+		} catch (err) {
+			// console.log('请求失败:', err)
+			uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+		}
+	}
+	
+	onLoad(() => {
+		getGuize()
+	})
+</script>
+
+<style>
+</style>

+ 262 - 0
pages/bindPhone/bindPhone.vue

@@ -0,0 +1,262 @@
+<template>
+  <view class="container">
+    <view class="card-wrapper">
+      <!-- 顶部蓝色卡片 -->
+      <view class="top-card">
+        <view class="top-left">物联网智能抄表管理系统</view>
+		
+        <picker @change="changeCity" :value="cityIndex" :range="cityList">
+           <view class="top-right">{{ cityList.length > 0 ? cityList[cityIndex] : '请选择城市' }}</view>
+        </picker>
+      </view>
+
+      <!-- 缴费单位 -->
+      <view class="unit-section">
+        <text class="label">缴费单位</text>
+        <picker @change="changeCompany" :value="companyIndex" :range="companyList">
+            <view class="unit-box">
+              <text class="unit-name">{{ companyList.length > 0 ? companyList[companyIndex] : '请选择缴纳单位' }}</text>
+              <text class="arrow">></text>
+            </view>
+        </picker>
+      </view>
+
+      <!-- 电话输入 -->
+      <view class="input-section">
+        <text class="label">电话号码</text>
+        <input type="text" placeholder="请输入电话号码" v-model="phone" class="input" />
+      </view>
+	  
+	  <!-- 密码输入 -->
+	  <view class="input-section">
+	    <text class="label">用户密码</text>
+	    <input type="text" password  placeholder="请输入用户密码" v-model="password" class="input" />
+	  </view>
+
+      <!-- 协议 -->
+      <view class="agreement">
+        <checkbox :checked="checked" @click="checked = !checked" />
+        <text>同意</text>
+        <navigator url="/pages/agreement/index">
+          <text class="link" @tap="gotourl('/pages/agreement/payfees')">《缴费协议》</text>
+        </navigator>
+      </view>
+
+      <!-- 提交按钮 -->
+      <button type="primary" class="confirm-btn" :loading="loading" @click="bindPhone">确定绑定</button>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { post, get } from '@/utils/request'
+import { onLoad } from '@dcloudio/uni-app'
+
+const phone = ref('')
+const password = ref('')
+const checked = ref(false)
+const loading = ref(false)
+
+const cityList = ref([]); 
+const cityIndex = ref(0);
+const changeCity = e => {
+  cityIndex.value = e.detail.value
+}
+
+
+const companyList = ref([]); 
+const companyIndex = ref(0)
+const changeCompany = e => {
+  companyIndex.value = e.detail.value
+}
+
+const gotourl = (text) => {
+	uni.navigateTo({
+		url: text
+	});
+}
+
+const bindPhone = async () => {
+  if (!phone.value) {
+    return uni.showToast({ title: '请输入手机号', icon: 'none' })
+  }
+  if (!/^1[3-9]\d{9}$/.test(phone.value)) {
+    return uni.showToast({ title: '手机号格式不正确', icon: 'none' })
+  }
+  if (!password.value) {
+    return uni.showToast({ title: '请输入用户密码', icon: 'none' })
+  }
+  if (!checked.value) {
+    return uni.showToast({ title: '请同意协议', icon: 'none' })
+  }
+
+  loading.value = true
+
+  try {
+    const res = await get('/api/waterUser/findByPhoneNumber', {
+      phoneNumber: phone.value,
+	  password: password.value
+    })
+
+    // 这里根据接口返回数据继续判断
+    if (res.code === '200') {
+      uni.showToast({ title: '绑定成功', icon: 'success' })
+	   const userInfoStr = encodeURIComponent(JSON.stringify(res.data))
+	  
+	   // 存储用户信息
+	   uni.setStorageSync('userInfo', res.data)
+	   
+	   // 关闭所有页面,跳转首页
+	   uni.navigateTo({
+	     url: '/pages/index/index'
+	   })
+    } else {
+      uni.showToast({ title: res.message || '绑定失败', icon: 'none' })
+    }
+  } catch (err) {
+    // console.log('请求失败:', err)
+    uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+  } finally {
+    loading.value = false
+  }
+}
+
+
+const getCity = async () => {
+	try {
+	  const res = await get('/api/communityManage/getCity')
+	
+	  // 这里根据接口返回数据继续判断
+	  if (res.code === '200') {
+	    cityList.value = res.data.map(item => item.name)
+		cityIndex.value = 0
+	  } else {
+	    uni.showToast({ title: res.message || '获取失败', icon: 'none' })
+	  }
+	} catch (err) {
+	  // console.log('请求失败:', err)
+	  uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+	}
+}
+
+const getCompany = async () => {
+	try {
+	  const res = await get('/api/companyManage/findAll')
+	
+	  // 这里根据接口返回数据继续判断
+	  if (res.code === '200') {
+	    companyList.value = res.data.map(item => item.companyName)
+		cityIndex.value = 0
+	  } else {
+	    uni.showToast({ title: res.message || '获取失败', icon: 'none' })
+	  }
+	} catch (err) {
+	  // console.log('请求失败:', err)
+	  uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+	}
+}
+
+onLoad((options) => {
+	if (options.phone) {
+		  phone.value = decodeURIComponent(options.phone); // 解码手机号
+		  // console.log('接收到的手机号:', phone.value);
+	}
+  getCity()
+  getCompany()
+})
+</script>
+
+<style scoped>
+.container {
+	background-color: #f5f5f5;
+    min-height: 100vh;
+    padding: 30rpx;
+    box-sizing: border-box;
+}
+
+.card-wrapper {
+  background-color: #ffffff;
+  border-radius: 20rpx;
+  padding: 0rpx 0rpx 60rpx 0rpx;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+
+.top-card {
+  background: linear-gradient(to right, #6ca3fe, #3f83fd);
+  border-radius: 20rpx 20rpx 0rpx 0rpx;
+  padding: 20rpx 20px 30px 20px;
+  color: white;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.top-left {
+  font-size: 26rpx;
+}
+.top-right {
+  background: white;
+  color: #2979ff;
+  padding: 10rpx 20rpx;
+  border-radius: 8rpx;
+  font-size: 24rpx;
+}
+
+.unit-section {
+  padding: 20px 30rpx;
+  border-radius: 20rpx 20rpx 0rpx 0rpx;
+  margin-top: -20px;
+  background: #ffffff;
+}
+.label {
+  font-size: 28rpx;
+  color: #b5b5b5;
+  margin-bottom: 10rpx;
+  display: block;
+}
+.unit-box {
+  border-radius: 12rpx;
+  font-weight: 600;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.unit-name {
+  font-size: 30rpx;
+}
+.arrow {
+  font-size: 34rpx;
+  color: #999;
+}
+
+.input-section {
+  padding: 20rpx 30rpx;
+}
+.input {
+  padding: 17rpx;
+  border: 1px solid #ccc;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+}
+
+.agreement {
+  display: flex;
+  align-items: center;
+  margin-bottom: 40rpx;
+  font-size: 26rpx;
+  padding: 0 30rpx;
+  margin-top: 40rpx;
+}
+.link {
+  color: #2979ff;
+  margin-left: 10rpx;
+}
+.confirm-btn {
+  width: calc(100% - 40rpx);
+  margin: 0 20rpx;
+  border-radius: 35rpx;
+  background: linear-gradient(to bottom, #6ca3fe, #3f83fd);
+  color: white;
+  font-size: 30rpx;
+}
+</style>

+ 676 - 0
pages/index/index.vue

@@ -0,0 +1,676 @@
+<template>
+	<scroll-view scroll-y class="page">
+		<!-- Header -->
+		<view class="title-card">
+			<text class="title">物联网智能抄表管理系统</text>
+		</view>
+
+		<!-- 应缴金额区域 -->
+		<view class="amount-area">
+			<view class="row between">
+				<text class="label">应缴金额(元)</text>
+				<text class="amount" v-if="userInfo.settledType === 1">{{ userInfo.shouldCollectMoney }}</text>
+				<text class="amount" v-else>0.00</text>
+			</view>
+			<view class="row between">
+				<text></text>
+				<text class="warn" v-if="userInfo.settledType === 1">有欠费,请尽快缴费</text>
+			</view>
+			<view class="info">
+				<view class="row between info-item">
+					<text>截止时间</text>
+					<text class="warn">{{ userInfo.lastExamineTime }}</text>
+				</view>
+				<view class="row between">
+					<text>户号信息</text>
+					<text class="warn">{{ userInfo.userCode }}</text>
+				</view>
+				<view class="row between">
+					<text>地址信息</text>
+					<text class="warn">{{ userInfo.roomNumber }}</text>
+				</view>
+				<view class="row between info-mani">
+					<text>缴费单位</text>
+					<text class="warn">{{ userInfo.companyName }}</text>
+				</view>
+			</view>
+		</view>
+		<view class="floating-view"></view>
+		<!-- 缴费金额选择 -->
+		<view class="pay-select">
+			<text class="label">缴费金额</text>
+			<view class="preset">
+				<button class="amount-btn" @click="duplicationEvents(20)">20元</button>
+				<button class="amount-btn" @click="duplicationEvents(50)">50元</button>
+				<button class="amount-btn" @click="duplicationEvents(100)">100元</button>
+			</view>
+		
+
+			<view class="input-wrapper">
+				<text class="prefix">¥</text>
+				<input class="custom-input" v-model="moneyvalue" type="number" min="0" step="0.01" placeholder="点击输入缴费金额" />
+			</view>
+			<view class="current-debt" v-if="userInfo.settledType === 1">
+				<text>当前欠费金额 {{ userInfo.shouldCollectMoney }} 元 </text>
+				<text class="auto-fill warn" @click="duplicationEvents(money.value)">点击自动填入</text>
+			</view>
+			<view class="current-debt" v-else>
+				<text>当前暂为欠费 </text>
+			</view>
+		</view>
+
+		<!-- 支付按钮 -->
+		<view class="pay-buttons">
+			<button class="wxpay" @click="qita('weiPay')">微信支付</button>
+			<button class="otherpay" @click="qita('zfbPay')">其他支付</button>
+		</view>
+
+		<!-- 二维码弹框 -->
+		<uni-popup ref="popup" type="center" z-index="999" >
+			<view class="modal">
+				<text class="modal-title">物联网智能抄表管理系统</text>
+				<view class="qrcode-container">
+					<img v-if="qrcodeContainer" :src="qrcodeContainer" alt="" />
+				</view>
+				<button class="modal_button" @click="saveScreenshot">截屏保存</button>
+			</view>
+		</uni-popup>
+
+		<!-- 账单信息 -->
+		<view class="bill-info">
+			<view class="bill-head">
+				<text class="label">账单信息</text>
+				<!-- <text class="date">2024年10月</text> -->
+				<view class="container">
+					<!-- <picker mode="date" fields="month" :value="single" start="2000-01-01" end="2030-12-31"
+						@change="onMonthChange">
+						<view class="uni-input">{{ single || defaultDate }} <text>▼</text></view>
+					</picker> -->
+					<picker
+						mode="date"
+						fields="month"
+						:value="single"
+						:start="startDate"
+						:end="endDate"
+						@change="onMonthChange"
+					>
+						<view class="uni-input">{{ single || defaultDate }} <text>▼</text></view>
+					</picker>
+				</view>
+			</view>
+			<view class="bill-detail">
+				<text>本期用水量(m³):<text class="bold">{{ currentWater }}</text></text>
+				<text>本期水费(元):<text class="bold">{{ currentMoney }}</text></text>
+			</view>
+
+			<!-- 用水柱状图 -->
+			<LEchart force-use-old-canvas="false" class="chart" ref="barChartref" @finished="init"></LEchart>
+
+			<text class="label">用水量情况</text>
+
+			<!-- 用水曲线图 -->
+			<LEchart force-use-old-canvas="false" class="chart2" ref="lineChartref" @finished="init"></LEchart>
+		</view>
+	</scroll-view>
+</template>
+
+<script setup>
+	import {
+		onMounted,
+		reactive,
+		ref,
+		nextTick
+	} from 'vue'
+	import { get } from '@/utils/request'
+	import { onLoad } from '@dcloudio/uni-app'
+	import QRCode from 'qrcode-generator';
+	import LEchart from '@/components/l-echart/l-echart.vue';
+	
+	const userInfo = ref({})
+	
+	// 图表  柱状  折线
+	const barChartref = ref();
+	const lineChartref = ref();
+	
+	onLoad(() => {
+	  userInfo.value = uni.getStorageSync('userInfo')
+	  setTimeout(() => {
+		  getCurrentYearMonth();
+	  }, 1000)
+	})
+	
+	const echarts = require('../../static/echarts.min');
+	
+	// 点击选择年月
+	const single = ref('');
+	// 默认年月
+	const defaultDate = ref('');
+	// 欠费金额
+	const money = ref(87.12)
+	// 缴费金额
+	const moneyvalue = ref()
+	// 是否显示二维码弹框
+	const showQRCodeModal = ref(false);
+	const popup = ref(null);
+	// 二维码容器
+	const qrcodeContainer = ref('http://192.168.50.127:18001/api/images/63a8db7bfb4f190d6aed819088bfb396.jpg');
+	// 本期用水量
+	const currentWater = ref(0)
+	// 本期水费
+	const currentMoney = ref(0)
+	// 自动填入
+	const duplicationEvents = (data) => {
+		moneyvalue.value = data
+	}
+
+	// 支付
+	const qita = async (value) => {
+		try {
+			const res = await get('/api/payConfig/pay', {
+			  payType: value
+			})
+			if (res.code === '200') {
+				qrcodeContainer.value = res.data
+				popup.value.open();
+			} else {
+				uni.showToast({ title: res.message || '绑定失败', icon: 'none' })
+			}
+		} catch (err) {
+		  // console.log('请求失败:', err)
+		  uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+		}
+	}
+
+	// 截屏保存按钮点击事件
+	const saveScreenshot = () => {
+		// 这里可以实现截屏保存的逻辑,例如使用 HTML2Canvas 库
+		console.log('截屏保存');
+	};
+	
+	// 获取当前年份
+	const currentYear = new Date().getFullYear()
+	
+	// 设置起始和结束为今年范围
+	const startDate = `${currentYear}-01-01`
+	const endDate = `${currentYear}-12-31`
+	
+	// 默认月份:当前月
+	const now = new Date()
+	const defaultMonth = `${currentYear}-${String(now.getMonth() + 1).padStart(2, '0')}`
+	
+	const selectedMonth = ref('')
+	// 点击弹框获取年月
+	const onMonthChange = (e) => {
+		 // 截取到年月,如 "2025-06"
+		const value = e.detail.value.slice(0, 7)
+		single.value = value
+		// console.log(single.value, 'selected month')
+	
+		// 提取月份部分(注意转为数字)
+		const month = Number(value.slice(5, 7))
+	
+		if (month <= 6) {
+			// console.log('上半年逻辑')
+			findByTimes(1, 6)
+		} else {
+			// console.log('下半年逻辑')
+			findByTimes(7, 12)
+		}
+	}
+
+	// 默认显示时间
+	const getCurrentYearMonth = () => {
+		const date = new Date()
+		const year = date.getFullYear()
+		const month = date.getMonth() + 1 // JS 中月份从 0 开始
+	
+		// 格式化为 "YYYY-MM"
+		const formattedMonth = String(month).padStart(2, '0')
+		const shij = `${year}-${formattedMonth}`
+	
+		defaultDate.value = shij
+		single.value = shij
+	
+		// 根据月份判断逻辑
+		if (month <= 6) {
+			// console.log('当前为上半年,执行上半年默认逻辑')
+			findByTimes(1, 6)
+		} else {
+			// console.log('当前为下半年,执行下半年默认逻辑')
+			findByTimes(7, 12)
+		}
+	}
+	
+	const realCollectMoneylist = ref([])
+	const waterlist = ref([])
+	const monthLabelslist = ref([])
+	const findByTimes = async (startTime, endTime) => {
+		
+		try {
+			const res = await get('/api/waterUser/findByTime', {
+			  startTime: startTime,
+			  endTime: endTime,
+			  phoneNumber: userInfo.value.phoneNumber
+			})
+			if (res.code === '200') {
+				currentMoney.value = res.data.currentMoney
+				currentWater.value = res.data.currentWater
+				
+				realCollectMoneylist.value = res.data.data.map(item => Number(item.realCollectMoney));
+				waterlist.value = res.data.data.map(item => item.waterData);
+				monthLabelslist.value = res.data.months.map(item => `${item}月`);
+				
+				// 动态更新柱状图配置
+				state.option.xAxis.data = monthLabelslist.value; // 更新 X 轴数据
+				state.option.series[0].data = realCollectMoneylist.value; // 更新柱状图数据
+			
+				// 动态更新柱状图配置
+				state.option.xAxis.data = monthLabelslist.value;
+				state.option.series[0].data = realCollectMoneylist.value;
+			
+				// 动态更新折线图配置
+				state2.option.xAxis.data = monthLabelslist.value;
+				state2.option.series[0].data = waterlist.value;
+			
+				// 立即初始化图表
+				barChartref.value.init(echarts, (chart) => {
+					chart.setOption(state.option);
+				});
+				lineChartref.value.init(echarts, (chart) => {
+					chart.setOption(state2.option);
+				});
+			} else {
+				uni.showToast({ title: res.message || '查询失败', icon: 'none' })
+			}
+		} catch (err) {
+		  uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+		}
+	}
+
+	// 折线图
+	const state2 = reactive({
+		option: {}
+	});
+
+	state2.option = {
+		grid: {
+			right: '2%',
+			top: '18%',
+			left: '8%',
+			bottom: '18%'
+		},
+		xAxis: {
+			type: 'category',
+			data: monthLabelslist.value,
+			axisTick: {
+				// 不显示刻度线
+				show: false
+			}
+		},
+		yAxis: {
+			type: 'value',
+			name: '(m³)',
+		},
+		series: [{
+			data: waterlist.value,
+			type: 'line',
+			areaStyle: {},
+			smooth: true,
+			color: '#7bed8b',
+			symbol: 'none',
+			emphasis: {
+				// 鼠标悬停时显示数据点
+				itemStyle: {
+					borderWidth: 2,
+					borderColor: '#fff'
+				},
+				label: {
+					show: true
+				},
+				// 鼠标悬停时显示圆形数据点
+				symbol: 'circle',
+				symbolSize: 8,
+			}
+		}]
+	}
+
+	// 柱状图
+	const state = reactive({
+		option: {}
+	})
+	// 获取当前月份(注意:getMonth() 返回值是 0 - 11,所以需要加 1)
+	// const currentMonth = new Date().getMonth() + 1;
+	// const months = ['5月', '6月', '7月', '8月', '9月'];
+	// const data = realCollectMoneylist.value;
+	const colors = new Array(realCollectMoneylist.value.length).fill('#cfdfff');
+
+	// // 找到当前月份在 months 数组中的索引
+	// const index = realCollectMoneylist.value.indexOf(`${currentMonth}月`);
+	// if (index !== -1) {
+	// 	// 如果找到当前月份对应的索引,将该柱子颜色修改为 #1756d9
+	// 	colors[index] = '#1756d9';
+	// }
+
+	state.option = {
+		grid: {
+			right: '2%',
+			top: '18%',
+			left: '9%',
+			bottom: '20%'
+		},
+		xAxis: {
+			type: 'category',
+			data: monthLabelslist.value, // X 轴数据
+			axisTick: {
+				// 不显示刻度线
+				show: false
+			}
+		},
+		yAxis: {
+			type: 'value',
+			name: '(元)'
+		},
+		series: [{
+			data: realCollectMoneylist.value,
+			type: 'bar',
+			// 使用 colors 数组来设置每个柱子的颜色
+			itemStyle: {
+				color:'#1756d9'
+			},
+			barWidth: '40%',
+			label: {
+				show: true,
+				position: 'top',
+				color: '#1756d9'
+			}
+		}]
+	};
+
+	// 渲染完成
+	const init = () => {
+		console.log('渲染完成');
+	}
+</script>
+
+<style scoped lang="scss">
+	:deep .uni-popup {
+		z-index: 999 !important;
+	}
+	/* 遮罩层样式 */
+	.modal-overlay {
+		z-index: 99999 !important;
+		width: 100%;
+		height: 100%;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		/* 修改为 align-items 以垂直居中 */
+		position: fixed;
+		top: 0;
+		left: 0;
+		background-color: rgba(0, 0, 0, 0.5);
+		/* 加深背景透明度 */
+		backdrop-filter: blur(3rpx);
+		/* 添加模糊效果 */
+	}
+
+	/* 模态框样式 */
+	.modal {
+		width: 600rpx;
+		height: 850rpx;
+		background: #ffffff;
+		/* 白色背景 */
+		border-radius: 15rpx;
+		/* 增大圆角 */
+		text-align: center;
+		position: relative;
+		box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.2);
+		/* 添加阴影效果 */
+		padding: 40rpx;
+		/* 添加内边距 */
+		box-sizing: border-box;
+		/* 包含内边距在宽度内 */
+	}
+
+	/* 模态框标题样式 */
+	.modal-title {
+		font-size: 36rpx;
+		font-weight: bold;
+		color: #333333;
+		margin-bottom: 30rpx;
+		/* 增加底部间距 */
+	}
+
+	/* 二维码容器样式 */
+	.qrcode-container {
+		width: 520rpx;
+		height: 520rpx;
+		/* 调整二维码容器高度 */
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		margin-bottom: 40rpx;
+		/* 增加底部间距 */
+	}
+
+	/* 按钮样式 */
+	.modal_button {
+		position: absolute;
+		/* 修改为相对定位 */
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		border: none;
+		background: linear-gradient(to right, #6ea5ff, #3d81fd);
+		/* 蓝色背景 */
+		color: #ffffff;
+		/* 白色文字 */
+		font-size: 32rpx;
+		padding: 15rpx 0;
+		border-radius: 0rpx;
+	}
+
+	/* 按钮悬停效果 */
+	.modal_button:hover {
+		background-color: #0056b3;
+		/* 悬停时颜色加深 */
+	}
+
+	.page {
+		width: calc(100% - 60rpx);
+		background-color: #f7f8fa;
+		padding: 30rpx;
+	}
+
+	.title-card {
+		background: linear-gradient(to right, #6ea5ff, #3c80fd);
+		padding: 25rpx 25rpx 45rpx 25rpx;
+		border-radius: 20rpx 20rpx 0 0;
+	}
+
+	.title {
+		color: #fff;
+		font-size: 30rpx;
+		font-weight: bold;
+		text-align: center;
+	}
+
+	.amount-area {
+		background: #fff;
+		padding: 30rpx;
+		border-radius: 20rpx;
+		margin-top: -20rpx;
+		margin-bottom: 2rpx;
+	}
+
+	.amount-area .row {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+	}
+
+	.floating-view {
+		border-top: 2rpx dashed #d8d8d8;
+		width: calc(100% - 40rpx);
+		margin: 0 20rpx;
+	}
+
+	.amount {
+		font-size: 50rpx;
+		color: #000;
+		font-weight: bold;
+	}
+
+	.label {
+		font-weight: 600;
+		font-size: 35rpx;
+		color: #515151;
+	}
+
+	.warn {
+		color: red;
+		margin-top: 10rpx;
+		font-size: 24rpx;
+	}
+
+	.info text {
+		display: block;
+		margin-top: 10rpx;
+		font-size: 26rpx;
+		color: #666;
+		margin: 13rpx;
+	}
+
+	.info-item {
+		margin-top: 16px;
+		border-bottom: 1rpx solid #e6e6e6;
+	}
+
+	.info-mani {
+		/* border-bottom: 1rpx solid #e6e6e6; */
+	}
+
+	.pay-select {
+		background: #fff;
+		padding: 30rpx;
+		border-radius: 20rpx;
+		margin-bottom: 20rpx;
+	}
+
+	.preset {
+		display: flex;
+		justify-content: space-between;
+		margin: 35rpx 0;
+	}
+
+	.amount-btn {
+		background-color: #f5f5f5;
+		color: #333;
+		font-weight: bold;
+		padding: 0rpx 40rpx;
+		border: none;
+		border-radius: 10rpx;
+		font-size: 28rpx;
+	}
+
+	.amount-btn::after {
+		border: 0px solid rgba(0, 0, 0, 0.2);
+	}
+
+	.input-wrapper {
+		display: flex;
+		align-items: center;
+		height: 70rpx;
+		border-bottom: 1rpx solid #e6e6e6;
+	}
+
+	.prefix {
+		font-size: 32rpx;
+		font-weight: 600;
+		color: #3e3e3e;
+		margin-right: 10rpx;
+	}
+
+	.custom-input {
+		flex: 1;
+		font-size: 32rpx;
+		color: #333;
+		border: none;
+		outline: none;
+	}
+
+	.current-debt {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		font-size: 24rpx;
+		color: #666;
+		margin-top: 16rpx;
+	}
+
+	.auto-fill {
+		color: #2f80ed;
+		margin-left: 10rpx;
+	}
+
+	.pay-buttons {
+		display: flex;
+		justify-content: space-around;
+		margin: 40rpx 0;
+	}
+
+	.wxpay {
+		background-color: #2f80ed;
+		color: white;
+		border-radius: 50rpx;
+		flex: 1;
+		margin-right: 20rpx;
+	}
+
+	.otherpay {
+		border: 1rpx solid #2f80ed;
+		color: #2f80ed;
+		background-color: white;
+		border-radius: 50rpx;
+		flex: 1;
+	}
+
+	.bill-info {
+		background: #fff;
+		padding: 30rpx;
+		border-radius: 20rpx;
+		margin-bottom: 40rpx;
+	}
+
+	.bill-head {
+		display: flex;
+		justify-content: space-between;
+		font-weight: bold;
+		font-size: 28rpx;
+		margin-bottom: 20rpx;
+	}
+
+	.bill-detail {
+		display: flex;
+		justify-content: space-between;
+		margin-bottom: 20rpx;
+	}
+
+	.bold {
+		font-weight: bold;
+		font-size: 30rpx;
+	}
+
+	.chart {
+		height: 300rpx;
+		width: 100%;
+		z-index: 2 !important;
+	}
+
+	.chart2 {
+		margin-top: 16rpx;
+		height: 300rpx;
+		width: 100%;
+		z-index: 2 !important;
+	}
+</style>

+ 169 - 0
pages/login/login.vue

@@ -0,0 +1,169 @@
+<template>
+	<view class="container">
+		<image src="/static/login-bg.png" class="bg-image" mode="aspectFit" />
+
+		<view class="btns">
+			<!-- <button class="primary-btn" @click="oneClickLogin">手机号一键登录</button> -->
+			<button class="primary-btn"
+			open-type="getPhoneNumber"
+			@getphonenumber="onGetPhoneNumber">手机号一键登录</button>
+			<button class="plain-btn" plain @click="goBindPhone">使用其他号码登录</button>
+		</view>
+
+		<view class="agreement">
+			<checkbox class="custom-checkbox" :checked="checked" @click="checked = !checked" />
+			<text class="agreement-text">
+				阅读并同意
+				<text class="link" @tap="gotourl('/pages/agreement/user')">《用户协议》</text>
+				<text class="link" @tap="gotourl('/pages/agreement/privacy')">《隐私声明》</text> 若您的手机号未注册,将为您自动注册
+			</text>
+		</view>
+	</view>
+</template>
+
+<script setup>
+	import {
+		ref
+	} from 'vue'
+	import {
+		onLoad
+	} from '@dcloudio/uni-app'
+
+	import { post, get } from '@/utils/request'
+	
+	const gotourl = (text) => {
+		uni.navigateTo({
+			url: text
+		});
+	}
+
+	const checked = ref(false)
+	
+	const onGetPhoneNumber = async (e) => {
+	  if (!checked.value) {
+	    uni.showToast({ title: '请同意协议', icon: 'none' })
+	    return
+	  }
+	  
+	  if (e.detail.errMsg !== 'getPhoneNumber:ok') {
+	    uni.showToast({ title: '获取手机号失败', icon: 'none' })
+	    return
+	  }
+	  
+	  try {
+		  // console.log(e.detail, 'e.detaile.detail');
+		  const { encryptedData, iv, code } = e.detail
+		  
+		  const loginRes = await uni.login({ provider: 'weixin' });
+		  
+		  const res = await get('/api/waterUser/parsePhone',{
+			  encryptedData,
+			  iv, 
+			  code: loginRes.code
+		  })
+		  	
+		  if (res.code === '200') {
+			  const phone = encodeURIComponent(res.data); // 对手机号进行编码
+			  uni.navigateTo({
+				url: `/pages/bindPhone/bindPhone?phone=${phone}`
+			  });
+		  } else {
+		    uni.showToast({ title: res.message || '手机号解析失败', icon: 'none' })
+		  }
+	  } catch (err) {
+		// console.log('请求失败:', err)
+		uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
+	  }
+	}
+
+	const goBindPhone = () => {
+		if (!checked.value) return uni.showToast({
+			title: '请同意协议',
+			icon: 'none'
+		})
+		uni.navigateTo({
+			url: '/pages/bindPhone/bindPhone'
+		})
+	}
+	
+	onLoad(() => {
+		// console.log(import.meta.env.MODE) // development / production
+		// console.log(import.meta.env.VITE_API_BASE_URL)
+	})
+</script>
+
+<style scoped>
+	.container {
+		padding: 40rpx;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		min-height: 100vh;
+		box-sizing: border-box;
+		background-color: #ffffff;
+		/* justify-content: center; */
+	}
+
+	.bg-image {
+		width: 100%;
+		max-width: 600rpx;
+		margin-top: 120rpx;
+	}
+
+	.btns {
+		width: 100%;
+		margin-top: 100rpx;
+		display: flex;
+		flex-direction: column;
+		gap: 30rpx;
+	}
+
+	.primary-btn,
+	.plain-btn {
+		width: 100%;
+		height: 90rpx;
+		font-size: 32rpx;
+		border-radius: 16rpx;
+	}
+
+	.primary-btn {
+		background: linear-gradient(to right, #6da4ff, #3f7ffb);
+		color: #ffffff;
+		border: none;
+	}
+
+	.plain-btn {
+		border: 2rpx solid #2979ff;
+		color: #2979ff;
+		background-color: #ffffff;
+	}
+
+	.agreement {
+		margin-top: 60rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: row;
+		flex-wrap: nowrap;
+		/* padding: 0 40rpx; */
+		font-size: 24rpx;
+		color: #666;
+		line-height: 1.5;
+	}
+
+	.custom-checkbox {
+		margin-right: 10rpx;
+		transform: scale(0.8);
+	}
+
+	.agreement-text {
+		flex: 1;
+		text-align: left;
+		word-break: break-word;
+	}
+
+	.link {
+		color: #2979ff;
+		margin: 0 6rpx;
+	}
+</style>

文件差異過大導致無法顯示
+ 0 - 0
static/ecStat.min.js


文件差異過大導致無法顯示
+ 50 - 0
static/echarts.min.js


二進制
static/login-bg.png


文件差異過大導致無法顯示
+ 0 - 0
static/uni.webview.1.5.5.js


+ 173 - 0
static/uvue.html

@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html lang="zh">
+	<head>
+		<meta charset="UTF-8">
+		<meta name="viewport"
+			content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+		<meta http-equiv="X-UA-Compatible" content="ie=edge">
+		<title></title>
+		<style type="text/css">
+			html,
+			body,
+			.canvas {
+				padding: 0;
+				margin: 0;
+				overflow-y: hidden;
+				background-color: transparent;
+				width: 100%;
+				height: 100%;
+			}
+		</style>
+	</head>
+	<body>
+		<div class="canvas" id="limeChart"></div>
+		<script type="text/javascript" src="./uni.webview.1.5.5.js"></script>
+		<script type="text/javascript" src="./echarts.min.js"></script>
+		<script type="text/javascript" src="./ecStat.min.js"></script>
+		<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts-liquidfill@latest/dist/echarts-liquidfill.min.js"></script> -->
+		<script>
+			let chart = null;
+			let cache = [];
+			console.log = function() {
+				emit('log', {
+					log: arguments,
+				})
+			}
+
+			function emit(event, data) {
+				postMessage({
+					event,
+					data
+				})
+				cache = []
+			}
+
+			function postMessage(data) {
+				uni.webView.postMessage({
+					data
+				})
+				// window.__uniapp_x_.postMessage(JSON.stringify(data))
+			};
+
+			function stringify(key, value) {
+				if (typeof value === 'object' && value !== null) {
+					if (cache.indexOf(value) !== -1) {
+						return;
+					}
+					cache.push(value);
+				}
+				return value;
+			}
+
+			function parse(name, callback, options) {
+				const optionNameReg = /[\w]+\.setOption\(([\w]+\.)?([\w]+)\)/
+				if (optionNameReg.test(callback)) {
+					const optionNames = callback.match(optionNameReg)
+					if (optionNames[1]) {
+						const _this = optionNames[1].split('.')[0]
+						window[_this] = {}
+						window[_this][optionNames[2]] = options
+						return optionNames[2]
+					} else {
+						return null
+					}
+				}
+				return null
+			}
+
+			function init(callback, options, opts, theme) {
+				if (!chart) {
+					chart = echarts.init(document.getElementById('limeChart'), theme, opts)
+
+					if (options) {
+						chart.setOption(options)
+					}
+				}
+			}
+
+			function on(data) {
+				if (chart && data.length > 0) {
+					const [type, query] = data
+					const key = `${type}${JSON.stringify(query||'')}`
+					if (query) {
+						chart.on(type, query, function(options) {
+							var obj = {};
+							Object.keys(options).forEach(function(key) {
+								if (key != 'event') {
+									obj[key] = options[key];
+								}
+							});
+							emit(key, {
+								event: key,
+								options: obj,
+							});
+						});
+					} else {
+						chart.on(type, function(options) {
+							var obj = {};
+							Object.keys(options).forEach(function(key) {
+								if (key != 'event') {
+									obj[key] = options[key];
+								}
+							});
+							emit(key, {
+								event: key,
+								options: obj,
+							});
+						});
+					}
+				}
+
+			}
+
+			function setChart(callback, options) {
+				if (!callback) return
+				if (chart && callback && options) {
+					var r = null
+					const name = parse('r', callback, options)
+					if (name) this[name] = options
+					eval(`r = ${callback};`)
+					if (r) {
+						r(chart)
+					}
+				}
+			}
+
+			function setOption(data) {
+				if (chart) chart.setOption(data[0], data[1])
+			}
+
+			function showLoading(data) {
+				if (chart) chart.showLoading(data[0], data[1])
+			}
+
+			function hideLoading() {
+				if (chart) chart.hideLoading()
+			}
+
+			function clear() {
+				if (chart) chart.clear()
+
+			}
+
+			function dispose() {
+				if (chart) chart.dispose()
+			}
+
+			function resize(size) {
+				if (chart) chart.resize(size)
+			}
+
+			function canvasToTempFilePath(opt) {
+				if (chart) {
+					delete opt.success
+					const src = chart.getDataURL(opt)
+					postMessage({
+						// event: 'file',
+						file: src
+					})
+				}
+			}
+		</script>
+	</body>
+</html>

+ 10 - 0
uni.promisify.adaptor.js

@@ -0,0 +1,10 @@
+uni.addInterceptor({
+  returnValue (res) {
+    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
+      return res;
+    }
+    return new Promise((resolve, reject) => {
+      res.then((res) => res[0] ? reject(res[0]) : resolve(res[1]));
+    });
+  },
+});

+ 75 - 0
uni.scss

@@ -0,0 +1,75 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+/* 颜色变量 */
+
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16px;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;

+ 55 - 0
utils/request.js

@@ -0,0 +1,55 @@
+const BASE_URL = import.meta.env.VITE_API_BASE_URL
+
+const request = (url, method = 'GET', data = {}, options = {}) => {
+  const {
+    showLoading = true,
+    loadingText = '加载中...',
+    customHeaders = {}
+  } = options
+
+  const token = uni.getStorageSync('token')
+
+  if (showLoading) {
+    uni.showLoading({ title: loadingText, mask: true })
+  }
+
+  return new Promise((resolve, reject) => {
+    uni.request({
+      url: BASE_URL + url,
+      method,
+      data,
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': token ? `Bearer ${token}` : '',
+        ...customHeaders
+      },
+      success: res => {
+        if (res.statusCode === 200) {
+          resolve(res.data)
+        } else {
+          uni.showToast({
+            title: res.data?.message || '请求出错',
+            icon: 'none'
+          })
+          reject(res)
+        }
+      },
+      fail: err => {
+        uni.showToast({
+          title: '网络异常,请稍后重试',
+          icon: 'none'
+        })
+        reject(err)
+      },
+      complete: () => {
+        if (showLoading) {
+          uni.hideLoading()
+        }
+      }
+    })
+  })
+}
+
+export const get = (url, data = {}, options = {}) => request(url, 'GET', data, options)
+export const post = (url, data = {}, options = {}) => request(url, 'POST', data, options)
+export default request

部分文件因文件數量過多而無法顯示