Commit 00c379db by huahua

提交

parent 973e5556
/unpackage
/unpackage
*.dex
*.class
# Memory Bank
你熟悉 uni-app x框架,擅长编写跨平台且高性能的代码。
uni-app x项目使用UTS语言编写script。 UTS是一种跨平台的强类型语言,类似TS语言但类型要求更加严格。
## 语言使用规则
- **对话交流**:所有回复必须使用中文
- **文档编写**:README、说明文档、注释等均使用中文
- **代码注释**:使用中文编写所有注释(包括单行注释、多行注释、JSDoc)
- **代码命名**:变量名、函数名、类名、接口名等使用英文(遵循编程规范)
- **提交信息**:Git commit message 使用中文
- **错误提示**:用户可见的错误信息使用中文
## Code Style and Structure
- 简洁易懂,复杂的代码配上中文注释。
- 严格类型匹配,不使用隐式转换。
- 不使用变量和函数的声明提升,严格的在清晰的范围内使用变量和函数。
- 当生成某个平台专用代码时,应使用条件编译进行平台约束,避免干扰其他平台。
## project
- 遵循uni-app x的项目结构,在正确的目录中放置生成的文件。
## page
- 使用uvue作为页面后缀名,uvue与vue基本类似,但有少量细节差异。
- 生成的uvue页面放置在项目的pages目录下,生成的页面需要在pages.json中注册。
- 可滚动内容必须在scroll-view、list-view、waterflow等滚动容器中。如果页面需要滚动,则在页面template的一级子节点放置滚动容器,例如` <scroll-view style="flex:1">`。此时应在 App 上使用条件编译,例如: `<!-- #ifdef APP --><scroll-view class="container"><!-- #endif -->`
- 生成uvue页面时,页面内容需符合uts.mdc、uvue.mdc、ucss.mdc、api.mdc约定的规范。
# API
- 可以使用uts的api,但注意版本和平台的兼容性。
- 可以使用uni-app x的api,但注意版本和平台的兼容性。
- 可以使用vue3的api,但注意版本和平台的兼容性。
- 可以使用操作系统的api,但注意版本和平台的兼容性。尽量在uts插件中调用系统原生API,而不是在uvue页面中直接调用系统原生API。
- 特定平台或特定版本以上才能使用的代码,需使用条件编译包围这些代码,或者放置在平台专用的目录文件中。
- 通过mcp工具查询项目下可用的插件。
- 跨页面通信优先使用eventbus。
# uvue rules
## vue support
- 仅使用vue3语法,避免使用vue2。
- 新页面尽量使用组合式API。
- 组件尽量使用easycom规范。
- 非easycom的自定义vue组件,调用组件方法时需使用组件实例的`$callMethod`方式调用。
- 不使用 pinia、vuex、i18n 等uni-app x不支持的vue插件。
- 使用vue语法时需注意uni-app x官网的平台和版本兼容性,平台特殊代码需包裹在条件编译中。
## component
- 组件可使用uni-app x内置组件,以及项目下的自定义组件。通过mcp工具查询项目下可用的easycom插件。
- 项目可使用vuejs组件规范,对应的文件扩展名为uvue。
- 符合easycom规范的组件无需import和注册,可直接在template中使用。
- 使用内置组件时需注意uni-app x官网的平台和版本兼容性,平台特殊代码需包裹在条件编译中。
# conditional compilation
## core syntax
```
// Platform basic judgment
#ifdef APP || MP
//Mini programs/APP common code
#ifdef APP-ANDROID
// Android-specific logic
#endif
#ifdef APP-IOS
// IOS-specific logic
#endif
#endif
```
## Core Platform Identifier
uniVersion is used to distinguish the version of the compiler Details HBuilderX 3.9.0
APP App
APP-ANDROID App Android Platform Details
APP-IOS App iOS Platform Details
APP-HARMONY App HarmonyOS Next platform
WEB web (same as H5) HBuilderX 3.6.3
MP-WEIXIN WeChat Mini Program
MP-ALIPAY APPLET
MP-BAIDU BAIDU MINI PROGRAM
MP-TUTIAO TIKTOK MINI PROGRAM
MP-KUAISHOU Kuaishou Mini Program
MP-JD JD Mini Program
MP-HARMONY Harmony Atom Service HBuilderX 4.34
MP-XHS Xiaohongshu Mini Program
MP WeChat Mini Program/Alipay Mini Program/Baidu Mini Program/Douyin Mini Program/Feishu Mini Program/QQ Mini Program/360 Mini Program/Hongmeng atom Service
# UTS Rules
- 生成的脚本代码使用跨平台的UTS语言。
- UTS语言类似ts,但为了跨平台编译为kotlin、swift等强类型语言,进行了约束。
- UTS是强类型语言,类型要求严格,不能动态转换类型。与kotlin等强类型语言一样。
- 不能使用类型隐式转换。尤其是条件语句(if、while、do-while、三元运算符、for 循环的条件部分)必须使用布尔类型作为条件。当判断变量a是否为空时,不能写成 `if (a)`,或`if (!a)` 要写成 `if (a!=null)`
- 可为null和不可为null的类型需要严格区分,使用 `|null``?` 来定义可为空。
- 可为null的数据类型在使用其属性或方法时,需要判断不为null,或者使用`?.`安全调用。谨慎使用 `!.` 断言。
- any类型的变量在使用其属性或方法时,需要as为正确的相容类型。
- 不支持object类型,使用UTSJSONObject类型替代。
- 不支持undefined,变量使用前必须赋值。
- 对象类型定义使用type而不是interface。interface是接口,不用于对象类型定义。
- 变量和常量定义使用let和const,不使用var。
- 不使用 JSX 表达式。
- 不使用 with 语句。
- 不使用ts的结构化类型系统。使用名义类型系统,强调类型名称和继承关系以确保类型安全。
- 不使用 is 运算符。使用 instanceof 和 as 进行类型保护。
- 尽量不使用any。
- 尽量不使用 === 和!==,使用 == 和!= 替代。
- 不使用js的原型链特性。
- 严格遵守“先定义后使用”的规则。使用代码在定义代码之前。
- 更多参考: [uts与ts的差异](https://doc.dcloud.net.cn/uni-app-x/uts/uts_diff_ts.html)
# css rules
ucss是css的子集,但可以跨平台使用。除了浏览器之外,还支持App原生平台。
## 布局规范
- 禁用浮动、网格等布局,仅使用flex布局或绝对定位。
- flex布局默认方向为垂直(通过 flex-direction:column 实现)。
## 选择器规则
- 仅支持基本的类选择器 (.class),禁止使用其他选择器。
- 类名必须符合 [A-Za-z0-9_-]+ 规范,禁止使用特殊字符(例如 @class)。
## 文字样式规则
- 文字内容需放置在组件 <text> 或 <button> 中。 文字类样式(color、font-size)只能设置在 <text> 或 <button> 组件上。 其他组件(如<view>)禁止设置文本相关样式
- 文字样式不继承。
- 禁用继承相关关键字,例如 inherit 和 unset。
## 层级控制
- z-index 仅对同级兄弟节点生效。
- absolute 固定位与文档流分离,不支持分层覆盖。
## 长度单位
- 仅支持px、rpx、百分比。 字体的line-height支持em。 不能使用其他单位,如vh。
- 除非width需要根据屏幕宽度而变化才使用rpx单位。 其他场景不使用rpx单位。
- 除非长度单位需要根据父容器大小而变化才使用百分比单位。 其他场景不使用rpx单位。
## at-rules
- 仅支持`@font-face`、`@import`,不使用其他at-rules。
- 如需使用`@media` 适配不同屏幕,改用 uts 代码实现,先通过API `uni.getWindowInfo`获取屏幕宽度,再通过代码进行适配。
- 如需使用`@media` 适配暗黑模式,改用 uts 代码 和 css变量 实现。
- 如需使用`@keyframes`,改为通过UniElement对象的animate方法实现相同逻辑。
## css function
- 仅支持 url()、rgb()、rgba()、var()、env(),不使用其他css方法。
## 样式作用范围规则
- 不使用css scoped。
uni_modules
\ No newline at end of file
module.exports = {
// 指定换行的行长<int>,默认80
printWidth: 80,
// 指定每个缩进级别的空格数<int>,默认2
tabWidth: 2,
// 用制表符而不是空格缩进<bool>,默认false
useTabs: false,
// 在语句末尾添加分号<bool>,默认true
semi: true,
// 使用单引号而不是双引号<bool>,默认false
singleQuote: false,
// object对象中key值是否加引号<as-needed|consistent|preserve>,默认as-needed
// as-needed-仅在需要时在对象属性周围添加引号
// consistent-如果一个对象中至少有一个属性需要引号,所有属性添加引号
// preserve-保留对象属性中用户输入使用的引号
quoteProps: "as-needed",
// 在 JSX 中使用单引号而不是双引号<bool>,默认false
jsxSingleQuote: false,
// 在多行逗号分隔的句法结构中尽可能打印尾随逗号<es5|none|all>,默认es5
// es5-在 ES5 中有效的尾随逗号(对象、数组等),TypeScript 的类型参数中没有尾随逗号
// none-没有尾随逗号
// all-尽可能以逗号结尾(包括函数参数和调用)。要运行以这种方式格式化的 JavaScript 代码需要一个支持 ES2017(Node.js 8+ 或现代浏览器)或下层编译的引擎。这还会在 TypeScript 的类型参数中启用尾随逗号(自 2018 年 1 月发布的 TypeScript 2.7 起支持)
trailingComma: "es5",
// 对象字面量中括号之间的空格<bool>,默认true
bracketSpacing: true,
// 将>放在多行 HTML(HTML、JSX、Vue、Angular)元素最后一行的末尾,而不是单独放在下一行(不适用于自关闭元素)<bool>,默认false
// true:
// <button
// className="prettier-class"
// id="prettier-id"
// onClick={this.handleClick}>
// Click Here
// </button>
// false:
// <button
// className="prettier-class"
// id="prettier-id"
// onClick={this.handleClick}
// >
// Click Here
// </button>
bracketSameLine: true,
// 在唯一的箭头函数参数周围包含括号<always|avoid>,默认always
// always-始终包含括号
// avoid-尽可能省略括号
arrowParens: "always",
// Prettier 可以限制自己只格式化在文件顶部包含特殊注释(称为 pragma)的文件。这在逐渐将大型、未格式化的代码库过渡到 Prettier 时非常有用<bool>,默认false
requirePragma: false,
// Prettier可以在文件的顶部插入一个 @format 的特殊注释,以表明该文件已经被Prettier格式化过了。在使用 --require-pragma 参数处理一连串的文件时这个功能将十分有用。如果文件顶部已经有一个doclock,这个选项将新建一行注释,并打上 @format 标记<bool>,默认false
insertPragma: false,
// 超过最大宽度是否换行<always|never|preserve>,默认preserve
// always-如果超过最大宽度换行
// never-不要换行
// preserve-按原样显示
proseWrap: "preserve",
// 指定 HTML、Vue、Angular 和 Handlebars 的全局空格敏感度<css|strict|ignore>,默认css
// css-遵循CSS属性的默认值
// strict-所有标签周围的空格(或缺少空格)被认为是重要的
// ignore-所有标签周围的空格(或缺少空格)被认为是无关紧要的
htmlWhitespaceSensitivity: "ignore",
// vue文件script和style标签中是否缩进<bool>,默认false
vueIndentScriptAndStyle: false,
// 行尾换行符<lf|crlf|cr|auto>,默认lf
endOfLine: "lf",
// 控制 Prettier 是否格式化嵌入在文件中的引用代码<off|auto>,默认auto
// auto–如果 Prettier 可以自动识别,则格式化嵌入代码
// off-从不自动格式化嵌入代码
embeddedLanguageFormatting: "auto",
// 在 HTML、Vue 和 JSX 中强制执行每行单个属性<bool>,默认false
singleAttributePerLine: false,
};
<script lang="uts">
// #ifdef APP-ANDROID || APP-HARMONY
let firstBackTime = 0
// #endif
import { guestLogin } from './utils/reques.uts'
import type { ResponseData } from './utils/reques.uts'
// #ifdef APP
import { getDevicesInfo } from "@/uni_modules/zws-deviceInfo"
// #endif
async function getGuestLogin() {
const params = {} as object
try {
const res = await guestLogin(params) as ResponseData
if (res.code == 0) {
let guestToken : string = ''
if (res.data != null) {
const dataObj = res.data as UTSJSONObject
guestToken = dataObj.getString('guest_token') ?? ''
}
if (guestToken.length > 0) {
uni.setStorage({ key: 'guestToken', data: guestToken })
}
} else {
console.log('获取到的登录信息失败:', res.message)
}
} catch (error) {
console.log('获取到的登录信息失败:', error)
}
}
export default {
globalData: {
cameraMirror: false as boolean,
cameraPosition: 'back' as string // 'front' 或 'back'
},
onLaunch: function () {
// 初始化 device_id
// #ifdef APP
const storedDeviceId = uni.getStorageSync('device_id') as string | null
if (storedDeviceId == null || (storedDeviceId as string).length == 0) {
const deviceId = getDevicesInfo().ANDROID_ID as string
if (deviceId.length > 0) {
uni.setStorageSync('device_id', deviceId)
console.log('初始化 device_id:', deviceId)
}
}
// #endif
// #ifdef H5
const storedDeviceIdH5 = uni.getStorageSync('device_id') as string | null
if (storedDeviceIdH5 == null || (storedDeviceIdH5 as string).length == 0) {
uni.setStorageSync('device_id', 'e95615ac0e36794e')
console.log('初始化 H5 device_id: e95615ac0e36794e')
}
// #endif
getGuestLogin()
},
onShow: function () {
},
onHide: function () {
},
// #ifdef APP-ANDROID || APP-HARMONY
onLastPageBackPress: function () {
console.log('App LastPageBackPress')
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
position: 'bottom',
})
firstBackTime = Date.now()
setTimeout(() => {
firstBackTime = 0
}, 2000)
} else if (Date.now() - firstBackTime < 2000) {
firstBackTime = Date.now()
uni.exit()
}
},
// #endif
onExit: function () {
console.log('App Exit')
}
}
</script>
<style>
.hover_btn {
opacity: 0.8;
}
.loading-text {
font-size: 24rpx;
color: #999999;
}
.empty-image {
width: 220rpx;
height: 220rpx;
margin-bottom: 20rpx;
}
</style>
\ No newline at end of file
<template>
<view class="h5-camera-container">
<!-- 视频流显示 -->
<view v-if="!capturedImage" class="video-wrapper" v-html="videoHtml"></view>
<!-- Canvas用于捕获照片 -->
<canvas class="camera-canvas" id="photoCanvas"></canvas>
<!-- 已拍摄的照片 -->
<image v-if="capturedImage" class="captured-image" :src="capturedImage" mode="aspectFill"></image>
<!-- 重拍按钮 -->
<view v-if="capturedImage" class="refresh-host" @click="retake">
<image class="refresh-icon" src="/static/icon_refresh.png" mode="scaleToFill"></image>
</view>
<!-- 人像预览框 -->
<image class="preview-frame" src="/static/silhouette.png" mode="scaleToFill"></image>
<!-- 倒计时显示 -->
<view v-if="countdownVisible" class="countdown-overlay">
<text class="countdown-number">{{ countdownNumber }}</text>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, onUnmounted } from 'vue'
const capturedImage = ref<string>('')
const countdownVisible = ref<boolean>(false)
const countdownNumber = ref<number>(5)
let countdownTimerId : number | null = null
let mediaStream : any = null
const videoHtml = ref<string>('<video class="camera-video" autoplay playsinline muted webkit-playsinline x5-playsinline style="width:100%;height:100%;object-fit:cover;transform:scaleX(-1);"></video>')
// 定义 props
const props = defineProps<{
imageSrc ?: string
}>()
// 定义 emits
const emit = defineEmits<{
captured : [imageSrc: string]
retake : []
}>()
onMounted(() => {
// 如果传入了 imageSrc,直接显示
if (props.imageSrc) {
capturedImage.value = props.imageSrc
} else {
// 延迟启动摄像头,确保 DOM 已渲染
setTimeout(() => {
startCamera()
}, 300)
}
})
onUnmounted(() => {
stopCamera()
if (countdownTimerId != null) {
clearInterval(countdownTimerId as number)
countdownTimerId = null
}
})
// 启动摄像头
const startCamera = () => {
// #ifdef H5
if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
uni.showToast({
title: '浏览器不支持摄像头',
icon: 'none'
})
return
}
navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment', // 后置摄像头
width: { ideal: 480 },
height: { ideal: 640 }
},
audio: false
})
.then((stream) => {
mediaStream = stream
// 使用原生 DOM 操作
const videoElement = document.querySelector('.camera-video') as HTMLVideoElement
if (videoElement) {
videoElement.srcObject = stream
// 设置视频属性
videoElement.setAttribute('autoplay', 'true')
videoElement.setAttribute('playsinline', 'true')
videoElement.setAttribute('muted', 'true')
// 尝试播放
const playPromise = videoElement.play()
if (playPromise !== undefined) {
playPromise.catch((err) => {
console.error('视频播放失败:', err)
})
}
} else {
console.error('未找到 video 元素')
}
})
.catch((error) => {
console.error('无法访问摄像头:', error)
let errorMsg = '无法访问摄像头'
if (error.name === 'NotAllowedError') {
errorMsg = '摄像头权限被拒绝'
} else if (error.name === 'NotFoundError') {
errorMsg = '未找到摄像头设备'
}
uni.showToast({
title: errorMsg,
icon: 'none'
})
})
// #endif
}
// 停止摄像头
const stopCamera = () => {
// #ifdef H5
if (mediaStream) {
const tracks = mediaStream.getTracks()
tracks.forEach((track : any) => {
track.stop()
})
mediaStream = null
}
// #endif
}
// 拍照
const takePhoto = () => {
// #ifdef H5
const videoElement = document.querySelector('.camera-video') as HTMLVideoElement
const canvasElement = document.getElementById('photoCanvas') as HTMLCanvasElement
if (!videoElement || !canvasElement) {
console.error('未找到 video 或 canvas 元素')
uni.showToast({
title: '拍照失败',
icon: 'none'
})
return
}
// 检查视频是否准备好
if (videoElement.readyState !== videoElement.HAVE_ENOUGH_DATA) {
console.error('视频未准备好')
uni.showToast({
title: '请稍候再试',
icon: 'none'
})
return
}
const context = canvasElement.getContext('2d')
if (!context) {
console.error('无法获取 canvas context')
return
}
// 设置 canvas 尺寸与视频一致
const width = videoElement.videoWidth || 480
const height = videoElement.videoHeight || 640
canvasElement.width = width
canvasElement.height = height
// 水平翻转画布(修复镜像问题)
context.translate(width, 0)
context.scale(-1, 1)
// 绘制当前视频帧到 canvas
context.drawImage(videoElement, 0, 0, width, height)
// 重置变换
context.setTransform(1, 0, 0, 1, 0, 0)
// 将 canvas 转换为图片
const dataUrl = canvasElement.toDataURL('image/jpeg', 0.9)
capturedImage.value = dataUrl
emit('captured', dataUrl)
stopCamera()
// #endif
}
// 重拍
const retake = () => {
capturedImage.value = ''
emit('retake')
// 延迟启动摄像头
setTimeout(() => {
startCamera()
}, 100)
}
// 开始倒计时拍照
const startCountdown = () => {
if (countdownVisible.value || capturedImage.value) {
return
}
countdownNumber.value = 5
countdownVisible.value = true
if (countdownTimerId != null) {
clearInterval(countdownTimerId as number)
countdownTimerId = null
}
countdownTimerId = setInterval(() => {
if (countdownNumber.value > 1) {
countdownNumber.value = countdownNumber.value - 1
} else {
if (countdownTimerId != null) {
clearInterval(countdownTimerId as number)
countdownTimerId = null
}
countdownVisible.value = false
takePhoto()
}
}, 1000) as number
}
// 获取已拍摄的图片
const getCapturedImage = () : string => {
return capturedImage.value
}
// 暴露方法给父组件
defineExpose({
startCountdown,
getCapturedImage,
retake
})
</script>
<style scoped>
.h5-camera-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.video-wrapper {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.camera-video {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.camera-canvas {
position: absolute;
left: -9999px;
top: -9999px;
visibility: hidden;
pointer-events: none;
}
.captured-image {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.refresh-host {
position: absolute;
right: 10rpx;
top: 10rpx;
width: 52rpx;
height: 52rpx;
background: #FFFFFF;
border-radius: 26rpx;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.refresh-icon {
width: 26rpx;
height: 26rpx;
}
.preview-frame {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 506rpx;
height: 560rpx;
pointer-events: none;
z-index: 5;
}
.countdown-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 20;
}
.countdown-number {
font-weight: bold;
font-size: 180rpx;
color: #FFFFFF;
text-shadow: 0 0 20rpx rgba(0, 0, 0, 0.5);
}
</style>
\ No newline at end of file
<template>
<view class="gradient-header">
<view class="back-btn" hover-class="hover_btn" @click="onBack">
<image class="back-icon" src="/static/icon_back.png" mode="aspectFill"></image>
</view>
<text class="header-title">
<slot>{{ title }}</slot>
</text>
<view style="width: 50rpx;"></view>
</view>
</template>
<script setup lang="uts">
type Props = {
title : string
}
const props = withDefaults(defineProps<Props>(), {
title: ''
})
const emit = defineEmits<{
(e : 'back') : void
}>()
const onBack = () : void => {
// 触发返回事件,由父组件决定返回逻辑,避免重复返回
emit('back')
}
</script>
<style>
.gradient-header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 36rpx;
padding-top: 56rpx;
height: 25%;
background: linear-gradient(to bottom, #FC81C0, #FFFFFF);
}
.back-btn {
width: 50rpx;
height: 50rpx;
border-radius: 25rpx;
background-color: rgba(255, 255, 255, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.back-icon {
width: 45rpx;
height: 45rpx;
}
.header-title {
font-weight: bold;
font-size: 25rpx;
color: #000000;
}
</style>
\ No newline at end of file
<template>
<view v-if="visible" class="confirm-overlay" :class="{ 'confirm-overlay-enter': showAnimation }" @click="onOverlay">
<view class="confirm-overlay__content" @click.stop>
<view class="confirm-fixed" :class="{ 'confirm-scale-enter': showAnimation }">
<view class="confirm-container">
<text class="confirm-title">{{ title }}</text>
<view class="confirm-content-wrap">
<text class="confirm-content">{{ content }}</text>
</view>
<view class="confirm-actions">
<view v-if="showCancel" class="btn-cancel" hover-class="hover_btn" @click="onCancel">
<text class="btn-text-cancel">{{ cancelText }}</text>
</view>
<view class="btn-confirm" hover-class="hover_btn" @click="onConfirm">
<text class="btn-text-confirm">{{ confirmText }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
type PopupConfirmProps = {
visible : boolean
title : string
content : string
confirmText : string
cancelText : string
closeOnOverlay : boolean
showCancel : boolean
}
type PopupConfirmEmits = {
(e : 'confirm') : void
(e : 'cancel') : void
}
const props = withDefaults(defineProps<PopupConfirmProps>(), {
visible: false,
title: '提示',
content: '',
confirmText: '确定',
cancelText: '取消',
closeOnOverlay: true,
showCancel: true
})
const emit = defineEmits<PopupConfirmEmits>()
const showAnimation = ref(false)
// 进入时触发动画,退出时复位
watch(() : boolean => props.visible, (val : boolean) : void => {
if (val == true) {
nextTick(() => {
showAnimation.value = true
})
} else {
showAnimation.value = false
}
})
const onCancel = () : void => {
emit('cancel')
}
const onConfirm = () : void => {
emit('confirm')
}
const onOverlay = () : void => {
if (props.closeOnOverlay == true) {
emit('cancel')
}
}
</script>
<style>
.confirm-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease-out;
}
.confirm-overlay-enter {
background-color: rgba(0, 0, 0, 0.5);
}
.confirm-overlay__content {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.confirm-fixed {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
opacity: 0;
transform: scale(0.7) translateY(50rpx);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.confirm-scale-enter {
opacity: 1;
transform: scale(1) translateY(0);
}
.confirm-container {
width: 566rpx;
background-color: #FFFFFF;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
padding: 24rpx;
box-sizing: border-box;
}
.confirm-title {
font-weight: bold;
font-size: 32rpx;
color: #21061A;
}
.confirm-content-wrap {
margin-top: 20rpx;
width: 100%;
max-width: 518rpx;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.confirm-content {
font-weight: 400;
font-size: 26rpx;
color: #333333;
text-align: center;
padding: 30rpx 0;
width: 100%;
white-space: pre-wrap;
line-height: 1.5;
}
.confirm-actions {
margin-top: 36rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.btn-cancel {
width: 210rpx;
height: 72rpx;
background-color: #F4F4F5;
border-radius: 12rpx;
margin-right: 20rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-confirm {
width: 210rpx;
height: 72rpx;
background-color: #fd3da0;
border-radius: 12rpx;
display: flex;
justify-content: center;
align-items: center;
}
.btn-text-cancel {
color: #333333;
font-size: 26rpx;
font-weight: 700;
}
.btn-text-confirm {
color: #FFFFFF;
font-size: 26rpx;
font-weight: 700;
}
</style>
\ No newline at end of file
<template>
<view v-if="display" class="sheet-root" @touchmove.stop="handlePanelTouch">
<view class="sheet-mask" :class="maskClass" @click="handleMaskClick"></view>
<view class="sheet-panel-wrapper">
<view class="sheet-panel" :class="panelClass" @click.stop="handlePanelClick">
<view class="sheet-handle"></view>
<view v-if="showHeader" class="sheet-header">
<view class="sheet-header-left">
<slot name="header">
<text v-if="showTitle" class="sheet-title">{{ title }}</text>
</slot>
</view>
<view v-if="showClose" class="sheet-close-area" @click.stop="handleCloseClick">
<image class="sheet-close-icon" src="/static/icon_close.png" mode="aspectFit"></image>
</view>
</view>
<view class="sheet-content">
<scroll-view scroll-y="true" style="flex:1">
<!-- 二维码区域 -->
<view class="qr-section">
<view class="qr-code-container" v-if="qrcode">
<image :src="qrcode" class="qr-code-image" mode="aspectFit" />
</view>
<view class="qr-loading" v-else>
<text class="loading-text">二维码生成中...</text>
</view>
<text class="qr-instruction">手机扫码即可分享</text>
</view>
<slot></slot>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, watchEffect, nextTick, computed, useSlots, onBeforeUnmount } from 'vue'
import { shareQrcode, type ResponseData } from '../../utils/reques.uts'
const TRANSITION_DURATION: number = 260
type ActionSheetProps = {
visible: boolean
title: string
showClose: boolean
closeOnOverlay: boolean
worksId?: number
}
type ActionSheetEmits = {
(e: 'update:visible', value: boolean): void
(e: 'open'): void
(e: 'closed'): void
(e: 'close'): void
}
const props = withDefaults(defineProps<ActionSheetProps>(), {
visible: false,
title: '',
showClose: true,
closeOnOverlay: true,
worksId: 0
})
const emit = defineEmits<ActionSheetEmits>()
const qrcode = ref('')
const getQrcode = async (worksId: number): Promise<void> => {
try {
const res = await shareQrcode(worksId) as ResponseData
if (res.code == 0 && res.data != null) {
const dataObj = res.data as UTSJSONObject
const base64Image = dataObj.get('base_64_image')
if (base64Image != null) {
qrcode.value = base64Image as string
}
} else {
console.log('获取二维码失败:', res.message)
}
} catch (error) {
console.log('请求二维码失败:', error)
}
}
const slots = useSlots()
const display = ref<boolean>(false)
const maskVisible = ref<boolean>(false)
const panelVisible = ref<boolean>(false)
let hideTimer: number = -1
let openTimer: number = -1
const showTitle = computed((): boolean => {
if (props.title.length > 0) {
return true
}
return false
})
const hasHeaderSlot = computed((): boolean => {
const headerSlot = slots['header']
if (headerSlot != null) {
return true
}
return false
})
const showHeader = computed((): boolean => {
if (hasHeaderSlot.value == true) {
return true
}
if (showTitle.value == true) {
return true
}
if (props.showClose == true) {
return true
}
return false
})
const maskClass = computed((): string => {
if (maskVisible.value == true) {
return 'sheet-mask--show'
}
return 'sheet-mask--hide'
})
const panelClass = computed((): string => {
if (panelVisible.value == true) {
return 'sheet-panel--show'
}
return ''
})
const clearTimers = (): void => {
if (hideTimer != -1) {
clearTimeout(hideTimer)
hideTimer = -1
}
if (openTimer != -1) {
clearTimeout(openTimer)
openTimer = -1
}
}
const openPanel = (): void => {
clearTimers()
display.value = true
maskVisible.value = true
nextTick(() => {
if (props.visible == true) {
panelVisible.value = true
}
})
openTimer = setTimeout(() => {
emit('open')
openTimer = -1
}, TRANSITION_DURATION) as number
}
const closePanel = (): void => {
clearTimers()
panelVisible.value = false
maskVisible.value = false
hideTimer = setTimeout(() => {
display.value = false
emit('closed')
hideTimer = -1
}, TRANSITION_DURATION) as number
}
const handleMaskClick = (): void => {
if (props.closeOnOverlay == true) {
emit('update:visible', false)
emit('close')
}
}
const handleCloseClick = (): void => {
emit('update:visible', false)
emit('close')
}
const handlePanelClick = (): void => {}
const handlePanelTouch = (): void => {}
watchEffect(() => {
const visible = props.visible
if (visible == true) {
openPanel()
// 打开时请求二维码
if (props.worksId > 0) {
qrcode.value = ''
getQrcode(props.worksId)
}
} else {
if (display.value == true) {
closePanel()
}
}
})
onBeforeUnmount(() => {
clearTimers()
})
</script>
<style>
.sheet-root {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.sheet-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.45);
opacity: 0;
transition: opacity 0.26s ease;
}
.sheet-mask--show {
opacity: 1;
}
.sheet-mask--hide {
opacity: 0;
}
.sheet-panel-wrapper {
position: absolute;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
}
.sheet-panel {
width: 100%;
max-height: 600rpx;
background-color: #FFFFFF;
border-top-left-radius: 28rpx;
border-top-right-radius: 28rpx;
padding: 24rpx 36rpx 36rpx;
box-shadow: 0px -8rpx 32rpx rgba(0, 0, 0, 0.08);
transform: translateY(100%);
transition: transform 0.26s ease;
display: flex;
flex-direction: column;
}
.sheet-panel--show {
transform: translateY(0%);
}
.sheet-handle {
width: 110rpx;
height: 10rpx;
border-radius: 5rpx;
background-color: rgba(0, 0, 0, 0.12);
align-self: center;
margin-bottom: 24rpx;
}
.sheet-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.sheet-header-left {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.sheet-title {
margin-left: 50rpx;
font-size: 30rpx;
color: #1F1F1F;
font-weight: bold;
}
.sheet-close-area {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
background-color: rgba(0, 0, 0, 0.05);
display: flex;
justify-content: center;
align-items: center;
}
.sheet-close-icon {
width: 28rpx;
height: 28rpx;
}
.sheet-content {
flex: 1;
overflow: hidden;
}
/* 二维码区域 */
.qr-section {
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.qr-code-container {
width: 300rpx;
height: 300rpx;
display: flex;
justify-content: center;
align-items: center;
}
.qr-code-image {
width: 100%;
height: 100%;
}
.qr-loading {
width: 300rpx;
height: 300rpx;
display: flex;
justify-content: center;
align-items: center;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
.qr-instruction {
margin-top: 30rpx;
font-weight: 400;
font-size: 20rpx;
color: #A9A9B2;
text-align: center;
}
</style>
.sheet-root {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
}
.sheet-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.45);
opacity: 0;
transition: opacity 0.26s ease;
}
.sheet-mask--show {
opacity: 1;
}
.sheet-mask--hide {
opacity: 0;
}
.sheet-panel-wrapper {
position: absolute;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
}
.sheet-panel {
width: 100%;
max-height: 600rpx;
background-color: #FFFFFF;
border-top-left-radius: 28rpx;
border-top-right-radius: 28rpx;
padding: 24rpx 36rpx 36rpx;
box-shadow: 0px -8rpx 32rpx rgba(0, 0, 0, 0.08);
transform: translateY(100%);
transition: transform 0.26s ease;
display: flex;
flex-direction: column;
}
.sheet-panel--show {
transform: translateY(0%);
}
.sheet-handle {
width: 110rpx;
height: 10rpx;
border-radius: 5rpx;
background-color: rgba(0, 0, 0, 0.12);
align-self: center;
margin-bottom: 24rpx;
}
.sheet-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.sheet-header-left {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.sheet-title {
text-align: center;
font-size: 30rpx;
color: #1F1F1F;
font-weight: bold;
}
.sheet-header-right {
width: 48rpx;
height: 48rpx;
}
.sheet-close-area {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
background-color: rgba(0, 0, 0, 0.05);
display: flex;
justify-content: center;
align-items: center;
}
.sheet-close-icon {
width: 28rpx;
height: 28rpx;
}
.sheet-content {
flex: 1;
overflow: hidden;
}
.product-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
padding: 20rpx 0;
}
.product-item {
width: 150rpx;
display: flex;
flex-direction: column;
align-items: center;
margin-right: 15rpx;
margin-bottom: 30rpx;
padding: 20rpx;
border-radius: 7rpx;
}
.product-image {
width: 120rpx;
height: 120rpx;
margin-bottom: 16rpx;
}
.product-name {
font-size: 20rpx;
color: #333333;
text-align: center;
line-height: 1.4;
}
/* 加载状态样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
flex: 1;
}
.loading-gif {
width: 120rpx;
height: 120rpx;
margin-bottom: 15rpx;
}
.loading-text {
font-size: 28rpx;
color: #666666;
margin-top: 20rpx;
}
/* 无数据状态样式 */
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
flex: 1;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
<template>
<view v-if="Bindvisible" class="popup-overlay" :class="{ 'popup-overlay-enter': showAnimation }">
<view class="popup-fixed" :class="{ 'popup-scale-enter': showAnimation }">
<view class="popup-container-bg">
<!-- <image src="/static/UploadQrcode.png" class="popup-bg-image" mode="aspectFill"></image> -->
<view class="popup-container">
<text class="popup-title">
绑定设备
</text>
<view class="popup-input-container">
<input class="popup-input" placeholder-style="placeholders" type="number" placeholder="请输入设备应用码"
v-model="machine_code" />
</view>
<view class="popup-btn">
<button class="popup-btn-btn" @click="handleBind">确定</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { binds, type ResponseData } from '../../utils/reques.uts'
// #ifdef APP
import { getDevicesInfo } from "@/uni_modules/zws-deviceInfo"
// #endif
type PopupUploadProps = {
Bindvisible : boolean
closeOnOverlay : boolean
}
type PopupUploadEmits = {
(e : 'close') : void
(e : 'success') : void
}
const props = withDefaults(defineProps<PopupUploadProps>(), {
Bindvisible: false,
closeOnOverlay: false
})
const emit = defineEmits<PopupUploadEmits>()
const machine_code = ref('')
const device_id = ref('')
const showAnimation = ref(false)
onMounted(() => {
// #ifdef APP
device_id.value = getDevicesInfo().ANDROID_ID
// #endif
})
// 监听弹窗显示状态,控制动画
watch(() : boolean => props.Bindvisible, (newVal : boolean) : void => {
if (newVal) {
// 弹窗打开时,延迟一帧触发动画
nextTick(() => {
showAnimation.value = true
})
} else {
// 弹窗关闭时,重置动画状态
showAnimation.value = false
}
})
const handleBind = async () : Promise<void> => {
const params = {
machine_code: machine_code.value,
device_id: device_id.value,
} as object
try {
const res = await binds(params) as ResponseData
if (res.code == 0) {
uni.setStorage({ key: 'machine_code', data: machine_code.value })
uni.setStorage({ key: 'device_id', data: device_id.value })
uni.showToast({ title: '绑定成功', icon: 'none' })
emit('success')
emit('close')
} else {
uni.showToast({ title: res.message, icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '绑定失败', icon: 'none' })
console.log('绑定失败:', e)
}
}
</script>
<style>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
z-index: 99;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
}
.popup-overlay-enter {
background-color: rgba(0, 0, 0, 0.5);
}
.popup-fixed {
margin-top: 50rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
transform: scale(0.7);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.popup-scale-enter {
transform: scale(1);
}
.popup-container {
height: 547.92rpx;
position: relative;
z-index: 2;
align-items: center;
}
.popup-container-bg {
display: flex;
justify-content: center;
position: relative;
width: 660rpx;
height: 516rpx;
background: #FFFFFF;
border-radius: 20px;
}
.popup-title {
margin-top: 57rpx;
font-weight: bold;
font-size: 40rpx;
color: #21061A;
align-items: center;
}
.popup-bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.icon-hand,
.icon-lightning {
font-size: 18rpx;
}
.popup-input-container {
width: 100%;
align-items: center;
margin-top: 75rpx;
}
.popup-input {
width: 460rpx;
height: 88rpx;
background: #FFFFFF;
border: 2rpx solid #B2B2B2;
border-radius: 10rpx;
font-weight: 400;
font-size: 25rpx;
color: #333333;
text-align: center;
}
.placeholders {
font-family: PingFang SC;
font-weight: 400;
font-size: 36rpx;
color: #333333;
}
.popup-btn {
margin-top: 102rpx;
width: 100%;
height: 100%;
align-items: center;
}
.popup-btn-btn {
width: 50%;
height: 76rpx;
background: #FD3DA0;
border-radius: 10rpx;
font-family: PingFang SC;
font-weight: 400;
font-size: 36rpx;
color: #FFFFFF;
line-height: 76rpx;
border: none;
}
</style>
\ No newline at end of file
<template>
<view v-if="visible" class="popup-overlay" :class="{ 'popup-overlay-enter': showAnimation }">
<view class="popup-overlay__content" :class="{ 'popup-scale-enter': showAnimation }"
@click="handleContentClick">
<view class="popup-container">
<!-- 背景图片 -->
<image src="/static/popup.png" class="popup-bg-image" mode="aspectFit"></image>
<!-- 弹窗内容 -->
<view class="popup-content">
<view class="popup-title">
<text class="title-text">请选择</text>
<text class="title-text">上传的方式</text>
</view>
<view class="btn-group">
<button class="Upload-btn" hover-class="hover_btn" @click="handleUpload">手机上传</button>
<button class="Qrcode-btn" hover-class="hover_btn" @click="handleTakePhoto">设备拍照</button>
</view>
</view>
</view>
<!-- 关闭按钮 -->
<view class="close-button" @click="handleClose">
<image src="/static/icon_close.png" class="close-icon"></image>
</view>
</view>
</view>
</template>
<script setup lang="uts">
// 定义组件属性接口
interface PopupCheckProps {
visible : boolean
closeOnOverlay ?: boolean
}
// 定义组件事件接口
interface Emits {
(e : 'close') : void
(e : 'upload') : void
(e : 'takePhoto') : void
}
// 定义属性默认值
const props = withDefaults(defineProps<PopupCheckProps>(), {
visible: false
})
const emit = defineEmits<Emits>()
const showAnimation = ref(false)
// 组件挂载时的处理
onMounted(() => {
// 组件挂载完成
})
// 组件卸载时的处理
onUnmounted(() => {
// 组件卸载完成
})
// 监听弹窗显示状态,控制动画
watch(() : boolean => props.visible, (newVal : boolean) : void => {
if (newVal) {
// 弹窗打开时,延迟一帧触发动画
nextTick(() => {
showAnimation.value = true
})
} else {
// 弹窗关闭时,重置动画状态
showAnimation.value = false
}
})
// 处理遮罩层点击
const handleOverlayClick = () => {
if (props.closeOnOverlay == true) {
emit('close')
}
}
// 处理内容区域点击
const handleContentClick = () => {
// 阻止事件冒泡,防止关闭弹窗
}
// 处理关闭按钮点击
const handleClose = () => {
emit('close')
}
// 处理上传按钮点击
const handleUpload = () => {
emit('upload')
}
// 处理拍照按钮点击
const handleTakePhoto = () => {
emit('takePhoto')
}
</script>
<style>
/* 弹窗遮罩层 */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
}
.popup-overlay-enter {
background-color: rgba(0, 0, 0, 0.5);
}
/* 弹窗内容容器 */
.popup-overlay__content {
margin-top: 50rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
transform: scale(0.7);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.popup-scale-enter {
transform: scale(1);
}
/* 弹窗容器 */
.popup-container {
width: 498.61rpx;
height: 547.92rpx;
position: relative;
}
/* 弹窗背景图片 */
.popup-bg-image {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
/* 弹窗内容 */
.popup-content {
position: relative;
z-index: 2;
width: 100%;
height: 100%;
}
/* 弹窗标题 */
.popup-title {
margin: 132.17rpx 0 55.56rpx 55.56rpx;
}
/* 弹窗标题文字样式:文字样式必须设置在 <text> 上 */
/* 按钮组容器 */
.btn-group {
display: flex;
justify-content: flex-start;
margin: 0rpx 0 0 55rpx;
flex-direction: column;
}
/* 上传按钮 */
.Upload-btn {
width: 194rpx;
height: 67rpx;
background: linear-gradient(to bottom, #FD3C9F, #FCA7D3);
font-weight: bold;
font-size: 25rpx;
color: #000000;
line-height: 67rpx;
border-radius: 20rpx;
border: none;
margin-bottom: 16rpx;
}
/* 拍照按钮 */
.Qrcode-btn {
width: 194rpx;
height: 67rpx;
background: #fff5fa;
font-weight: bold;
font-size: 25rpx;
color: #000000;
line-height: 67rpx;
border-radius: 20rpx;
border: none;
}
/* 关闭按钮 */
.close-button {
margin-top: 21rpx;
width: 48rpx;
height: 48rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
border-radius: 25rpx;
display: flex;
justify-content: center;
align-items: center;
}
/* 关闭图标 */
.close-icon {
width: 48rpx;
height: 48rpx;
}
/* 弹窗标题文字样式 */
.title-text {
font-size: 36rpx;
font-weight: bold;
color: #21061A;
line-height: 1.2em;
}
</style>
\ No newline at end of file
<template>
<view v-if="visible" class="popup-overlay" :class="{ 'popup-overlay-enter': showAnimation }">
<view class="popup-fixed" :class="{ 'popup-scale-enter': showAnimation }">
<image src="/static/PopupPrintov.png" class="popup-bg-image" mode="aspectFill"></image>
<image src="/static/icon_printing.gif" class="icon_success" />
<text class="print_text">{{ printText }}</text>
</view>
</view>
</template>
<script setup lang="uts">
type PopupUploadProps = {
visible : boolean
closeOnOverlay : boolean
printText : string
}
type PopupUploadEmits = {
(e : 'close') : void
}
const props = withDefaults(defineProps<PopupUploadProps>(), {
visible: false,
closeOnOverlay: false,
printText: '打印中...'
})
const emit = defineEmits<PopupUploadEmits>()
const showAnimation = ref(false)
// 监听弹窗显示状态,控制动画
watch(() : boolean => props.visible, (newVal : boolean) : void => {
if (newVal) {
// 弹窗打开时,延迟一帧触发动画
nextTick(() => {
showAnimation.value = true
})
} else {
// 弹窗关闭时,重置动画状态
showAnimation.value = false
}
})
const handleOverlayClick = () => {
if (props.closeOnOverlay == true) {
emit('close')
}
}
const handleClose = () => {
emit('close')
}
</script>
<style>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
}
.popup-overlay-enter {
background-color: rgba(0, 0, 0, 0.5);
}
.popup-fixed {
margin-top: 50rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 440.97rpx;
height: 417.36rpx;
transform: scale(0.7);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.popup-scale-enter {
transform: scale(1);
}
.popup-bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.icon_success {
width: 100rpx;
height: 100rpx;
margin-bottom: 33rpx;
}
.print_text {
font-weight: bold;
font-size: 28rpx;
color: #FD3DA0;
align-items: center;
;
}
</style>
\ No newline at end of file
<template>
<view v-if="visible" class="popup-overlay" :class="{ 'popup-overlay-enter': showAnimation }">
<view class="popup-fixed" :class="{ 'popup-scale-enter': showAnimation }">
<image src="/static/PopupPrintov.png" class="popup-bg-image" mode="aspectFill"></image>
<image src="/static/icon_success.png" class="icon_success" />
<text class="print_text">您的作品已经打印完成</text>
<text class="print_text">请及时取货</text>
</view>
</view>
</template>
<script setup lang="uts">
type PopupUploadProps = {
visible : boolean
closeOnOverlay : boolean
}
type PopupUploadEmits = {
(e : 'close') : void
}
const props = withDefaults(defineProps<PopupUploadProps>(), {
visible: false,
closeOnOverlay: false
})
const emit = defineEmits<PopupUploadEmits>()
const showAnimation = ref(false)
// 监听弹窗显示状态,控制动画
watch((): boolean => props.visible, (newVal: boolean): void => {
if (newVal) {
// 弹窗打开时,延迟一帧触发动画
nextTick(() => {
showAnimation.value = true
})
} else {
// 弹窗关闭时,重置动画状态
showAnimation.value = false
}
})
const handleOverlayClick = () => {
if (props.closeOnOverlay == true) {
emit('close')
}
}
const handleClose = () => {
emit('close')
}
</script>
<style>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
}
.popup-overlay-enter {
background-color: rgba(0, 0, 0, 0.5);
}
.popup-fixed {
margin-top: 50rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 440.97rpx;
height: 417.36rpx;
transform: scale(0.7);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.popup-scale-enter {
transform: scale(1);
}
.popup-bg-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.icon_success {
width: 100rpx;
height: 100rpx;
margin-bottom: 33rpx;
}
.print_text {
font-weight: bold;
font-size: 28rpx;
color: #FD3DA0;
align-items: center;
;
}
</style>
\ No newline at end of file
<template>
<view v-if="visible" class="popup-overlay" :class="{ 'popup-overlay-enter': showAnimation }">
<view class="popup-overlay__content" :class="{ 'popup-scale-enter': showAnimation }" @click="handleContentClick">
<view class="lang-container">
<!-- 顶部留白占位,视觉更贴近设计稿 -->
<view class="lang-body">
<!-- #ifdef APP -->
<scroll-view class="list-scroll">
<view v-for="item in langList" :key="item.value" class="lang-item" @click="handleSelect(item)">
<text class="lang-text">{{ item.label }}</text>
<text v-if="selectedValue == item.value" class="check-text">✔</text>
</view>
<!-- 调试信息:显示数据长度 -->
<view v-if="langList.length == 0" class="debug-info">
<text class="debug-text">暂无语言数据</text>
</view>
</scroll-view>
<!-- #endif -->
</view>
<view class="action-bar">
<button class="btn-cancel" hover-class="hover_btn" @click="handleCancel">
取消
</button>
<button class="btn-confirm" hover-class="hover_btn" @click="handleConfirm">
确定
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { LanguageOption } from '../../types/index.uts'
// 语言选项类型统一在 types/language.uts 中定义
// 组件属性(避免与其它文件的 Props 命名冲突)
interface LangProps {
visible : boolean
closeOnOverlay : boolean
languages : Array<LanguageOption> | null
modelValue : string | null
}
// 组件事件
interface Emits {
(e : 'close') : void
(e : 'cancel') : void
(e : 'confirm', value : string) : void
(e : 'update:modelValue', value : string) : void
(e : 'change', value : string) : void
}
const props = withDefaults(defineProps<LangProps>(), {
visible: false,
closeOnOverlay: false
})
const emit = defineEmits<Emits>()
// 最终用于渲染的语言列表,只使用从父组件传入的数据
const langList = computed<Array<LanguageOption>>(() => {
if (props.languages != null && props.languages.length > 0) {
return props.languages as Array<LanguageOption>
}
// 如果没有传入数据,返回空数组
return [] as Array<LanguageOption>
})
const selectedValue = ref<string>((props.modelValue != null) ? (props.modelValue as string) : 'zh-Hans')
const showAnimation = ref(false)
// 监听外部传入的 modelValue 变化
const modelValueRef = computed<string | null>(() => props.modelValue as string | null)
watch(modelValueRef, (nv : string | null, _ov : string | null) : void => {
if (nv != null) {
selectedValue.value = nv as string
}
})
// 监听外部传入的 languages 变化,用于调试
const languagesRef = computed<Array<LanguageOption> | null>(() => props.languages)
watch(languagesRef, (nv : Array<LanguageOption> | null, _ov : Array<LanguageOption> | null) : void => {
}, { immediate: true })
// 监听弹窗显示状态,控制动画
watch((): boolean => props.visible, (newVal: boolean): void => {
if (newVal) {
// 弹窗打开时,延迟一帧触发动画
nextTick(() => {
showAnimation.value = true
})
} else {
// 弹窗关闭时,重置动画状态
showAnimation.value = false
}
})
const handleOverlayClick = () => {
if (props.closeOnOverlay == true) {
emit('close')
}
}
const handleContentClick = () => {
// 阻止事件冒泡
}
const handleClose = () => {
emit('close')
}
const handleCancel = () => {
emit('cancel')
handleClose()
}
const handleConfirm = () => {
emit('confirm', selectedValue.value)
emit('update:modelValue', selectedValue.value)
handleClose()
}
const handleSelect = (item : LanguageOption) => {
selectedValue.value = item.value
emit('update:modelValue', item.value)
emit('change', item.value)
}
</script>
<style>
/* 遮罩层,与其它弹窗保持一致 */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
}
.popup-overlay-enter {
background-color: rgba(0, 0, 0, 0.5);
}
.popup-overlay__content {
margin-top: 50rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
transform: scale(0.7);
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.popup-scale-enter {
transform: scale(1);
}
/* 容器 */
.lang-container {
width: 640rpx;
background-color: #FFFFFF;
border-radius: 24rpx;
padding: 32rpx 24rpx 24rpx 24rpx;
box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.2);
}
.lang-body {
max-height: 800rpx;
}
.list-scroll {
width: 100%;
height: 800rpx;
}
.list-view {
width: 100%;
}
.lang-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 96rpx;
border-bottom: 1rpx solid #EEEEEE;
padding: 0 8rpx 0 8rpx;
}
.lang-text {
font-size: 28rpx;
color: #333333;
}
.check-text {
font-size: 28rpx;
color: #111111;
}
.action-bar {
margin-top: 24rpx;
display: flex;
flex-direction: row;
justify-content: space-around;
}
.btn-cancel {
width: 181rpx;
height: 83rpx;
background: #FFFFFF;
border-radius: 14rpx;
border: 2rpx solid #FF86C3;
font-family: Source Han Sans CN;
font-weight: bold;
font-size: 24rpx;
color: #000000;
margin-right: 20rpx;
line-height: 83rpx;
}
.btn-confirm {
width: 181rpx;
height: 83rpx;
background: #FD3DA0;
border-radius: 14rpx;
font-family: Source Han Sans CN;
font-weight: bold;
font-size: 24rpx;
color: #FFFFFF;
border: none;
line-height: 83rpx;
}
.debug-info {
padding: 20rpx;
text-align: center;
}
.debug-text {
font-size: 24rpx;
color: #999999;
}
</style>
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<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"></script>
</body>
</html>
\ No newline at end of file
import App from './App.uvue'
import PopupConfirm from './components/Common/PopupConfirm.uvue'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
// 注册全局组件
app.component('PopupConfirm', PopupConfirm)
return {
app
}
}
\ No newline at end of file
{
"name": "ColorAI",
"appid": "__UNI__8013F16",
"description": "AI拍照机,一款可将相片实时通过AI,模型转换成指定场景的设备",
"versionName": "1.1.28",
"versionCode": "112",
"uni-app-x": {},
/* 快应用特有相关 */
"quickapp": {},
/* 小程序特有相关 */
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"app": {
"distribute": {
"icons": {
"android": {
"hdpi": "",
"xhdpi": "",
"xxhdpi": "",
"xxxhdpi": ""
}
}
}
},
"app-android": {
"distribute": {
"modules": {},
"icons": {
"hdpi": "C:/Users/20224/Documents/GitHub/ai-app-uniapp/static/logo.png",
"xhdpi": "C:/Users/20224/Documents/GitHub/ai-app-uniapp/static/logo.png",
"xxhdpi": "C:/Users/20224/Documents/GitHub/ai-app-uniapp/static/logo.png",
"xxxhdpi": "C:/Users/20224/Documents/GitHub/ai-app-uniapp/static/logo.png"
},
"splashScreens": {
"default": {}
}
}
},
"app-ios": {
"distribute": {
"modules": {},
"icons": {},
"splashScreens": {}
}
},
"web": {
"router": {
"mode": "",
"base": "/ai_camera"
}
}
}
\ No newline at end of file
{
"name": "ai-app-uniapp",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ai-app-uniapp",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.2.0"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
}
}
}
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/stop/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/list/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/camera/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/gener/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/posture/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path" : "pages/print/index",
"style" :
{
"navigationBarTitleText" : ""
}
},
{
"path": "pages/printlist/index",
"style": {
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app x",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "custom"
},
"uniIdRouter": {}
}
<template>
<view class="take-photo-container" @touchstart="resetTimer">
<image class="background-image" src="/static/takebg.png" mode="aspectFill"></image>
<view class="back-btn" hover-class="hover_btn" @click="handleBack">
<image class="back-icon" src="/static/icon_back.png" mode="aspectFill"></image>
</view>
<text class="header-title">艺术照拍摄</text>
<view class="main-content">
<view class="left-section">
<image class="wrong-img" src="/static/wrong.png" />
<image class="right-img" src="/static/icon_right.png" />
<text class="section-text">五官清晰,正面半身照</text>
</view>
<view class="right-section">
<view class="error-examples">
<view class="error-example">
<image class="error-img" src="/static/wrong_one.png" />
<image class="left-img" src="/static/icon_wrong.png" />
<view>不是正面</view>
</view>
<view style="height: 22rpx"></view>
<view class="error-example">
<image class="error-img" src="/static/wrong_two.png" />
<image class="left-img" src="/static/icon_wrong.png" />
<view>有遮挡</view>
</view>
<view style="height: 22rpx"></view>
<view class="error-example">
<image class="error-img" src="/static/wrong_three.png" />
<image class="left-img" src="/static/icon_wrong.png" />
<view>过于模糊</view>
</view>
</view>
</view>
</view>
<view class="bottom-section">
<view class="take-photo-btn" hover-class="hover_btn" @click="take">
<image class="take-photo-icon" src="/static/icon_takepictures.png" /><text
class="take-photo-text">点击拍照</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
let inactivityTimer : number = 0
const INACTIVITY_TIMEOUT = 3 * 60 * 1000 // 3分钟
const handleBack = () => {
uni.navigateBack()
}
const take = () => {
uni.navigateTo({
url: '/pages/camera/index'
})
}
// 重置定时器
const resetTimer = () => {
// 清除现有定时器
if (inactivityTimer > 0) {
clearTimeout(inactivityTimer)
}
// 启动新的定时器
inactivityTimer = setTimeout(() => {
// 3分钟无操作,跳转到首页
uni.reLaunch({
url: '/pages/index/index'
})
}, INACTIVITY_TIMEOUT)
}
// 页面加载时启动定时器
onLoad(() => {
resetTimer()
})
// 页面隐藏时清除定时器
onHide(() => {
if (inactivityTimer > 0) {
clearTimeout(inactivityTimer)
inactivityTimer = 0
}
})
// 页面卸载时清除定时器
onUnload(() => {
if (inactivityTimer > 0) {
clearTimeout(inactivityTimer)
inactivityTimer = 0
}
})
</script>
<style>
.take-photo-container {
height: 100%;
background-color: #f8f8f8;
display: flex;
flex-direction: column;
position: relative;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.back-btn {
margin-top: 56rpx;
margin-left: 36rpx;
width: 50rpx;
height: 50rpx;
border-radius: 25rpx;
background-color: rgba(255, 255, 255, 0.5);
display: flex;
justify-content: center;
align-items: center;
position: relative;
z-index: 1;
}
.back-icon {
width: 45rpx;
height: 45rpx;
}
.header-title {
margin-top: 31rpx;
font-weight: bold;
font-size: 36rpx;
color: #000000;
text-align: center;
}
/* 头部样式 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 40rpx 48rpx;
}
.exit-btn {
color: #333;
font-size: 32rpx;
padding: 16rpx 24rpx;
border-radius: 12rpx;
}
.title {
display: flex;
align-items: center;
font-size: 36rpx;
font-weight: bold;
color: #333;
}
/* 主要内容区域 */
.main-content {
margin-top: 81rpx;
display: flex;
justify-content: space-evenly;
flex-direction: row;
height: 50%;
position: relative;
z-index: 1;
}
/* 左侧区域 */
.left-section {
position: relative;
width: 370.83rpx;
height: 508.33rpx;
overflow: visible;
}
.wrong-img {
width: 100%;
height: 100%;
}
.right-img {
position: absolute;
bottom: -40rpx;
right: -20rpx;
width: 66rpx;
height: 66rpx;
}
.section-text {
font-weight: bold;
font-size: 21rpx;
color: #333333;
text-align: center;
line-height: 2.8em;
}
/* 右侧区域 */
.right-section {
width: 30%;
height: 90%;
}
.error-examples {
display: flex;
flex-direction: column;
align-items: center;
}
/* 删除 mt-22 工具类,改为内联占位 view 实现间距 */
.error-example {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: visible;
font-weight: 400;
font-size: 19rpx;
color: #333333;
}
.error-img {
width: 139.58rpx;
height: 136.81rpx;
margin-bottom: 10rpx;
}
.left-img {
position: absolute;
bottom: 10rpx;
right: -10rpx;
width: 40.28rpx;
height: 40.28rpx;
}
.Loading {
position: fixed;
z-index: 10001;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.98);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 76rpx;
font-family: Source Han Sans CN;
font-weight: 400;
color: #333333;
box-shadow: 0 0 100rpx rgba(0, 0, 0, 0.1);
}
/* 底部区域 */
.bottom-section {
padding: 48rpx;
height: 100%;
position: relative;
z-index: 1;
}
.take-photo-btn {
max-width: 500rpx;
border: none;
width: 407.64rpx;
height: 97.22rpx;
background: linear-gradient(to left, #FD3C9F, #FCA7D3);
border-radius: 20rpx;
flex-direction: row;
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin: 0 auto;
}
.take-photo-icon {
margin-right: 20rpx;
width: 29.86rpx;
height: 27.08rpx;
}
.take-photo-text {
font-size: 28rpx;
font-weight: bold;
color: #333333;
}
</style>
\ No newline at end of file
<template>
<!-- 页面容器使用flex垂直布局,简单静态内容无需滚动容器 -->
<view class="page">
<!-- <view class="back-btn" hover-class="hover_btn" @click="handleBack">
<image class="back-icon" src="/static/icon_back.png" mode="aspectFill"></image>
</view> -->
<view class="content">
<image class="icon" src="/static/icon_guaqi.png" mode="aspectFill"></image>
<!-- <text class="title">设备挂起</text> -->
<text class="desc">请稍等,我很快恢复...</text>
</view>
<!-- 底部联系方式,绝对定位固定在页面底部 -->
<view class="footer">
<!-- <text class="contact">请联系管理员</text> -->
<view class="refresh-btn" hover-class="hover_btn" @click="handlerefresh">刷新</view>
</view>
</view>
</template>
<script setup lang="uts">
import { getMachineApp } from '../../utils/reques.uts'
import type { ResponseData } from '../../utils/reques.uts'
// 1分钟倒计时,结束后自动返回首页
// 严格类型:定时器id为 number,可为空
let countdownTimer: number | null = null
let remainingSeconds: number = 60
// 页面加载时启动倒计时
onLoad((): void => {
remainingSeconds = 60
// 每秒减少一次,直到为0
countdownTimer = setInterval(() => {
remainingSeconds = remainingSeconds - 1
if (remainingSeconds <= 0) {
// 结束倒计时并跳转首页
if (countdownTimer != null) {
clearInterval(countdownTimer as number)
countdownTimer = null
}
uni.reLaunch({
url: '/pages/index/index'
})
}
}, 1000) as number
})
// 页面卸载时清理定时器,避免内存泄漏
onUnload((): void => {
if (countdownTimer != null) {
clearInterval(countdownTimer as number)
countdownTimer = null
}
})
const getMachineinfo = async () : Promise<void> => {
const params = {} as object
try {
const res : ResponseData = await getMachineApp(params) as ResponseData
if (res.code == 0) {
uni.reLaunch({
url: '/pages/index/index'
})
}
} catch (error) {
console.log('获取机器信息失败:', error)
}
}
const handlerefresh = () => {
getMachineinfo()
}
</script>
<style>
.page {
/* 页面背景与布局 */
flex: 1;
background-color: #F7F7F7;
align-items: center;
}
.back-btn {
position: absolute;
left: 50rpx;
top: 50rpx;
width: 50rpx;
height: 50rpx;
border-radius: 25rpx;
background-color: rgba(255, 255, 255, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.back-icon {
width: 45rpx;
height: 45rpx;
}
.content {
/* 通过 flex 垂直居中内容,不使用 margin 顶起 */
flex: 1;
justify-content: center;
align-items: center;
}
.icon {
width: 426rpx;
height: 426rpx;
}
.title {
font-size: 48rpx;
color: #000000;
margin-top: 28rpx;
}
.desc {
font-size: 42rpx;
color: #000000;
margin-top: 12px;
}
.footer {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100rpx;
align-items: center;
}
.refresh-btn {
width: 160rpx;
height: 80rpx;
border-radius: 40rpx 0 40rpx 0;
color: #000000ff;
border: 2rpx solid #FF86C3;
font-size: 42rpx;
display: flex;
justify-content: center;
align-items: center;
}
.contact {
font-size: 30rpx;
color: #000000;
}
</style>
\ No newline at end of file
// 参考链接 https://doc.dcloud.net.cn/uni-app-x/tutorial/ls-plugin.html#setting
{
"targets": [
"APP-ANDROID"
]
}
\ No newline at end of file
This diff is collapsed. Click to expand it.
// 全局数据类型定义
// 定义设备数据类型
export type machineItems = {
value : Number
label : string
}
// 定义产品分类数据类型
export type categoryItem = {
value : string
label : string
default_icon : string
}
export type LanguageOption = {
label : string
value : string
}
export type AdsOption = {
image : string
}
/**
* 这里是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;
## 1.0.0(2025-01-19)
初始版本
{
"id": "zws-deviceInfo",
"displayName": "zws-deviceInfo",
"version": "1.0.0",
"description": "zws-deviceInfo",
"keywords": [
"zws-deviceInfo"
],
"repository": "",
"engines": {
"HBuilderX": "^3.6.8"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": "3973360713"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-android": "y",
"app-ios": "u",
"app-harmony": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}
\ No newline at end of file
# zws-deviceInfo
### 开发文档
> 引入zws-encrypt
```
import { getDevicesInfo } from "@/uni_modules/zws-deviceInfo"
```
> 方法调用示例
```
getDevicesInfo().ANDROID_ID
```
> 方法说明表
| 方法名 | 作用 |
|-------------------------------|--------------|
| getDevicesInfo().ANDROID_ID | 安卓ID(可用做设备唯一标识) |
| getDevicesInfo().MANUFACTURER | 制造商 |
| getDevicesInfo().MODEL | 设备型号 |
| getDevicesInfo().BOARD | 主板名称 |
| getDevicesInfo().PRODUCT | 产品名称 |
| getDevicesInfo().BRAND | 设备品牌 |
| getDevicesInfo().HOST | 执行代码编译的HOST值 |
| getDevicesInfo().SDK_INT | 安卓SDK代码 |
| getDevicesInfo().RELEASE | 安卓系统版本号 |
| getDevicesInfo().SERIAL | 序列号(已被弃用) |
| getDevicesInfo().HARDWARE | 硬件信息 |
| getDevicesInfo().FINGERPRINT | 设备指纹 |
\ No newline at end of file
{
"minSdkVersion": "21"
}
\ No newline at end of file
import Build from "android.os.Build"
import Settings from "android.provider.Settings"
import { UTSAndroid } from "io.dcloud.uts";
type DevicesInfo = {
ANDROID_ID : String,
MANUFACTURER : String,
MODEL : String,
BOARD : String,
PRODUCT : String,
BRAND : String,
HOST : String,
SDK_INT : Int,
RELEASE : String,
SERIAL : String,
HARDWARE : String,
FINGERPRINT : String
}
export function getDevicesInfo() : DevicesInfo {
const context = UTSAndroid.getAppContext();
let androidId = "";
const tempAndroidId = Settings.Secure.getString(context?.contentResolver, Settings.Secure.ANDROID_ID);
if (tempAndroidId != null && tempAndroidId !== "") {
androidId = tempAndroidId;
}
//3. 兼容处理 SERIAL(低版本兼容 + 高版本返回空)
let serial = "";
if (Build.VERSION.SDK_INT < 26) {
//@ts-ignore 抑制低版本废弃警告
const tempSerial = Build.SERIAL;
if (tempSerial != null && tempSerial !== "") {
serial = tempSerial;
}
}
return {
ANDROID_ID: androidId != null ? androidId : "unknown",
MANUFACTURER: Build.MANUFACTURER != null ? Build.MANUFACTURER : "unknown",
MODEL: Build.MODEL != null ? Build.MODEL : "unknown",
BOARD: Build.BOARD != null ? Build.BOARD : "unknown",
PRODUCT: Build.PRODUCT != null ? Build.PRODUCT : "unknown",
BRAND: Build.BRAND != null ? Build.BRAND : "unknown",
HOST: Build.HOST != null ? Build.HOST : "unknown",
SDK_INT: Build.VERSION.SDK_INT,
RELEASE: Build.VERSION.RELEASE != null ? Build.VERSION.RELEASE : "unknown",
SERIAL: serial != null ? serial : "unknown",
HARDWARE: Build.HARDWARE != null ? Build.HARDWARE : "unknown",
FINGERPRINT: Build.FINGERPRINT != null ? Build.FINGERPRINT : "unknown"
}
/*const res:DevicesInfo={
ANDROID_ID:Settings.Secure.getString(context?.contentResolver,Settings.Secure.ANDROID_ID),
MANUFACTURER:Build.MANUFACTURER,
MODEL:Build.MODEL,
BOARD:Build.BOARD,
PRODUCT:Build.PRODUCT,
BRAND:Build.BRAND,
HOST:Build.HOST,
SDK_INT:Build.VERSION.SDK_INT,
RELEASE:Build.VERSION.RELEASE,
SERIAL:Build.SERIAL,
HARDWARE:Build.HARDWARE,
FINGERPRINT:Build.FINGERPRINT
};
return res; */
// 3. 构建设备信息(统一空值兜底)
\ No newline at end of file
/**
* 引用 iOS 系统库,示例如下:
* import { UIDevice } from "UIKit";
* [可选实现,按需引入]
*/
/* 引入 interface.uts 文件中定义的变量 */
import { MyApiOptions, MyApiResult, MyApi, MyApiSync } from '../interface.uts';
/* 引入 unierror.uts 文件中定义的变量 */
import { MyApiFailImpl } from '../unierror';
/**
* 引入三方库
* [可选实现,按需引入]
*
* 在 iOS 平台引入三方库有以下两种方式:
* 1、通过引入三方库framework 或者.a 等方式,需要将 .framework 放到 ./Frameworks 目录下,将.a 放到 ./Libs 目录下。更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#ios-平台原生配置)
* 2、通过 cocoaPods 方式引入,将要引入的 pod 信息配置到 config.json 文件下的 dependencies-pods 字段下。详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-ios-cocoapods.html)
*
* 在通过上述任意方式依赖三方库后,使用时需要在文件中 import:
* 示例:import { LottieLoopMode } from 'Lottie'
*/
/**
* UTSiOS 为平台内置对象,不需要 import 可直接调用其API,[详见](https://uniapp.dcloud.net.cn/uts/utsios.html)
*/
/**
* 异步方法
*
* uni-app项目中(vue/nvue)调用示例:
* 1、引入方法声明 import { myApi } from "@/uni_modules/uts-api"
* 2、方法调用
* myApi({
* paramA: false,
* complete: (res) => {
* console.log(res)
* }
* });
*
*/
export const myApi : MyApi = function (options : MyApiOptions) {
if (options.paramA == true) {
// 返回数据
const res : MyApiResult = {
fieldA: 85,
fieldB: true,
fieldC: 'some message'
};
options.success?.(res);
options.complete?.(res);
} else {
// 返回错误
let failResult = new MyApiFailImpl(9010001);
options.fail?.(failResult)
options.complete?.(failResult)
}
}
/**
* 同步方法
*
* uni-app项目中(vue/nvue)调用示例:
* 1、引入方法声明 import { myApiSync } from "@/uni_modules/uts-api"
* 2、方法调用
* myApiSync(true);
*
*/
export const myApiSync : MyApiSync = function (paramA : boolean) : MyApiResult {
// 返回数据,根据插件功能获取实际的返回值
const res : MyApiResult = {
fieldA: 85,
fieldB: paramA,
fieldC: 'some message'
};
return res;
}
/**
* 更多插件开发的信息详见:https://uniapp.dcloud.net.cn/plugin/uts-plugin.html
*/
// const apiurl = `http://192.168.31.147:3001/ai-camera`
// const apiurl = `http://test.refinecolor.com/ai-camera`
const apiurl = `http://ai.colorpark.cn/ai-camera`
// const apiurl = `http://tprint-dev.refinecolor.com/ai-camera`
type ApiEndpoints = {
binds: string
languages: string
list: string
category: string
render: string
queryRenderResult: string
goodscategory: string
ossUpload:string
creatework:string
getmachines:string
getgoods:string
uploadQrcode:string
uploadurl:string
getMachineApp:string
guestLogin:string
print:string
createorder:string
queryStatus:string
cancel:string
queueList:string
ads:string
shareQrcode:string
}
const api: ApiEndpoints = {
binds: `${apiurl}/index/binds`,
languages: `${apiurl}/index/languages`,
list: `${apiurl}/aiModel/list`,
category: `${apiurl}/aiModel/category`,
render: `${apiurl}/aiModel/render`,
queryRenderResult: `${apiurl}/aiModel/queryRenderResult/`,
goodscategory: `${apiurl}/index/category`,
ossUpload: `${apiurl}/index/ossUpload`,
creatework: `${apiurl}/works/create`,
getmachines: `${apiurl}/index/machines`,
getgoods: `${apiurl}/index/goods`,
uploadQrcode: `${apiurl}/index/uploadQrcode`,
uploadurl: `${apiurl}/aiModel/render`,
getMachineApp: `${apiurl}/index/getMachineApp`,
guestLogin: `${apiurl}/index/guestLogin`,
print: `${apiurl}/aiModel/print`,
createorder: `${apiurl}/order/create`,
queryStatus: `${apiurl}/order/queryStatus/`,
cancel: `${apiurl}/aiModel/cancel/`,
queueList: `${apiurl}/index/queueList`,
ads: `${apiurl}/index/ads`,
shareQrcode: `${apiurl}/index/shareQrcode`,
}
export default api
\ No newline at end of file
import api from "./api.uts"
// 定义并导出响应数据类型
export type ResponseData = {
code : number
message : string
// data/meta 可能为对象或数组,保持为 any,由调用方按需转换
data ?: any | null
meta ?: any | null
}
// 缓存获取函数,确保每次都获取最新的缓存值
const getAuthHeaders = () : UTSJSONObject => {
const machine_code = uni.getStorageSync('machine_code')
const guest_Token = uni.getStorageSync('guestToken') ?? ''
const device_id = uni.getStorageSync('device_id') ?? ''
return {
'machine-code': machine_code,
'Authorization': `Guest ${guest_Token}`,
'device-id': device_id,
} as UTSJSONObject
}
// 显示Toast后延迟跳转,避免立即reLaunch导致Toast不显示
function showToastThenReLaunch(msg : string, url : string, duration : number = 4500) : void {
// 先关闭可能存在的Loading遮罩,确保Toast可见
uni.showToast({
title: msg,
icon: 'none',
duration: duration
})
// 延迟跳转,等待Toast展示完成
setTimeout(() : void => {
uni.reLaunch({
url: url
})
}, duration)
}
const request = (url : string, method : string, data : any) => {
return new Promise((resolve, reject) => {
uni.request({
url: url,
method: method as RequestMethod,
timeout: 15000,
data: data,
header: getAuthHeaders(),
success: (res) => {
// 直接使用 res.data,不进行类型转换
const responseData = res.data as UTSJSONObject
const code = responseData.getNumber('code') ?? 0
const message = responseData.getString('message') ?? ''
const dataField = responseData.getAny('data')
const meta = responseData.getAny('meta')
if ((code as number) == 0) {
const result : ResponseData = {
code: (code as number),
message: message,
data: dataField != null ? dataField : null,
meta: meta != null ? meta : null,
}
resolve(result)
} else {
console.log('message', message)
if ((code as number) == 130001) {
showToastThenReLaunch(message, '/pages/stop/index', 4500)
return;
}
// 处理错误码130002 - 清除machine_code并跳转首页
if ((code as number) == 130002) {
uni.removeStorage({
key: 'machine_code',
success: function (res) {
console.log('success,清除machine_code并跳转首页');
}
});
// 先显示提示,再延迟跳转,避免Toast未显示
showToastThenReLaunch(message, '/pages/index/index', 4500)
return;
}
// 对于其他错误码,reject promise 以便调用方的 catch 块能够处理
reject({ code: code, message: message })
}
},
fail: (error) => {
uni.showToast({
title: `网络错误`,
icon: 'error'
})
reject(error)
}
})
})
}
const ossUpload = (
filePath : string
) => {
return new Promise((resolve, reject) => {
uni.showLoading({
title: '上传中...',
mask: true
})
uni.uploadFile({
url: api.ossUpload as string,
filePath: filePath,
name: 'file',
timeout: 15000,
header: getAuthHeaders(),
success(res) {
try {
const dataStr = (res.data as string) ?? ''
if (dataStr.length == 0) {
const emptyResult : ResponseData = { code: 0, message: '', data: null }
resolve(emptyResult)
return
}
const responseJSON = JSON.parse(dataStr) as UTSJSONObject
const code = responseJSON.getNumber('code') ?? 0
const message = responseJSON.getString('message') ?? ''
const dataField = responseJSON.getAny('data')
// 处理错误码130002 - 清除machine_code并跳转首页
if (code == 130002) {
uni.removeStorage({
key: 'machine_code',
success: function (res) {
console.log('success');
}
});
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
});
return;
}
const result : ResponseData = {
code: (code as number),
message: message,
data: dataField != null ? (dataField as UTSJSONObject) : null
}
uni.hideLoading()
resolve(result)
} catch (e) {
uni.hideLoading()
uni.showToast({
title: '响应解析失败',
icon: 'error'
})
reject(e as any)
}
},
fail(error_1) {
uni.hideLoading()
uni.showToast({
title: '网络错误',
icon: 'error'
})
reject(error_1)
}
})
})
}
const binds = (params : object) => {
return request(api.binds as string, 'POST', params)
}
const guestLogin = (params : object) => {
return request(api.guestLogin as string, 'POST', params)
}
const languages = (params : object) => {
return request(api.languages as string, 'POST', params)
}
const list = (params : object) => {
return request(api.list as string, 'POST', params)
}
const category = (params : object) => {
return request(api.category as string, 'POST', params)
}
const queryRenderResult = (prompt_id : string) => {
return request(api.queryRenderResult + prompt_id as string, 'GET', {})
}
const goodscategory = (params : object) => {
return request(api.goodscategory as string, 'POST', params)
}
const creatework = (params : object) => {
return request(api.creatework as string, 'POST', params)
}
const getmachines = (params : object) => {
return request(api.getmachines as string, 'POST', params)
}
const getgoods = (params : object) => {
return request(api.getgoods as string, 'POST', params)
}
const uploadQrcode = (fd : string) => {
return request(api.uploadQrcode + fd as string, 'POST', {})
}
const uploadurl = (params : object) => {
return request(api.uploadurl as string, 'POST', params)
}
const getMachineApp = (params : object) => {
return request(api.getMachineApp as string, 'POST', params)
}
const print = (params : object) => {
return request(api.print as string, 'POST', params)
}
const createorder = (params : object) => {
return request(api.createorder as string, 'POST', params)
}
const queryStatus = (params : object) => {
return request(api.queryStatus as string, 'POST', params)
}
const cancel = (params : object) => {
return request(api.cancel as string, 'POST', params)
}
const queueList = (params : object) => {
return request(api.queueList as string, 'POST', params)
}
const ads = (params : object) => {
return request(api.ads as string, 'POST', params)
}
const shareQrcode = (works_id: number) => {
return request(api.shareQrcode + '?works_id=' + works_id.toString(), 'GET', {})
}
export { binds, guestLogin, list, category, languages, queryRenderResult, uploadurl, ossUpload, goodscategory, creatework, getmachines, getgoods, uploadQrcode, getMachineApp, print, createorder, queryStatus, cancel, queueList, ads, shareQrcode}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment