Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d1e709be9 | ||
|
|
182fd11399 | ||
|
|
ee29bdfcc8 | ||
|
|
b7a94c91fe | ||
|
|
298503fec8 | ||
|
|
1bcfde72ee | ||
|
|
6c86dc0760 | ||
|
|
f3c15b3692 | ||
|
|
fcc04865f9 | ||
|
|
96cc747150 | ||
|
|
44398c6004 | ||
|
|
f1efe8ee61 | ||
|
|
6255d6ae3f | ||
|
|
c504c9b2a7 | ||
|
|
c3630e74ec | ||
|
|
8a3e48d856 | ||
|
|
753766893b | ||
|
|
7ee3df9ab9 | ||
|
|
0494d224be | ||
|
|
06210e79fd | ||
|
|
00e21aebd3 | ||
|
|
d3f6ad4bf6 | ||
|
|
45ba9b4899 | ||
|
|
4fb5eecdb2 | ||
|
|
3a5acea5a3 | ||
|
|
970a8b8eae | ||
|
|
fb41fa9a03 | ||
|
|
5916b8c833 | ||
|
|
b5aed8573a | ||
|
|
c8b2d8683e | ||
|
|
2b50debcd3 | ||
|
|
386f1e7466 |
42
.env
42
.env
@@ -1,36 +1,16 @@
|
||||
# 版本号
|
||||
SHOPRO_VERSION=v2.4.1
|
||||
# 正式环境接口域名 已经改为可配置的,不在此配置
|
||||
NX_BASE_URL = http://192.168.26.247:9999
|
||||
|
||||
# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development)
|
||||
SHOPRO_BASE_URL=http://api-dashboard.yudao.iocoder.cn
|
||||
|
||||
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development)
|
||||
; SHOPRO_DEV_BASE_URL=http://127.0.0.1:48080
|
||||
SHOPRO_DEV_BASE_URL=http://172.16.46.63:30081
|
||||
### SHOPRO_DEV_BASE_URL=http://10.171.1.188:48080
|
||||
### SHOPRO_DEV_BASE_URL = http://yunai.natapp1.cc
|
||||
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
||||
SHOPRO_UPLOAD_TYPE=server
|
||||
|
||||
# 后端接口前缀(一般不建议调整)
|
||||
SHOPRO_API_PATH=/admin-api
|
||||
|
||||
# 后端 websocket 接口前缀
|
||||
SHOPRO_WEBSOCKET_PATH=/infra/ws
|
||||
# 开发环境接口域名
|
||||
# NX_DEV_BASE_URL = http://192.168.26.247:9999
|
||||
NX_DEV_BASE_URL = http://192.168.26.105:888/api
|
||||
|
||||
# 开发环境运行端口
|
||||
SHOPRO_DEV_PORT=3000
|
||||
NX_DEV_PORT = 3000
|
||||
|
||||
# 接口地址前缀
|
||||
NX_API_PATH = /api/
|
||||
|
||||
NX_VERSION = v1.0
|
||||
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
|
||||
SHOPRO_STATIC_URL=http://test.yudao.iocoder.cn
|
||||
### SHOPRO_STATIC_URL = https://file.sheepjs.com
|
||||
|
||||
# 前端 H5 访问域名
|
||||
SHOPRO_H5_URL=http://127.0.0.1:3000
|
||||
|
||||
# 是否开启直播 1 开启直播 | 0 关闭直播
|
||||
SHOPRO_MPLIVE_ON=0
|
||||
|
||||
# 租户ID 默认 1
|
||||
SHOPRO_TENANT_ID=1
|
||||
NX_STATIC_URL = https://file.nx.com
|
||||
|
||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -1,11 +1,19 @@
|
||||
unpackage/*
|
||||
node_modules/*
|
||||
.idea/*
|
||||
deploy.sh
|
||||
.hbuilderx/
|
||||
.vscode/
|
||||
**/.DS_Store
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
*.keystore
|
||||
pnpm-lock.yaml
|
||||
# ---> VisualStudioCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
/unpackage/cache
|
||||
/unpackage/debug
|
||||
/unpackage/dist
|
||||
/.hbuilderx
|
||||
/unpackage/release
|
||||
/unpackage/resources
|
||||
/.guiplan_cache
|
||||
.idea
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/unpackage/*
|
||||
/node_modules/**
|
||||
/uni_modules/**
|
||||
/public/*
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
10
.prettierrc
10
.prettierrc
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"semi": true,
|
||||
"vueIndentScriptAndStyle": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"proseWrap": "never",
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
74
App.vue
74
App.vue
@@ -1,32 +1,58 @@
|
||||
<script setup>
|
||||
import { onLaunch, onShow, onError } from '@dcloudio/uni-app';
|
||||
import { ShoproInit } from './sheep';
|
||||
import { onLaunch, onShow, onError } from '@dcloudio/uni-app'
|
||||
|
||||
onLaunch(() => {
|
||||
// 隐藏原生导航栏 使用自定义底部导航
|
||||
uni.hideTabBar({
|
||||
fail: () => {},
|
||||
});
|
||||
|
||||
// 加载Shopro底层依赖
|
||||
ShoproInit();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
import { NxInit } from './nx'
|
||||
import $store from '@/nx/store'
|
||||
// #ifdef APP-PLUS
|
||||
// 获取urlSchemes参数
|
||||
const args = plus.runtime.arguments;
|
||||
if (args) {
|
||||
}
|
||||
|
||||
// 获取剪贴板
|
||||
uni.getClipboardData({
|
||||
success: (res) => {},
|
||||
});
|
||||
import * as urovo from './uni_modules/zzjc-urovo'
|
||||
// #endif
|
||||
});
|
||||
onLaunch(async () => {
|
||||
// 加载nx底层依赖
|
||||
await NxInit()
|
||||
// #ifdef APP-PLUS
|
||||
let sysInfo = uni.getSystemInfoSync()
|
||||
let brand = sysInfo.brand.toLowerCase()
|
||||
|
||||
if (brand === 'huawei') {
|
||||
plus.screen.lockOrientation('landscape')
|
||||
}
|
||||
if (brand === 'urovo') {
|
||||
urovo.scanRegister(data => {
|
||||
console.log('优博讯扫码结果:' + data)
|
||||
$store('biz').scanQRInfo = data
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
|
||||
onError(err => {
|
||||
console.log('AppOnError:', err)
|
||||
})
|
||||
|
||||
onShow(() => {})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/sheep/scss/index.scss';
|
||||
@import '@/uview-plus/index.scss';
|
||||
@import '@/nx/scss/index.scss';
|
||||
#u-a-p > div {
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'zzjc-lcd';
|
||||
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAQQAA0AAAAADNgAAAO6AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4GYACCWhEIColEhk4LIAABNgIkAyoEIAWILgdAG1sKAI7RS+dwknLKwRCOFCJ62I82+05UkhDNk0toJKgnoWEW8UYotAuZLN4ogW/0ul/7EucnQS2Yf8HMQ1ALgRPXku5MpmeuEm86VZ/nmOmibc+Hf8ET2dCiAmfjYWSBbhR9kVjhi+cndnx0eOZncg/xTozVeOFDCPDF/mEXfL++dWY6NAG9YBwUglAQDCjQkgf5og3qXj3Gg2SS/GQLBUY0iDe/gHyqjBb1fzrUZzh7nqiQF4VbQAuYZXNVT+q3M9JBNwGFAosSZTT/GYjW9Zq/zZvmNWIZTjuQUSmAbekt0dCKbeefjfoTgPmMaRoFGADMg92QI0BeAloBBSA4FBaLweIweFwrw/Acy7E8z3NiScPiOYkamVPoS1uVTKZzfL6vj5HyZFZq64bllpZPwXRsu1PzqxR5MI0r1ETvndkWy2DFNJGBHfSVPc2AMxCVjboO5ch5uR0SUkEHRAEol8eOumGDytORSX162TbqIaWFybLSpkU7nVAe8DOGLs4CXuz+/t2Hd01+UT5OX6hnfhvOL70IrDPP+jjq5A8XAs9uxsva9uOdTM/dOPNsXeD7PJFxPN4++tPhc3YzsO4FislD9YrTj9cNxlgX8xQS4pVxzepk0GLTJlbyrD+Vo6dCHY+/5XmT8gU+rSjtco+V1VNq5zWu3YVXu6q1bc7ZaoKZ+1XmTk6O8e0BAYFdi1Whfc7ZXwULU71WfZ0H5F89A50oQGBN6M9/IIYgmOvb84QHkDsgC3x0PP9mwoVk1fEJGBCLBxQIAo4BYDlXBMCEcjmCN4vC2YMWLjbQytkPtJETjnZ8CtGBSj06IbfRBdlANypb0cMrN4i91K/ow5QX+nlVBQbxNUscun2fFqRVBByHBISiHxTJCmjpDoMR5B1YfhHAUWIHj0klBD5pgkjINJwwMWERnx1EiboBmVASFPHlDxdK9RNd97OiyIxZpnljRoxapFq3SGVnZZMhQfZTdjbq+eNNbX1vt7GrPwddzOeD20bEMxcbJpt+sG23rS/eA6NLOBY+FLBl1TjNJM0QbG2Na5PaEHCObsiYJVPwjsDyYMRODqhhUpWaMW2RqpFuxJJJA+bhwDnzpSvK9yBWBKTkReh1unkLxupglVUiqy8Ycf5kTGui1Wpz1SyXhcLysktcaNUBKDr4+3WLZmVKksRgBRMZxkzelcAwwJRIMw1Hsj7DMP7a5D9IMAbMRG2a0BU4bp5Oh5u2c083WVio/b4owkSddVAY08AB5DO5cufBkxdvPnz58RcAAAA=')
|
||||
format('woff2');
|
||||
src: url('~@/static/font/zzjc-lcd.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.zzjc-lcd {
|
||||
font-family: 'zzjc-lcd' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</style>
|
||||
|
||||
21
LICENSE
21
LICENSE
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 lidongtony
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
25
README.md
25
README.md
@@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
|
||||
## 技术栈
|
||||
uni-app、ES6、Vue3、Vite、Pinia;
|
||||
|
||||
- 登录,注册
|
||||
- 全局路由守卫(路由配置)
|
||||
- Request封装(请求封装)
|
||||
- api集中管理
|
||||
- flex常用布局css,
|
||||
- utils常用工具函数
|
||||
- 配置pinia(store)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"prompt" : "template"
|
||||
}
|
||||
189
components/company-dept-dialog/company-dept-dialog.vue
Normal file
189
components/company-dept-dialog/company-dept-dialog.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<view v-if="dialogStore.show" class="company-dept-dialog">
|
||||
<view class="company-dept-dialog__mask" />
|
||||
<view class="company-dept-dialog__panel">
|
||||
<view class="company-dept-dialog__header">
|
||||
<text class="company-dept-dialog__title">{{ dialogStore.title }}</text>
|
||||
</view>
|
||||
<view class="company-dept-dialog__body">
|
||||
<!-- 公司选择 -->
|
||||
<view class="company-dept-dialog__field">
|
||||
<text class="company-dept-dialog__label">公司</text>
|
||||
<uni-data-select
|
||||
v-model="selectedCompanyModel"
|
||||
:localdata="companyOptionsForSelect"
|
||||
:clear="false"
|
||||
@change="onCompanyChangeUni"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 部门选择 -->
|
||||
<view class="company-dept-dialog__field">
|
||||
<text class="company-dept-dialog__label">部门</text>
|
||||
<uni-data-select
|
||||
v-model="selectedDeptModel"
|
||||
:localdata="deptOptionsForSelect"
|
||||
:clear="false"
|
||||
@change="onDeptChangeUni"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="company-dept-dialog__footer">
|
||||
<u-button v-if="showCancel" style="width: 40%" @tap="handleCancel">取消</u-button>
|
||||
<u-button style="width: 40%" type="primary" :disabled="!isConfirmEnabled" @tap="handleConfirm"> 确定 </u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import nx from '@/nx'
|
||||
|
||||
const dialogStore = nx.$store('company-dept')
|
||||
const { companyList, selectedCompanyId, selectedDeptId, showCancel } = storeToRefs(dialogStore)
|
||||
|
||||
const deptOptions = computed(() => dialogStore.getDeptsByCompanyId(selectedCompanyModel.value))
|
||||
|
||||
// 用于 uni-data-select 的格式:必须包含 value 和 text 字段
|
||||
const companyOptionsForSelect = computed(() =>
|
||||
companyList.value.map(item => ({
|
||||
value: item.companyId,
|
||||
text: item.companyName
|
||||
}))
|
||||
)
|
||||
|
||||
const deptOptionsForSelect = computed(() =>
|
||||
deptOptions.value.map(item => ({
|
||||
value: item.deptId,
|
||||
text: item.deptName
|
||||
}))
|
||||
)
|
||||
|
||||
// 双向绑定模型(uni-data-select 使用 value 绑定)
|
||||
const selectedCompanyModel = ref(selectedCompanyId.value)
|
||||
const selectedDeptModel = ref(selectedDeptId.value)
|
||||
|
||||
// 监听 store 变化,同步到 model(应对外部触发更新)
|
||||
watch(
|
||||
() => selectedCompanyId.value,
|
||||
newVal => {
|
||||
if (newVal !== selectedCompanyModel.value) {
|
||||
selectedCompanyModel.value = newVal
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => selectedDeptId.value,
|
||||
newVal => {
|
||||
if (newVal !== selectedDeptModel.value) {
|
||||
selectedDeptModel.value = newVal
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// uni-data-select 的 change 回调
|
||||
const onCompanyChangeUni = e => {
|
||||
const companyId = e
|
||||
if (companyId !== undefined && companyId !== null) {
|
||||
dialogStore.setSelectedCompany(companyId)
|
||||
// 重置部门选择
|
||||
dialogStore.setSelectedDept('')
|
||||
selectedDeptModel.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const onDeptChangeUni = e => {
|
||||
const deptId = e
|
||||
if (deptId !== undefined && deptId !== null) {
|
||||
dialogStore.setSelectedDept(deptId)
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮状态
|
||||
const isConfirmEnabled = computed(() => !!dialogStore.selectedCompanyId && !!dialogStore.selectedDeptId)
|
||||
|
||||
// 操作
|
||||
const handleCancel = () => {
|
||||
dialogStore.cancel()
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!isConfirmEnabled.value) return
|
||||
dialogStore.confirm()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.company-dept-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 15000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
&__panel {
|
||||
position: relative;
|
||||
width: 45vw;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px 20px 16px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
&__panel {
|
||||
width: 70vw;
|
||||
}
|
||||
}
|
||||
&__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f1f1f;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
&__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
196
components/da-tree/changelog.md
Normal file
196
components/da-tree/changelog.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 1.4.2
|
||||
|
||||
新增
|
||||
|
||||
1. 新增`filterValue`属性,支持通过此关键词来搜索并筛选树结构的内容
|
||||
|
||||
# 1.4.1
|
||||
|
||||
修复
|
||||
|
||||
1. 修复单选 onlyRadioLeaf 时末级节点无法选中的 bug
|
||||
|
||||
# 1.4.0
|
||||
|
||||
版本调整
|
||||
|
||||
建议更新,但需要注意,异步数据的时候,后台需返回 leaf 字段来判断是否末项数据
|
||||
|
||||
1. **调整数据项格式,新增 `leaf` 字段,来判断是否为末节点**
|
||||
2. **调整数据项格式,新增 `sort` 字段,来排序节点位置**
|
||||
3. **注意:异步加载数据,当为末项的时候,需要服务端数据返回 `leaf` 字段**
|
||||
4. 新增 `alwaysFirstLoad` ,即异步数据总会在第一次展开节点时,拉取一次后台数据,来比对是否一致
|
||||
5. 拆分 `field` 属性,**注意: 1.5.0 版本后将移除 `field` 属性**
|
||||
6. 新增 `labelField` 同 `field.label`,指定节点对象中某个属性为**标签**字段,默认`label`
|
||||
7. 新增 `valueField` 同 `field.key`,指定节点对象中某个属性为**值**字段,默认`value`
|
||||
8. 新增 `childrenField` 同 `field.children`,指定节点对象中某个属性为**子树节点**字段,默认`children`
|
||||
9. 新增 `disabledField` 同 `field.disabled`,指定节点对象中某个属性为**禁用**字段,默认`disabled`
|
||||
10. 新增 `appendField` 同 `field.append`,指定节点对象中某个属性为**副标签**字段,默认`append`
|
||||
11. 新增 `leafField` 同 `field.label`,指定节点对象中某个属性为**末级节点**字段,默认`leaf`
|
||||
12. 新增 `sortField` 同 `field.label`,指定节点对象中某个属性为**排序**字段,默认`sort`
|
||||
13. 新增 `isLeafFn` ,用来自定义控制数据项的末项
|
||||
14. 更多的项目示例
|
||||
15. 支持单选取消选中
|
||||
16. 修复节点展开时可能存在的 bug
|
||||
17. 修复节点选择可能存在的 bug
|
||||
18. 调整为子节点默认继承父节点禁用属性
|
||||
19. `setExpandedKeys` 添加参数一为 `all` 即可支持一键展开/收起全部节点
|
||||
20. 其它更多优化
|
||||
|
||||
# 1.3.4
|
||||
|
||||
优化
|
||||
|
||||
1. 优化图标字体命名
|
||||
|
||||
# 1.3.3
|
||||
|
||||
优化
|
||||
|
||||
1. 新增方法调用
|
||||
> - 新增`getUncheckedKeys`,返回未选的 key
|
||||
> - 新增`getUncheckedNodes`,返回未选的节点
|
||||
> - 新增`getUnexpandedKeys`,返回未展开的 key
|
||||
> - 新增`getUnexpandedNodes`,返回未展开的节点
|
||||
2. 优化示例项目
|
||||
|
||||
# 1.3.2
|
||||
|
||||
修复
|
||||
|
||||
1. 修复在 APP 真机环境中的报错
|
||||
|
||||
# 1.3.1
|
||||
|
||||
修复
|
||||
|
||||
1. 修复方法`setExpandedKeys`没联动展开上级父子节点
|
||||
|
||||
# 1.3.0
|
||||
|
||||
优化
|
||||
|
||||
1. `field`新增字段 `append` 用于在标签后面显示小提示
|
||||
2. 新增支持点击标签也能选中节点
|
||||
3. 方法`setExpandedKeys`支持加载动态数据
|
||||
4. 修复父节点禁用,则不能展开及图标展开显示
|
||||
5. 修复动态加载数据时,末级节点的 `children` 为 `null` 时仍显示展开图标
|
||||
|
||||
# 1.2.6
|
||||
|
||||
新增
|
||||
|
||||
1. 新增支持主题换色
|
||||
2. 支持单选的`onlyRadioLeaf`为`true`时可点父节点展开/收起
|
||||
3. 优化`expandChecked`调整为不展开无子节点的节点
|
||||
|
||||
# 1.2.5
|
||||
|
||||
新增
|
||||
|
||||
1. 新增 `expandChecked`,控制选择时是否展开当前已选的所有下级节点
|
||||
|
||||
# 1.2.4
|
||||
|
||||
修复
|
||||
|
||||
1. 修复动态数据展开状态异常问题
|
||||
|
||||
# 1.2.3
|
||||
|
||||
新增
|
||||
|
||||
1. 新增 `checkedDisabled`,是否渲染禁用值
|
||||
2. 新增 `packDisabledkey`,是否返回已禁用并选中的 key
|
||||
3. 修复选择父级时,子级已禁用但仍被选中的问题
|
||||
|
||||
# 1.2.2
|
||||
|
||||
优化
|
||||
|
||||
1. 调整动态数据载入处理方式
|
||||
2. 修复节点数据因动态数据引起的状态异常
|
||||
3. 修复初始节点数据默认选中
|
||||
|
||||
# 1.2.1
|
||||
|
||||
修复
|
||||
|
||||
1. 修复切换`选中状态`被重复选中问题
|
||||
2. 修复动态数据引起的重复选择问题
|
||||
|
||||
# 1.2.0
|
||||
|
||||
新增
|
||||
|
||||
1. 新增方法调用
|
||||
> - 新增`setCheckedKeys`,方法设置指定 key 的节点选中状态
|
||||
> - 新增`setExpandedKeys`,方法设置指定 key 的节点展开状态
|
||||
2. 修复小程序重复插槽一直刷报错问题
|
||||
3. 优化展开时,会展开子级所以下级节点
|
||||
|
||||
# 1.1.1
|
||||
|
||||
新增
|
||||
|
||||
1. 新增`data`的`disabled`,支持节点禁用状态
|
||||
2. 新增`field`的`disabled`,可自定`disabled`字段值
|
||||
|
||||
# 1.1.0
|
||||
|
||||
新增
|
||||
|
||||
1. 新增`loadMode`、`loadApi`,支持展开时加载异步数据
|
||||
2. 新增方法调用
|
||||
> - 新增`getCheckedKeys`,方法返回已选的 key
|
||||
> - 新增`getHalfCheckedKeys`,方法返回半选的 key
|
||||
> - 新增`getExpandedKeys`,方法返回已展开的 key
|
||||
> - 新增`getCheckedNodes`,方法返回已选的节点
|
||||
> - 新增`getHalfCheckedNodes`,方法返回半选的节点
|
||||
> - 新增`getExpandedNodes`,方法返回已展开的节点
|
||||
3. 对代码进行重构,更易于后期拓展
|
||||
4. 此次更新后,页面多个的 DaTee 组件间的数据不再关联
|
||||
|
||||
# 1.0.6
|
||||
|
||||
新增
|
||||
|
||||
1. 新增`checkStrictly`,多选模式下选中时是否父子不关联
|
||||
|
||||
# 1.0.5
|
||||
|
||||
修复
|
||||
|
||||
1. 修复多选时已选数据重复问题
|
||||
|
||||
# 1.0.4
|
||||
|
||||
修复
|
||||
|
||||
1. 修复 `change` 事件回调数据的问题
|
||||
|
||||
# 1.0.3
|
||||
|
||||
优化
|
||||
|
||||
1. 优化文档及示例说明
|
||||
|
||||
# 1.0.2
|
||||
|
||||
新增
|
||||
|
||||
1. 新增 `onlyRadioLeaf` ,单选时只允许选中末级
|
||||
2. 优化默认展开及默认选择的展开问题
|
||||
|
||||
# 1.0.1
|
||||
|
||||
新增
|
||||
|
||||
1. 支持展开/收起回调事件`@expand`
|
||||
|
||||
# 1.0.0
|
||||
|
||||
初始版本 1.0.0,基于 Vue3 进行开发,支持单选、多选,兼容各大平台
|
||||
|
||||
1. 支持单选
|
||||
2. 支持多选
|
||||
1181
components/da-tree/index.vue
Normal file
1181
components/da-tree/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
197
components/da-tree/props.ts
Normal file
197
components/da-tree/props.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
export default {
|
||||
/**
|
||||
* 树的数据
|
||||
*/
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 主题色
|
||||
*/
|
||||
themeColor: {
|
||||
type: String,
|
||||
default: '#007aff'
|
||||
},
|
||||
/**
|
||||
* 是否开启多选,默认单选
|
||||
*/
|
||||
showCheckbox: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 默认选中的节点,注意单选时为单个key,多选时为key的数组
|
||||
*/
|
||||
defaultCheckedKeys: {
|
||||
type: [Array, String, Number],
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 是否默认展开全部
|
||||
*/
|
||||
defaultExpandAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 默认展开的节点
|
||||
*/
|
||||
defaultExpandedKeys: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 筛选关键词
|
||||
*/
|
||||
filterValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
/**
|
||||
* 是否自动展开到选中的节点,默认不展开
|
||||
*/
|
||||
expandChecked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* (旧)字段对应内容,默认为 {label: 'label',key: 'key', children: 'children', disabled: 'disabled', append: 'append'}
|
||||
* 注意:1.5.0版本后不再兼容
|
||||
*/
|
||||
field: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 标签字段(新,拆分了)
|
||||
*/
|
||||
labelField: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
/**
|
||||
* 值字段(新,拆分了)
|
||||
*/
|
||||
valueField: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
/**
|
||||
* 下级字段(新,拆分了)
|
||||
*/
|
||||
childrenField: {
|
||||
type: String,
|
||||
default: 'children'
|
||||
},
|
||||
/**
|
||||
* 禁用字段(新,拆分了)
|
||||
*/
|
||||
disabledField: {
|
||||
type: String,
|
||||
default: 'disabled'
|
||||
},
|
||||
/**
|
||||
* 末级节点字段(新,拆分了)
|
||||
*/
|
||||
leafField: {
|
||||
type: String,
|
||||
default: 'leaf'
|
||||
},
|
||||
/**
|
||||
* 副标签字段(新,拆分了)
|
||||
*/
|
||||
appendField: {
|
||||
type: String,
|
||||
default: 'append'
|
||||
},
|
||||
/**
|
||||
* 排序字段(新,拆分了)
|
||||
*/
|
||||
sortField: {
|
||||
type: String,
|
||||
default: 'sort'
|
||||
},
|
||||
/**
|
||||
* Api数据返回后的结果路径,支持嵌套如`data.list`
|
||||
*/
|
||||
resultField: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isLeafFn: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 是否显示单选图标,默认显示
|
||||
*/
|
||||
showRadioIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 单选时只允许选中末级,默认可随意选中
|
||||
*/
|
||||
onlyRadioLeaf: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 多选时,是否执行父子不关联的任意勾选,默认父子关联
|
||||
*/
|
||||
checkStrictly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 为 true 时,空的 children 数组会显示展开图标
|
||||
*/
|
||||
loadMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 异步加载接口
|
||||
*/
|
||||
loadApi: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 是否总在首次的时候加载一下内容,来比对是否一致
|
||||
*/
|
||||
alwaysFirstLoad: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否渲染(操作)禁用值
|
||||
*/
|
||||
checkedDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* 是否返回已禁用的但已选中的key
|
||||
*/
|
||||
packDisabledkey: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* 选择框的位置,可选 left/right
|
||||
*/
|
||||
checkboxPlacement: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
/**
|
||||
* 子项缩进距离,默认40,单位rpx
|
||||
*/
|
||||
indent: {
|
||||
type: Number,
|
||||
default: 20
|
||||
}
|
||||
}
|
||||
310
components/da-tree/readme.md
Normal file
310
components/da-tree/readme.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# da-tree
|
||||
|
||||
一个基于 Vue3 的 tree(树)组件,同时支持主题换色,可能是最适合你的 tree(树)组件
|
||||
|
||||
组件一直在更新,遇到问题可在下方讨论。
|
||||
|
||||
`同时更新 Vue2 版本,在此查看 ===>` **[Vue2 版](https://ext.dcloud.net.cn/plugin?id=12692)**
|
||||
|
||||
### 关于使用
|
||||
|
||||
可在右侧的`使用 HBuilderX 导入插件`或`下载示例项目ZIP`,方便快速上手。
|
||||
|
||||
可通过下方的示例及文档说明,进一步了解使用组件相关细节参数。
|
||||
|
||||
插件地址:https://ext.dcloud.net.cn/plugin?id=12384
|
||||
|
||||
### 组件示例
|
||||
|
||||
```jsx
|
||||
<template>
|
||||
<view>多选</view>
|
||||
<view><button @click="doCheckedTree(['2'],true)">全选</button></view>
|
||||
<view><button @click="doCheckedTree(['2'],false)">取消全选</button></view>
|
||||
<view><button @click="doCheckedTree(['211','222'],true)">选中指定节点</button></view>
|
||||
<view><button @click="doCheckedTree(['211','222'],false)">取消选中指定节点</button></view>
|
||||
<view><button @click="doExpandTree('all',true)">展开全部节点</button></view>
|
||||
<view><button @click="doExpandTree('all',false)">收起全部节点</button></view>
|
||||
<view><button @click="doExpandTree(['22','23'],true)">展开节点</button></view>
|
||||
<view><button @click="doExpandTree(['22','23'],false)">收起节点</button></view>
|
||||
<DaTree
|
||||
ref="DaTreeRef"
|
||||
:data="roomTreeData"
|
||||
labelField="name"
|
||||
valueField="id"
|
||||
defaultExpandAll
|
||||
showCheckbox
|
||||
:defaultCheckedKeys="defaultCheckedKeysValue"
|
||||
@change="handleTreeChange"
|
||||
@expand="handleExpandChange"></DaTree>
|
||||
<view>单选</view>
|
||||
<DaTree
|
||||
:data="roomTreeData"
|
||||
labelField="name"
|
||||
valueField="id"
|
||||
defaultExpandAll
|
||||
:defaultCheckedKeys="defaultCheckedKeysValue2"
|
||||
@change="handleTreeChange"
|
||||
@expand="handleExpandChange"></DaTree>
|
||||
<view>默认展开指定节点</view>
|
||||
<DaTree
|
||||
:data="roomTreeData"
|
||||
labelField="name"
|
||||
valueField="id"
|
||||
showCheckbox
|
||||
:defaultExpandedKeys="defaultExpandKeysValue3"
|
||||
@change="handleTreeChange"
|
||||
@expand="handleExpandChange"></DaTree>
|
||||
<view>异步加载数据</view>
|
||||
<DaTree
|
||||
:data="roomTreeData"
|
||||
labelField="name"
|
||||
valueField="id"
|
||||
showCheckbox
|
||||
loadMode
|
||||
:loadApi="GetApiData"
|
||||
defaultExpandAll
|
||||
@change="handleTreeChange"
|
||||
@expand="handleExpandChange"></DaTree>
|
||||
</template>
|
||||
```
|
||||
|
||||
```js
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
/**
|
||||
* 模拟创建一个接口数据
|
||||
*/
|
||||
function GetApiData(currentNode) {
|
||||
const { key } = currentNode
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
// 模拟返回空数据
|
||||
if (key.indexOf('-') > -1) {
|
||||
return resolve(null)
|
||||
// return resolve([])
|
||||
}
|
||||
|
||||
return resolve([
|
||||
{
|
||||
id: `${key}-1`,
|
||||
name: `行政部X${key}-1`,
|
||||
},
|
||||
{
|
||||
id: `${key}-2`,
|
||||
name: `财务部X${key}-2`,
|
||||
append: '定义了末项数据',
|
||||
leaf: true,
|
||||
},
|
||||
{
|
||||
id: `${key}-3`,
|
||||
name: `资源部X${key}-3`,
|
||||
},
|
||||
{
|
||||
id: `${key}-4`,
|
||||
name: `资源部X${key}-3`,
|
||||
append: '被禁用,无展开图标',
|
||||
disabled: true,
|
||||
},
|
||||
])
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
import DaTree from '@/components/da-tree/index.vue'
|
||||
export default defineComponent({
|
||||
components: { DaTree },
|
||||
setup() {
|
||||
const DaTreeRef = ref()
|
||||
// key的类型必须对应树数据key的类型
|
||||
const defaultCheckedKeysValue = ref(['211', '222'])
|
||||
const defaultCheckedKeysValue2 = ref('222')
|
||||
const defaultExpandKeysValue3 = ref(['212', '231'])
|
||||
const roomTreeData = ref([
|
||||
{
|
||||
id: '2',
|
||||
name: '行政中心',
|
||||
children: [
|
||||
{
|
||||
id: '21',
|
||||
name: '行政部',
|
||||
children: [
|
||||
{
|
||||
id: '211',
|
||||
name: '行政一部',
|
||||
children: null,
|
||||
},
|
||||
{
|
||||
id: '212',
|
||||
name: '行政二部',
|
||||
children: [],
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
name: '财务部',
|
||||
children: [
|
||||
{
|
||||
id: '221',
|
||||
name: '财务一部',
|
||||
children: [],
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: '222',
|
||||
name: '财务二部',
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
name: '人力资源部',
|
||||
children: [
|
||||
{
|
||||
id: '231',
|
||||
name: '人力一部',
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: '232',
|
||||
name: '人力二部',
|
||||
append: '更多示例,请下载示例项目查看',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
function doExpandTree(keys, expand) {
|
||||
DaTreeRef.value?.setExpandedKeys(keys, expand)
|
||||
|
||||
const gek = DaTreeRef.value?.getExpandedKeys()
|
||||
console.log('当前已展开的KEY ==>', gek)
|
||||
}
|
||||
function doCheckedTree(keys, checked) {
|
||||
DaTreeRef.value?.setCheckedKeys(keys, checked)
|
||||
|
||||
const gek = DaTreeRef.value?.getCheckedKeys()
|
||||
console.log('当前已选中的KEY ==>', gek)
|
||||
}
|
||||
function handleTreeChange(allSelectedKeys, currentItem) {
|
||||
console.log('handleTreeChange ==>', allSelectedKeys, currentItem)
|
||||
}
|
||||
function handleExpandChange(expand, currentItem) {
|
||||
console.log('handleExpandChange ==>', expand, currentItem)
|
||||
}
|
||||
return {
|
||||
DaTreeRef,
|
||||
roomTreeData,
|
||||
defaultCheckedKeysValue,
|
||||
defaultCheckedKeysValue2,
|
||||
defaultExpandKeysValue3,
|
||||
handleTreeChange,
|
||||
handleExpandChange,
|
||||
GetApiData,
|
||||
doExpandTree,
|
||||
doCheckedTree,
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
** 更多示例请下载/导入示例项目 ZIP 查看 **
|
||||
|
||||
### 组件参数
|
||||
|
||||
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| :------------------ | :------------------------------ | :--------- | :--- | :--------------------------------------------------------------------------- |
|
||||
| data | `Array` | - | 是 | 树的数据 |
|
||||
| themeColor | `String` | `#007aff` | 否 | 主题色,十六进制 |
|
||||
| defaultCheckedKeys | `Array` \| `Number` \| `String` | - | 否 | 默认选中的节点,单选为单个 key,多选为 key 的数组 |
|
||||
| showCheckbox | `Boolean` | `false` | 否 | 是否开启多选,默认单选 |
|
||||
| checkStrictly | `Boolean` | `false` | 否 | 多选时,是否执行父子不关联的任意勾选,默认父子关联 |
|
||||
| showRadioIcon | `Boolean` | `true` | 否 | 是否显示单选图标,默认显示 |
|
||||
| onlyRadioLeaf | `Boolean` | `true` | 否 | 单选时只允许选中末级,默认可随意选中 |
|
||||
| defaultExpandAll | `Boolean` | `false` | 否 | 是否默认展开全部 |
|
||||
| defaultExpandedKeys | `Array` | - | 否 | 默认展开的节点 |
|
||||
| indent | `Number` | `40` | 否 | 子项缩进距离,单位 rpx |
|
||||
| checkboxPlacement | `String` | `left` | 否 | 选择框的位置,可选 left/right |
|
||||
| loadMode | `Boolean` | `false` | 否 | 为 true 时,空的 children 数组会显示展开图标 |
|
||||
| loadApi | `Function` | - | 否 | 选择框的位置,可选 left/right |
|
||||
| checkedDisabled | `Boolean` | `false` | 否 | 是否渲染禁用值,默认不渲染 |
|
||||
| packDisabledkey | `Boolean` | `true` | 否 | 是否返回已禁用的但已选中的 key,默认返回禁用已选值 |
|
||||
| expandChecked | `Boolean` | `false` | 否 | 是否自动展开到选中的节点,默认不展开 |
|
||||
| alwaysFirstLoad | `Boolean` | `false` | 否 | 是否总在首次的时候加载一下内容,默认不加载,否则只有展开末级节点才会加载数据 |
|
||||
| isLeafFn | `Function` | - | 否 | 自定义函数返回来控制数据项的末项 |
|
||||
| field | `Object` | - | 否 | 字段对应内容,格式参考下方(1.5.0 后移除,请用单独的字段匹配) |
|
||||
| labelField | `String` | `label` | 否 | 指定节点对象中某个属性为标签字段,默认`label` |
|
||||
| valueField | `String` | `value` | 否 | 指定节点对象中某个属性为值字段,默认`value` |
|
||||
| childrenField | `String` | `children` | 否 | 指定节点对象中某个属性为子树节点字段,默认`children` |
|
||||
| disabledField | `String` | `disabled` | 否 | 指定节点对象中某个属性为禁用字段,默认`disabled` |
|
||||
| appendField | `String` | `append` | 否 | 指定节点对象中某个属性为副标签字段,默认`append` |
|
||||
| leafField | `String` | `leaf` | 否 | 指定节点对象中某个属性为末级节点字段,默认`leaf` |
|
||||
| sortField | `String` | `sort` | 否 | 指定节点对象中某个属性为排序字段,默认`sort` |
|
||||
| filterValue | `String` | - | 否 | 搜索筛选的关键词,通过输入关键词筛选内容 |
|
||||
|
||||
**field 格式(1.5.0 后移除,请用单独的字段匹配)**
|
||||
|
||||
```js
|
||||
{
|
||||
label: 'label',
|
||||
key: 'key',
|
||||
children: 'children',
|
||||
disabled: 'disabled',
|
||||
append: 'append'
|
||||
}
|
||||
```
|
||||
|
||||
### 组件事件
|
||||
|
||||
| 事件名称 | 回调参数 | 说明 |
|
||||
| :------- | :-------------------------------------- | :-------------- |
|
||||
| change | `(allCheckedKeys, currentItem) => void` | 选中时回调 |
|
||||
| expand | `(expandState, currentItem) => void` | 展开/收起时回调 |
|
||||
|
||||
### 组件方法
|
||||
|
||||
| 方法名称 | 参数 | 说明 |
|
||||
| :------------------ | :--------------- | :------------------------------------------------------------------------------------------------ |
|
||||
| setCheckedKeys | `(keys,checked)` | 设置指定 key 的节点选中/取消选中的状态。注: keys 单选时为 key,多选时为 key 的数组 |
|
||||
| setExpandedKeys | `(keys,expand)` | 设置指定 key 的节点展开/收起的状态,当 keys 为 all 时即代表展开/收起全部。注:keys 为数组或 `all` |
|
||||
| getCheckedKeys | - | 返回已选的 key |
|
||||
| getHalfCheckedKeys | - | 返回半选的 key |
|
||||
| getUncheckedKeys | - | 返回未选的 key |
|
||||
| getCheckedNodes | - | 返回已选的节点 |
|
||||
| getUncheckedNodes | - | 返回未选的节点 |
|
||||
| getHalfCheckedNodes | - | 返回半选的节点 |
|
||||
| getExpandedKeys | - | 返回已展开的 key |
|
||||
| getUnexpandedKeys | - | 返回未展开的 key |
|
||||
| getExpandedNodes | - | 返回已展开的节点 |
|
||||
| getUnexpandedNodes | - | 返回未展开的节点 |
|
||||
|
||||
### 组件版本
|
||||
|
||||
v1.4.2
|
||||
|
||||
### 差异化
|
||||
|
||||
已通过测试
|
||||
|
||||
> - H5 页面
|
||||
> - 微信小程序
|
||||
> - 支付宝、钉钉小程序
|
||||
> - 字节跳动、抖音、今日头条小程序
|
||||
> - 百度小程序
|
||||
> - 飞书小程序
|
||||
> - QQ 小程序
|
||||
> - 京东小程序
|
||||
|
||||
未测试
|
||||
|
||||
> - 快手小程序由于非企业用户暂无演示
|
||||
> - 快应用、360 小程序因 Vue3 支持的原因暂无演示
|
||||
|
||||
### 开发组
|
||||
|
||||
[@CRLANG](https://crlang.com)
|
||||
150
components/da-tree/utils.ts
Normal file
150
components/da-tree/utils.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/** 未选 */
|
||||
export const unCheckedStatus = 0
|
||||
/** 半选 */
|
||||
export const halfCheckedStatus = 1
|
||||
/** 选中 */
|
||||
export const isCheckedStatus = 2
|
||||
|
||||
/**
|
||||
* 深拷贝内容
|
||||
* @param originData 拷贝对象
|
||||
* @author crlang(https://crlang.com)
|
||||
*/
|
||||
export function deepClone(originData) {
|
||||
const type = Object.prototype.toString.call(originData)
|
||||
let data
|
||||
if (type === '[object Array]') {
|
||||
data = []
|
||||
for (let i = 0; i < originData.length; i++) {
|
||||
data.push(deepClone(originData[i]))
|
||||
}
|
||||
} else if (type === '[object Object]') {
|
||||
data = {}
|
||||
for (const prop in originData) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (originData.hasOwnProperty(prop)) { // 非继承属性
|
||||
data[prop] = deepClone(originData[prop])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data = originData
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指定的节点
|
||||
* @param type
|
||||
* @param value
|
||||
* @author crlang(https://crlang.com)
|
||||
*/
|
||||
export function getAllNodes(list, type, value, packDisabledkey = true) {
|
||||
if (!list || list.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const res = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
if (item[type] === value) {
|
||||
if ((packDisabledkey && item.disabled) || !item.disabled) {
|
||||
res.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指定的key值
|
||||
* @param type
|
||||
* @param value
|
||||
* @author crlang(https://crlang.com)
|
||||
*/
|
||||
export function getAllNodeKeys(list, type, value, packDisabledkey = true) {
|
||||
if (!list || list.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const res = []
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
if (item[type] === value) {
|
||||
if ((packDisabledkey && item.disabled) || !item.disabled) {
|
||||
res.push(item.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.length ? res : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误输出
|
||||
*
|
||||
* @param msg
|
||||
*/
|
||||
export function logError(msg, ...args) {
|
||||
console.error(`DaTree: ${msg}`, ...args)
|
||||
}
|
||||
|
||||
const toString = Object.prototype.toString
|
||||
|
||||
export function is(val, type) {
|
||||
return toString.call(val) === `[object ${type}]`
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否对象(Object)
|
||||
* @param val
|
||||
|
||||
*/
|
||||
export function isObject(val) {
|
||||
return val !== null && is(val, 'Object')
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否数字(Number)
|
||||
* @param val
|
||||
|
||||
*/
|
||||
export function isNumber(val) {
|
||||
return is(val, 'Number')
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否字符串(String)
|
||||
* @param val
|
||||
|
||||
*/
|
||||
export function isString(val) {
|
||||
return is(val, 'String')
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否函数方法(Function)
|
||||
* @param val
|
||||
|
||||
*/
|
||||
export function isFunction(val) {
|
||||
return typeof val === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否布尔(Boolean)
|
||||
* @param val
|
||||
|
||||
*/
|
||||
export function isBoolean(val) {
|
||||
return is(val, 'Boolean')
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否数组(Array)
|
||||
* @param val
|
||||
|
||||
*/
|
||||
export function isArray(val) {
|
||||
return val && Array.isArray(val)
|
||||
}
|
||||
64
components/my-tabBar/my-tabBar.vue
Normal file
64
components/my-tabBar/my-tabBar.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
//my-tabbar文件
|
||||
<template>
|
||||
<view>
|
||||
<u-tabbar
|
||||
:value="currentTab"
|
||||
:fixed="true"
|
||||
:border="false"
|
||||
activeColor="#0055a2"
|
||||
:placeholder="false"
|
||||
@change="changeTabIndex"
|
||||
>
|
||||
<u-tabbar-item
|
||||
v-for="item in switchTabs"
|
||||
:key="item.name"
|
||||
:text="item.text"
|
||||
:icon="item.iconName"
|
||||
></u-tabbar-item>
|
||||
</u-tabbar>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue'
|
||||
|
||||
let props = defineProps({
|
||||
currentTab: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
const switchTabs = reactive([
|
||||
{
|
||||
pagePath: '/pages/index/index',
|
||||
iconName: 'home',
|
||||
text: '首页',
|
||||
name: 'home'
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/me/index',
|
||||
iconName: 'account',
|
||||
text: '我的',
|
||||
name: 'account'
|
||||
}
|
||||
])
|
||||
|
||||
function changeTabIndex(e) {
|
||||
let pagePath = switchTabs[e].pagePath
|
||||
uni.switchTab({
|
||||
url: pagePath
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .u-tabbar__content {
|
||||
background-color: #f2f2f2;
|
||||
padding: 10rpx 0;
|
||||
.u-icon__icon {
|
||||
font-size: 54rpx !important;
|
||||
}
|
||||
.u-tabbar-item__text {
|
||||
font-size: 38rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
10
components/n-layout/n-layout.vue
Normal file
10
components/n-layout/n-layout.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<view>
|
||||
<slot />
|
||||
<company-dept-dialog />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
67
components/n-scanTemp/n-scanTemp.vue
Normal file
67
components/n-scanTemp/n-scanTemp.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<view class="content">
|
||||
<view class="x-c">
|
||||
<image style="width: 80px; height: 80px" :src="`/static/images/menus/${icon}.png`"></image>
|
||||
</view>
|
||||
<view class="pt50 pb50 fs30">
|
||||
<span>{{ title }}</span>
|
||||
</view>
|
||||
<view class="x-c">
|
||||
<u-icon size="150" color="#0055A2" name="scan"></u-icon>
|
||||
</view>
|
||||
<!-- #ifdef H5 -->
|
||||
<up-search
|
||||
shape="square"
|
||||
placeholder="请输入设备id"
|
||||
actionText="查询"
|
||||
:clearabled="false"
|
||||
:showAction="true"
|
||||
@custom="handleInputSearch"
|
||||
></up-search>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
let props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '请扫描设备'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'useRecord'
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['deviceId'])
|
||||
const customIconsMap = new Map([
|
||||
['useRecord', ''],
|
||||
['dailyCheck', ''],
|
||||
['maintain', ''],
|
||||
['periodCheck', ''],
|
||||
['calibration', ''],
|
||||
['accept', ''],
|
||||
['transfer', '']
|
||||
])
|
||||
function iconMap(key) {
|
||||
return customIconsMap.get(key)
|
||||
}
|
||||
|
||||
function handleInputSearch(e) {
|
||||
emits('deviceId', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('@/static/iconfont/iconfont.ttf');
|
||||
}
|
||||
.content {
|
||||
padding-top: 100px;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
136
components/n-upload/n-upload.vue
Normal file
136
components/n-upload/n-upload.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<view>
|
||||
<up-upload
|
||||
v-if="isUpdate"
|
||||
:file-list="fileList"
|
||||
:accept="accept"
|
||||
multiple
|
||||
:size-type="['original', 'compressed']"
|
||||
:max-count="maxCount"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:preview-full-image="true"
|
||||
:disabled="disabled"
|
||||
@delete="deletePic"
|
||||
@after-read="afterRead"
|
||||
>
|
||||
</up-upload>
|
||||
<u-album v-else multiple-size="85" single-size="85" :urls="imgs" row-count="4" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { getBaseUrl, getImgBaseUrl } from '@/defaultBaseUrl'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Array, String], default: '' },
|
||||
isUpdate: { type: Boolean, default: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
accept: { type: String, default: 'image' },
|
||||
width: { type: String, default: '80px' },
|
||||
height: { type: String, default: '80px' },
|
||||
maxCount: { type: Number, default: 20 },
|
||||
returnAsString: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const fileList = ref([])
|
||||
const imgs = ref([])
|
||||
const uploadConfig = reactive({
|
||||
baseURL: getBaseUrl(),
|
||||
imgBaseURL: getImgBaseUrl(),
|
||||
header: { 'X-Access-Token': uni.getStorageSync('token') }
|
||||
})
|
||||
|
||||
// 同步外部值到组件
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newVal => {
|
||||
if (!newVal) return (fileList.value = [])
|
||||
const urls = Array.isArray(newVal) ? newVal : newVal.split(',')
|
||||
imgs.value = urls
|
||||
fileList.value = urls.map(url => ({
|
||||
url: uploadConfig.imgBaseURL + url,
|
||||
status: 'success',
|
||||
message: ''
|
||||
}))
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const deletePic = event => {
|
||||
fileList.value.splice(event.index, 1)
|
||||
updateParent(fileList.value)
|
||||
}
|
||||
|
||||
const afterRead = async event => {
|
||||
const files = [].concat(event.file)
|
||||
const initialLength = fileList.value.length
|
||||
|
||||
// 添加上传状态
|
||||
fileList.value.push(
|
||||
...files.map(file => ({
|
||||
...file,
|
||||
status: 'uploading',
|
||||
message: '上传中'
|
||||
}))
|
||||
)
|
||||
|
||||
try {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const result = await uploadFile(files[i])
|
||||
const index = initialLength + i
|
||||
fileList.value[index] = {
|
||||
...files[i],
|
||||
status: 'success',
|
||||
url: uploadConfig.imgBaseURL + result.message
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error)
|
||||
// 回滚失败的上传
|
||||
fileList.value.splice(initialLength, files.length)
|
||||
} finally {
|
||||
updateParent(fileList.value)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadFile = file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${uploadConfig.baseURL}/sys/common/upload`,
|
||||
filePath: file.url,
|
||||
name: 'file',
|
||||
header: uploadConfig.header,
|
||||
formData: { biz: 'lims-device' },
|
||||
success: res => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(JSON.parse(res.data))
|
||||
} else {
|
||||
reject(new Error(res.data))
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const formatValue = fileList => {
|
||||
// 去掉 imgBaseURL 后的部分
|
||||
const baseUrlLength = uploadConfig.imgBaseURL.length
|
||||
return props.returnAsString
|
||||
? fileList.map(item => item.url.slice(baseUrlLength)).join(',')
|
||||
: fileList.map(item => item.url.slice(baseUrlLength))
|
||||
}
|
||||
const updateParent = fileList => {
|
||||
emit('update:modelValue', formatValue(fileList))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.u-upload__wrap__preview) {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
566
components/n-verify/n-verify.vue
Normal file
566
components/n-verify/n-verify.vue
Normal file
@@ -0,0 +1,566 @@
|
||||
<template>
|
||||
<view>
|
||||
<view v-if="visible" class="s-verify-mask" @touchmove.stop.prevent>
|
||||
<view class="s-verify-wrapper" @tap.stop>
|
||||
<view class="s-verify-header">
|
||||
<text class="s-verify-title">安全验证</text>
|
||||
<!-- <view class="s-verify-close" @tap="handleClose">×</view> -->
|
||||
</view>
|
||||
|
||||
<view class="s-verify-body">
|
||||
<view class="s-verify-image" :style="imageStyle">
|
||||
<image v-if="backgroundImage" class="s-verify-image-bg" :src="backgroundImage" mode="aspectFill" />
|
||||
<image
|
||||
v-if="showBlock"
|
||||
class="s-verify-image-block"
|
||||
:style="blockStyle"
|
||||
:src="blockImage"
|
||||
mode="widthFix"
|
||||
/>
|
||||
|
||||
<view class="s-verify-refresh" @tap="refreshCaptcha">
|
||||
<text>↻</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="s-verify-loading">加载中...</view>
|
||||
</view>
|
||||
|
||||
<view v-if="tipMessage" :class="['s-verify-tip', tipClass]">
|
||||
{{ tipMessage }}
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="s-verify-slider"
|
||||
ref="sliderRef"
|
||||
@touchstart.stop.prevent="onDragStart"
|
||||
@touchmove.stop.prevent="onDragMove"
|
||||
@touchend.stop.prevent="onDragEnd"
|
||||
@mousedown.stop="onMouseDown"
|
||||
>
|
||||
<view class="s-verify-slider-track" />
|
||||
<view class="s-verify-slider-fill" :style="sliderFillStyle" />
|
||||
<view
|
||||
class="s-verify-slider-handle"
|
||||
:class="{
|
||||
's-verify-slider-success': success,
|
||||
's-verify-slider-fail': failAnimate
|
||||
}"
|
||||
:style="sliderHandleStyle"
|
||||
>
|
||||
<text class="s-verify-handle-icon">{{ success ? '✔' : '›' }}</text>
|
||||
</view>
|
||||
<text class="s-verify-slider-text">{{ sliderHint }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import nx from '@/nx'
|
||||
import { aesEncrypt } from './utils/aes'
|
||||
|
||||
const props = defineProps({
|
||||
captchaType: {
|
||||
type: String,
|
||||
default: 'blockPuzzle'
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'pop'
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['success', 'error'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const success = ref(false)
|
||||
const failAnimate = ref(false)
|
||||
const verifying = ref(false)
|
||||
const tipMessage = ref('')
|
||||
const sliderRef = ref(null)
|
||||
|
||||
const sliderWidth = 310
|
||||
const sliderHeight = 40
|
||||
const blockWidth = Math.floor((sliderWidth * 47) / 310)
|
||||
const imageHeight = 155
|
||||
|
||||
const dragState = reactive({
|
||||
startClientX: 0,
|
||||
baseOffset: 0,
|
||||
isDragging: false
|
||||
})
|
||||
|
||||
const captchaState = reactive({
|
||||
originalImageBase64: '',
|
||||
jigsawImageBase64: '',
|
||||
token: '',
|
||||
secretKey: ''
|
||||
})
|
||||
|
||||
const handleOffset = ref(0)
|
||||
const maxOffset = computed(() => Math.max(sliderWidth - blockWidth, 0))
|
||||
|
||||
const showBlock = computed(() => props.captchaType === 'blockPuzzle' && blockImage.value)
|
||||
const backgroundImage = computed(() =>
|
||||
captchaState.originalImageBase64 ? `data:image/png;base64,${captchaState.originalImageBase64}` : ''
|
||||
)
|
||||
const blockImage = computed(() =>
|
||||
captchaState.jigsawImageBase64 ? `data:image/png;base64,${captchaState.jigsawImageBase64}` : ''
|
||||
)
|
||||
|
||||
const imageStyle = computed(() => ({
|
||||
width: `${sliderWidth}px`,
|
||||
height: `${imageHeight}px`
|
||||
}))
|
||||
|
||||
const blockStyle = computed(() => ({
|
||||
width: `${blockWidth}px`,
|
||||
height: `${imageHeight}px`,
|
||||
transform: `translateX(${handleOffset.value}px)`
|
||||
}))
|
||||
|
||||
const sliderHandleStyle = computed(() => ({
|
||||
width: `${blockWidth}px`,
|
||||
height: `${sliderHeight}px`,
|
||||
transform: `translateX(${handleOffset.value}px)`
|
||||
}))
|
||||
|
||||
const sliderFillStyle = computed(() => ({
|
||||
width: `${handleOffset.value > 0 ? Math.min(handleOffset.value + blockWidth, sliderWidth) : 0}px`
|
||||
}))
|
||||
|
||||
const sliderHint = computed(() => {
|
||||
if (success.value) {
|
||||
return '验证通过'
|
||||
}
|
||||
if (loading.value) {
|
||||
return '验证码加载中'
|
||||
}
|
||||
return '请按住滑块,拖动完成拼图'
|
||||
})
|
||||
|
||||
const tipClass = computed(() => {
|
||||
if (success.value) return 's-verify-tip-success'
|
||||
if (failAnimate.value) return 's-verify-tip-error'
|
||||
return ''
|
||||
})
|
||||
|
||||
let closeTimer = null
|
||||
let mouseMoveListener = null
|
||||
let mouseUpListener = null
|
||||
|
||||
function show() {
|
||||
if (visible.value) return
|
||||
visible.value = true
|
||||
resetState()
|
||||
nextTick(() => {
|
||||
loadCaptcha()
|
||||
})
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
cleanupMouseListeners()
|
||||
clearCloseTimer()
|
||||
}
|
||||
|
||||
function refreshCaptcha() {
|
||||
if (loading.value) return
|
||||
resetSlider()
|
||||
loadCaptcha()
|
||||
}
|
||||
|
||||
function resetSlider() {
|
||||
handleOffset.value = 0
|
||||
dragState.baseOffset = 0
|
||||
dragState.startClientX = 0
|
||||
dragState.isDragging = false
|
||||
success.value = false
|
||||
failAnimate.value = false
|
||||
tipMessage.value = ''
|
||||
verifying.value = false
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
captchaState.originalImageBase64 = ''
|
||||
captchaState.jigsawImageBase64 = ''
|
||||
captchaState.token = ''
|
||||
captchaState.secretKey = ''
|
||||
resetSlider()
|
||||
}
|
||||
|
||||
async function loadCaptcha() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await nx.$api.sys.getCaptchaCode({ captchaType: props.captchaType })
|
||||
if (res && (res.repCode === '0000' || res.code === 0)) {
|
||||
const data = res.repData || res.data || {}
|
||||
captchaState.originalImageBase64 = data.originalImageBase64 || ''
|
||||
captchaState.jigsawImageBase64 = data.jigsawImageBase64 || ''
|
||||
captchaState.token = data.token || ''
|
||||
captchaState.secretKey = data.secretKey || ''
|
||||
} else {
|
||||
tipMessage.value = res?.repMsg || res?.msg || '验证码获取失败,请重试'
|
||||
emits('error', res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('captcha load error', error)
|
||||
tipMessage.value = '验证码加载异常,请稍后再试'
|
||||
emits('error', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onDragStart(event) {
|
||||
if (loading.value || success.value) return
|
||||
dragState.isDragging = true
|
||||
dragState.startClientX = getClientX(event)
|
||||
dragState.baseOffset = handleOffset.value
|
||||
failAnimate.value = false
|
||||
tipMessage.value = ''
|
||||
}
|
||||
|
||||
function onDragMove(event) {
|
||||
if (!dragState.isDragging) return
|
||||
const currentX = getClientX(event)
|
||||
const delta = currentX - dragState.startClientX
|
||||
let next = dragState.baseOffset + delta
|
||||
if (next < 0) next = 0
|
||||
if (next > maxOffset.value) next = maxOffset.value
|
||||
handleOffset.value = next
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
if (!dragState.isDragging) return
|
||||
dragState.isDragging = false
|
||||
submitVerification()
|
||||
}
|
||||
|
||||
function onMouseDown(event) {
|
||||
if (event?.button !== 0) return
|
||||
if (typeof window !== 'undefined') {
|
||||
mouseMoveListener = ev => {
|
||||
onDragMove(ev)
|
||||
ev.preventDefault()
|
||||
}
|
||||
mouseUpListener = ev => {
|
||||
cleanupMouseListeners()
|
||||
onDragEnd(ev)
|
||||
}
|
||||
window.addEventListener('mousemove', mouseMoveListener)
|
||||
window.addEventListener('mouseup', mouseUpListener)
|
||||
}
|
||||
onDragStart(event)
|
||||
}
|
||||
|
||||
function cleanupMouseListeners() {
|
||||
if (typeof window === 'undefined') return
|
||||
if (mouseMoveListener) {
|
||||
window.removeEventListener('mousemove', mouseMoveListener)
|
||||
mouseMoveListener = null
|
||||
}
|
||||
if (mouseUpListener) {
|
||||
window.removeEventListener('mouseup', mouseUpListener)
|
||||
mouseUpListener = null
|
||||
}
|
||||
}
|
||||
|
||||
function clearCloseTimer() {
|
||||
if (closeTimer) {
|
||||
clearTimeout(closeTimer)
|
||||
closeTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
async function submitVerification() {
|
||||
if (verifying.value) return
|
||||
verifying.value = true
|
||||
failAnimate.value = false
|
||||
|
||||
const point = {
|
||||
x: Math.round(handleOffset.value),
|
||||
y: 5.0
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = captchaState.secretKey
|
||||
? aesEncrypt(JSON.stringify(point), captchaState.secretKey)
|
||||
: JSON.stringify(point)
|
||||
|
||||
const res = await nx.$api.sys.verifyCaptcha({
|
||||
captchaType: props.captchaType,
|
||||
token: captchaState.token,
|
||||
pointJson: payload
|
||||
})
|
||||
|
||||
if (res && (res.repCode === '0000' || res.code === 0)) {
|
||||
success.value = true
|
||||
tipMessage.value = '验证成功'
|
||||
const captchaVerification = captchaState.secretKey
|
||||
? aesEncrypt(`${captchaState.token}---${JSON.stringify(point)}`, captchaState.secretKey)
|
||||
: `${captchaState.token}---${JSON.stringify(point)}`
|
||||
|
||||
emits('success', {
|
||||
captchaVerification,
|
||||
token: captchaState.token,
|
||||
point
|
||||
})
|
||||
|
||||
clearCloseTimer()
|
||||
closeTimer = setTimeout(() => {
|
||||
handleClose()
|
||||
}, 800)
|
||||
} else {
|
||||
handleFail(res?.repMsg || res?.msg || '验证失败,请重试')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('captcha verify error', error)
|
||||
handleFail('网络异常,请重试')
|
||||
} finally {
|
||||
verifying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleFail(message) {
|
||||
failAnimate.value = true
|
||||
tipMessage.value = message
|
||||
emits('error', message)
|
||||
setTimeout(() => {
|
||||
failAnimate.value = false
|
||||
}, 400)
|
||||
setTimeout(() => {
|
||||
refreshCaptcha()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function getClientX(event) {
|
||||
if (event?.touches && event.touches.length) {
|
||||
return event.touches[0].clientX
|
||||
}
|
||||
if (event?.changedTouches && event.changedTouches.length) {
|
||||
return event.changedTouches[0].clientX
|
||||
}
|
||||
return event?.clientX || 0
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupMouseListeners()
|
||||
clearCloseTimer()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close: handleClose,
|
||||
refresh: refreshCaptcha
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.s-verify-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.s-verify-wrapper {
|
||||
width: 340px;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.s-verify-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
.s-verify-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
}
|
||||
|
||||
.s-verify-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
color: #999999;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.s-verify-body {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.s-verify-image {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.s-verify-image-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.s-verify-image-block {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.s-verify-refresh {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.s-verify-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.s-verify-tip {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
padding: 6px 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.s-verify-tip-success {
|
||||
color: #52c41a;
|
||||
background: rgba(82, 196, 26, 0.12);
|
||||
}
|
||||
|
||||
.s-verify-tip-error {
|
||||
color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.12);
|
||||
}
|
||||
|
||||
.s-verify-slider {
|
||||
position: relative;
|
||||
width: 310px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: #f2f3f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.s-verify-slider-track {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 20px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
|
||||
.s-verify-slider-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: var(--ui-BG-Main, #409eff);
|
||||
border-radius: 20px;
|
||||
transition: width 0.05s linear;
|
||||
}
|
||||
|
||||
.s-verify-slider-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 6px 14px rgba(25, 87, 170, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1f2d3d;
|
||||
transition: transform 0.05s linear;
|
||||
}
|
||||
|
||||
.s-verify-slider-success {
|
||||
background: #52c41a;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.s-verify-slider-fail {
|
||||
animation: s-verify-shake 0.3s;
|
||||
}
|
||||
|
||||
.s-verify-handle-icon {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.s-verify-slider-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes s-verify-shake {
|
||||
0% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
components/navbar-back/navbar-back.vue
Normal file
58
components/navbar-back/navbar-back.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<up-navbar
|
||||
v-bind="$attrs"
|
||||
:bgColor="bgColor"
|
||||
titleStyle="color:#fff"
|
||||
leftIconColor="#fff"
|
||||
:left-text="leftText"
|
||||
:title="title"
|
||||
placeholder
|
||||
:autoBack="autoBack"
|
||||
:leftIcon="leftIcon"
|
||||
@rightClick="rightClick"
|
||||
@leftClick="leftClick"
|
||||
>
|
||||
<template #right>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</up-navbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emits = defineEmits(['leftClick', 'rightClick'])
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
autoBack: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#fff'
|
||||
},
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: 'arrow-left'
|
||||
},
|
||||
leftText: {
|
||||
type: String,
|
||||
default: '返回'
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#0055A2'
|
||||
}
|
||||
})
|
||||
|
||||
function rightClick() {
|
||||
emits('rightClick')
|
||||
}
|
||||
function leftClick() {
|
||||
emits('leftClick')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
304
components/sample/auncel-select-popup.vue
Normal file
304
components/sample/auncel-select-popup.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<view>
|
||||
<u-popup :show="showAuncelSelector" closeable @close="close" @open="open" mode="right">
|
||||
<view class="p10">天平选择</view>
|
||||
<scroll-view scroll-y="true" class="content">
|
||||
<u-grid :col="3" @click="doSelect" style="gap: 20px">
|
||||
<u-grid-item v-for="(auncel, index) in auncelList" :index="index" :key="index">
|
||||
<view class="auncel-item">
|
||||
<view class="auncel-name">
|
||||
{{ auncel.deviceCode }}
|
||||
<view style="text-align: center">{{ auncel.controlRealName }}</view>
|
||||
</view>
|
||||
|
||||
<view class="weight">
|
||||
<view :class="getWeightClass(auncel)" :style="getWeightStyle(auncel)">
|
||||
{{ getWeightText(auncel) }}
|
||||
</view>
|
||||
<view v-if="auncel.isConnected === 1" class="weight-unit">{{ auncel.weightUnit }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="auncel.isConnected != 1" class="shade">天平断开</view>
|
||||
</u-grid-item>
|
||||
</u-grid>
|
||||
</scroll-view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import nx from '@/nx' // 假设你的全局状态/工具挂载在 nx
|
||||
|
||||
// Props & Emits
|
||||
const props = defineProps({
|
||||
showAuncelSelector: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
previousAuncelId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:showAuncelSelector', 'doSelect'])
|
||||
|
||||
// 响应式数据
|
||||
const auncelList = ref([])
|
||||
|
||||
// 计算属性:获取用户信息
|
||||
const userInfo = computed(() => nx.$store('user').userInfo)
|
||||
|
||||
// 方法
|
||||
const open = () => {
|
||||
getPageData()
|
||||
listenDeviceData()
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
auncelList.value = []
|
||||
closeDeviceListener()
|
||||
emit('update:showAuncelSelector', false)
|
||||
}
|
||||
|
||||
const getPageData = () => {
|
||||
nx.$api.laboratory
|
||||
.getDeviceLaboratoryListBy({
|
||||
pageNo: 1,
|
||||
pageSize: 999,
|
||||
collectDeviceType: 'balance',
|
||||
deviceStatus: '0',
|
||||
isEnable: '1'
|
||||
})
|
||||
.then(res => {
|
||||
auncelList.value = res.list
|
||||
})
|
||||
}
|
||||
|
||||
const doSelect = index => {
|
||||
const currentAuncel = auncelList.value[index]
|
||||
if (currentAuncel.isConnected !== 1) {
|
||||
uni.showToast({ title: '天平设备尚未连接!', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (currentAuncel.controlRealName && currentAuncel.controlRealName !== userInfo.value.nickname) {
|
||||
uni.showToast({
|
||||
title: `当前天平正被“${currentAuncel.controlRealName}”使用,请选择其他天平!`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let delayFlag = false
|
||||
if (props.previousAuncelId && props.previousAuncelId !== '' && props.previousAuncelId !== currentAuncel.id) {
|
||||
releaseDeviceControl(props.previousAuncelId)
|
||||
delayFlag = true
|
||||
}
|
||||
|
||||
const controlDevice = {
|
||||
msgId: currentAuncel.id,
|
||||
cmd: 'controlDevice',
|
||||
clientType: 'caaClient',
|
||||
data: {
|
||||
deviceId: currentAuncel.id,
|
||||
deviceCode: currentAuncel.deviceCode,
|
||||
deviceName: currentAuncel.productName,
|
||||
isControl: true,
|
||||
controlRealName: userInfo.value.nickname
|
||||
}
|
||||
}
|
||||
|
||||
const sendControl = () => {
|
||||
const controlData = JSON.stringify(controlDevice)
|
||||
nx.$measure.send(controlData)
|
||||
emit('update:showAuncelSelector', false)
|
||||
emit('doSelect', controlDevice)
|
||||
}
|
||||
|
||||
if (delayFlag) {
|
||||
setTimeout(sendControl, 300)
|
||||
} else {
|
||||
sendControl()
|
||||
}
|
||||
}
|
||||
|
||||
const releaseDeviceControl = deviceId => {
|
||||
if (!deviceId) return
|
||||
const controlDevice = {
|
||||
msgId: deviceId,
|
||||
cmd: 'controlDevice',
|
||||
clientType: 'caaClient',
|
||||
data: {
|
||||
deviceId,
|
||||
isControl: false,
|
||||
controlRealName: userInfo.value.nickname
|
||||
}
|
||||
}
|
||||
nx.$measure.send(JSON.stringify(controlDevice))
|
||||
}
|
||||
|
||||
const listenDeviceData = () => {
|
||||
uni.$on('deviceData', handleDeviceData)
|
||||
uni.$on('deviceStatus', handleDeviceStatus)
|
||||
uni.$on('connClose', handleConnClose)
|
||||
}
|
||||
|
||||
const handleDeviceData = res => {
|
||||
if (res.deviceType === 'balance') {
|
||||
auncelList.value.forEach(item => {
|
||||
if (item.id === res.deviceId) {
|
||||
item.weightData = res.weightData
|
||||
item.weightUnit = res.weightUnit
|
||||
item.weightStable = res.weightStable
|
||||
item.isConnected = 1
|
||||
item.controlRealName = res.controlRealName
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeviceStatus = res => {
|
||||
if (res.deviceType === 'balance') {
|
||||
auncelList.value.forEach(item => {
|
||||
if (item.id === res.deviceId) {
|
||||
item.isConnected = res.connected
|
||||
if (res.connected == 0) {
|
||||
item.weightData = ''
|
||||
item.weightUnit = ''
|
||||
item.weightStable = 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleConnClose = () => {
|
||||
auncelList.value.forEach(item => {
|
||||
item.weightData = ''
|
||||
item.weightUnit = ''
|
||||
item.weightStable = 0
|
||||
item.controlRealName = ''
|
||||
item.isConnected = 0
|
||||
})
|
||||
}
|
||||
|
||||
const closeDeviceListener = () => {
|
||||
uni.$off('deviceData', handleDeviceData)
|
||||
uni.$off('deviceStatus', handleDeviceStatus)
|
||||
uni.$off('connClose', handleConnClose)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 如果组件在 mounted 时已打开,可考虑自动加载(但通常由父组件控制 show)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
closeDeviceListener()
|
||||
})
|
||||
function getWeightText(auncel) {
|
||||
if (auncel.isConnected !== 1) {
|
||||
return ''
|
||||
}
|
||||
return auncel.weightData || ''
|
||||
}
|
||||
|
||||
function getWeightClass(auncel) {
|
||||
if (auncel.weightStable === 0) return 'weight-data-yellow'
|
||||
if (auncel.weightStable === 1) return 'weight-data'
|
||||
if (auncel.weightStable === 2) return 'weight-data-warning'
|
||||
return 'weight-data-yellow'
|
||||
}
|
||||
|
||||
function getWeightStyle(auncel) {
|
||||
if (auncel.isConnected !== 1) {
|
||||
return {
|
||||
textAlign: 'center',
|
||||
fontSize: '28px'
|
||||
}
|
||||
}
|
||||
return {
|
||||
textAlign: 'right',
|
||||
fontSize: '32px'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.content {
|
||||
width: 60vw;
|
||||
height: 90vh;
|
||||
padding: 0 20px 40px 20px;
|
||||
}
|
||||
.auncel-item {
|
||||
height: 180px;
|
||||
width: 180px;
|
||||
background-image: url(/static/images/auncel.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.shade {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(31, 41, 55, 0.55);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
z-index: 10;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.auncel-name {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.weight {
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
padding-top: 10px;
|
||||
padding: 10px 30px 0 30px;
|
||||
position: relative;
|
||||
}
|
||||
.weight-data,
|
||||
.weight-data-yellow,
|
||||
.weight-data-warning {
|
||||
font-family: zzjc-lcd;
|
||||
}
|
||||
|
||||
.weight-data {
|
||||
color: #4cd964;
|
||||
}
|
||||
|
||||
.weight-data-yellow {
|
||||
color: #ffff00;
|
||||
}
|
||||
|
||||
.weight-data-warning {
|
||||
color: #ff3333;
|
||||
}
|
||||
.weight-unit {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.auncel-item {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
.auncel-name {
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
components/sample/sample-detail-popup.vue
Normal file
88
components/sample/sample-detail-popup.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<!-- 查看样品详情 -->
|
||||
<template>
|
||||
<view>
|
||||
<u-popup :show="showPopup" @close="close" @open="open" mode="left">
|
||||
<view class="detail_title">
|
||||
{{ taskDetail.sampleName }}
|
||||
{{ taskDetail.sampleAssayCode }}
|
||||
</view>
|
||||
<view>
|
||||
<scroll-view scroll-y style="height: 100vh; width: 30vw">
|
||||
<view style="padding: 10px">
|
||||
<view class="form-item-my" v-for="(field, index) in fields" :key="index">
|
||||
<view class="label-my">{{ field.title }}</view>
|
||||
<view class="value-my">{{ field.value }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="p30"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import nx from '@/nx'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
showPopup: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
detailId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:showPopup'])
|
||||
// Data
|
||||
const fields = ref([])
|
||||
const taskDetail = ref({})
|
||||
|
||||
// Methods
|
||||
const getAllIndexes = arr => {
|
||||
return arr.map((_, index) => index)
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
emit('update:showPopup', false)
|
||||
}
|
||||
|
||||
const getSampleData = () => {
|
||||
const businessAssayTaskDataId = props.detailId
|
||||
nx.$api.assayTask.getSampleAnalysisDataByTaskDataId({ businessAssayTaskDataId }).then(res => {
|
||||
taskDetail.value = res
|
||||
fields.value = res.columns
|
||||
})
|
||||
}
|
||||
const open = () => {
|
||||
getSampleData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-item-my {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.label-my {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.detail_title {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
padding: 20px;
|
||||
padding-top: 35px;
|
||||
background-color: $uni-color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
37
components/wf-comment/wf-comment.vue
Normal file
37
components/wf-comment/wf-comment.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view>
|
||||
<up-row justify="space-between" v-for="item in commentWf">
|
||||
<up-col span="6">
|
||||
<view>
|
||||
{{ item.nodeName }}:<text class="value">{{ item.comment }}</text>
|
||||
</view>
|
||||
</up-col>
|
||||
<up-col span="6">
|
||||
<view class="y-end">
|
||||
<text class="value">{{ item.userName }}</text>
|
||||
<text class="value">{{ item.time }}</text>
|
||||
</view>
|
||||
</up-col>
|
||||
</up-row>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
commentWf: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.u-row {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.value {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
184
components/zzjc-num-keyboard/zzjc-num-keyboard.vue
Normal file
184
components/zzjc-num-keyboard/zzjc-num-keyboard.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<view class="y-f" style="height: 55vh">
|
||||
<view class="weight">
|
||||
<view class="weight-data">{{ nums }}</view>
|
||||
</view>
|
||||
|
||||
<view class="keyboard-container">
|
||||
<view class="keypad">
|
||||
<view
|
||||
class="oner"
|
||||
:style="getListItemStyle(index)"
|
||||
v-for="(item, index) in numbers"
|
||||
:key="'num_' + index"
|
||||
@click="changeNums(item, index)"
|
||||
>
|
||||
{{ item.text }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="func-pad">
|
||||
<view @click="jianshao()" class="oner flex1">
|
||||
<u-icon name="arrow-leftward" bold></u-icon>
|
||||
</view>
|
||||
<view @click="setNull()" class="oner flex1 mt10 mb10">清空</view>
|
||||
<view class="oner confirm" @click="ok()">确认</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
numKeyboardParam: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
|
||||
// Emits(虽然你用的是 uni.$emit,但也可以定义 emit 用于规范)
|
||||
// const emit = defineEmits(['keyboardOK'])
|
||||
|
||||
// Data
|
||||
const nums = ref('')
|
||||
const numbers = ref([
|
||||
{ text: '1' },
|
||||
{ text: '2' },
|
||||
{ text: '3' },
|
||||
{ text: '4' },
|
||||
{ text: '5' },
|
||||
{ text: '6' },
|
||||
{ text: '7' },
|
||||
{ text: '8' },
|
||||
{ text: '9' },
|
||||
{ text: '0' },
|
||||
{ text: '-' },
|
||||
{ text: '.' }
|
||||
])
|
||||
|
||||
// Methods
|
||||
const ok = () => {
|
||||
const val = { val: nums.value }
|
||||
nums.value = ''
|
||||
if (val.val) {
|
||||
uni.$emit('keyboardOK', val)
|
||||
}
|
||||
}
|
||||
|
||||
const setNull = () => {
|
||||
nums.value = ''
|
||||
uni.$emit('keyboardOK', null)
|
||||
}
|
||||
const clearNum = () => {
|
||||
nums.value = ''
|
||||
}
|
||||
const jianshao = () => {
|
||||
if (nums.value) {
|
||||
nums.value = nums.value.substring(0, nums.value.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const changeNums = (item, index) => {
|
||||
const inputChar = item.text
|
||||
// 处理负号 '-'
|
||||
if (inputChar === '-') {
|
||||
// 只允许在输入为空时输入负号(即只能作为第一个字符)
|
||||
if (nums.value === '') {
|
||||
nums.value = '-'
|
||||
}
|
||||
return // 无论是否成功,都不继续处理
|
||||
}
|
||||
if (inputChar === '.') {
|
||||
// 不能以 '.' 开头(除非已有负号)
|
||||
if (nums.value === '' || nums.value === '-') {
|
||||
return false
|
||||
}
|
||||
// 已存在小数点则不允许再输入
|
||||
if (nums.value.indexOf('.') !== -1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查小数位数限制
|
||||
let decimal = props.numKeyboardParam?.decimal ?? -1
|
||||
const parts = nums.value.split('.')
|
||||
if (parts.length === 2 && decimal !== -1) {
|
||||
if (parts[1].length >= decimal) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
nums.value += inputChar
|
||||
}
|
||||
|
||||
const getListItemStyle = index => {
|
||||
return {
|
||||
background: numbers.value[index]?.background || ''
|
||||
}
|
||||
}
|
||||
defineExpose({ clearNum })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.keyboard-container {
|
||||
flex: 5;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(232, 232, 232, 0.98);
|
||||
}
|
||||
.keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: 1fr;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
flex: 4;
|
||||
height: 95%;
|
||||
}
|
||||
|
||||
.oner {
|
||||
width: 95%;
|
||||
font-size: 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.func-pad {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 10px 10px 10px 0;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.confirm {
|
||||
flex: 2;
|
||||
color: #ffffff;
|
||||
background-color: #19be6b;
|
||||
}
|
||||
.weight {
|
||||
width: 100%;
|
||||
background-color: #2c405a;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.weight-data {
|
||||
color: #4cd964;
|
||||
text-align: right;
|
||||
letter-spacing: 5px;
|
||||
font-size: 50px;
|
||||
font-family: zzjc-lcd;
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
36
defaultBaseUrl.js
Normal file
36
defaultBaseUrl.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// 在此不用配置接口前缀
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const BaseUrl = isDev ? 'http://192.168.26.116:888/admin-api' : 'http://172.16.46.62:30081/admin-api'
|
||||
// const BaseUrl = isDev ? 'http://192.168.26.163:48080/admin-api' : 'http://192.168.26.116:888/admin-api'
|
||||
//
|
||||
// const BaseUrl = isDev ? 'http://localhost:9999' : ''
|
||||
const upgradeBaseUrl = 'http://192.168.26.116:888'
|
||||
|
||||
const tenantId = '1'
|
||||
export const clientId = 'zgty_lims'
|
||||
|
||||
function initDefaultBaseUrl() {
|
||||
uni.setStorageSync('base_url', BaseUrl)
|
||||
uni.setStorageSync('tenant_id', tenantId)
|
||||
uni.setStorageSync('upgradeBaseUrl', upgradeBaseUrl)
|
||||
}
|
||||
initDefaultBaseUrl()
|
||||
|
||||
export function getBaseUrl() {
|
||||
return uni.getStorageSync('base_url')
|
||||
}
|
||||
|
||||
export function getImgBaseUrl() {
|
||||
return uni.getStorageSync('base_url').replace('/api', '')
|
||||
}
|
||||
export function getTenantId() {
|
||||
return uni.getStorageSync('tenant_id')
|
||||
}
|
||||
|
||||
export function getUpgradeBaseUrl() {
|
||||
return uni.getStorageSync('upgradeBaseUrl')
|
||||
}
|
||||
export function getWebSocketUrl() {
|
||||
// return uni.getStorageSync('base_url').replace('/api', '') + '/ws'
|
||||
return 'ws://192.168.26.116:888/ws'
|
||||
}
|
||||
13421
hybrid/html/build/pdf.js
Normal file
13421
hybrid/html/build/pdf.js
Normal file
File diff suppressed because it is too large
Load Diff
1
hybrid/html/build/pdf.js.map
Normal file
1
hybrid/html/build/pdf.js.map
Normal file
File diff suppressed because one or more lines are too long
45929
hybrid/html/build/pdf.worker.js
vendored
Normal file
45929
hybrid/html/build/pdf.worker.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
hybrid/html/build/pdf.worker.js.map
vendored
Normal file
1
hybrid/html/build/pdf.worker.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
hybrid/html/web/cmaps/78-EUC-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78-EUC-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/78-EUC-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78-EUC-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/78-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/78-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/78-RKSJ-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/78-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/78ms-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78ms-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/78ms-RKSJ-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/78ms-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/83pv-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/83pv-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/90ms-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/90ms-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/90ms-RKSJ-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/90ms-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/90msp-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/90msp-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/90msp-RKSJ-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/90msp-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/90pv-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/90pv-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/90pv-RKSJ-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/90pv-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Add-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Add-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Add-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Add-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Add-RKSJ-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Add-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Add-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Add-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-0.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-0.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-1.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-1.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-3.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-3.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-4.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-4.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-5.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-5.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-6.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-6.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-CNS1-UCS2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-CNS1-UCS2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-GB1-0.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-GB1-0.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-GB1-1.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-GB1-1.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-GB1-2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-GB1-2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-GB1-3.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-GB1-3.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-GB1-4.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-GB1-4.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-GB1-5.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-GB1-5.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-GB1-UCS2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-GB1-UCS2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-0.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-0.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-1.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-1.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-3.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-3.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-4.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-4.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-5.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-5.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-6.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-6.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Japan1-UCS2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Japan1-UCS2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Korea1-0.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Korea1-0.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Korea1-1.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Korea1-1.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Korea1-2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Korea1-2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Adobe-Korea1-UCS2.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Adobe-Korea1-UCS2.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/B5-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/B5-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/B5-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/B5-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/B5pc-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/B5pc-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/B5pc-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/B5pc-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/CNS-EUC-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/CNS-EUC-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/CNS-EUC-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/CNS-EUC-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/CNS1-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/CNS1-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/CNS1-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/CNS1-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/CNS2-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/CNS2-H.bcmap
Normal file
Binary file not shown.
3
hybrid/html/web/cmaps/CNS2-V.bcmap
Normal file
3
hybrid/html/web/cmaps/CNS2-V.bcmap
Normal file
@@ -0,0 +1,3 @@
|
||||
<03>RCopyright 1990-2009 Adobe Systems Incorporated.
|
||||
All rights reserved.
|
||||
See ./LICENSE<53>CNS2-H
|
||||
BIN
hybrid/html/web/cmaps/ETHK-B5-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/ETHK-B5-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/ETHK-B5-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/ETHK-B5-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/ETen-B5-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/ETen-B5-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/ETen-B5-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/ETen-B5-V.bcmap
Normal file
Binary file not shown.
3
hybrid/html/web/cmaps/ETenms-B5-H.bcmap
Normal file
3
hybrid/html/web/cmaps/ETenms-B5-H.bcmap
Normal file
@@ -0,0 +1,3 @@
|
||||
<02>RCopyright 1990-2009 Adobe Systems Incorporated.
|
||||
All rights reserved.
|
||||
See ./LICENSE<53> ETen-B5-H` ^
|
||||
BIN
hybrid/html/web/cmaps/ETenms-B5-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/ETenms-B5-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/EUC-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/EUC-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/EUC-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/EUC-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Ext-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Ext-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Ext-RKSJ-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Ext-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Ext-RKSJ-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Ext-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/Ext-V.bcmap
Normal file
BIN
hybrid/html/web/cmaps/Ext-V.bcmap
Normal file
Binary file not shown.
BIN
hybrid/html/web/cmaps/GB-EUC-H.bcmap
Normal file
BIN
hybrid/html/web/cmaps/GB-EUC-H.bcmap
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user