19 Commits

Author SHA1 Message Date
geht
7a9c19e4f3 实现IM系统与PC端一致的WebSocket连接,优化聊天功能,支持多端消息同步,更新联系人和会话列表接口,增强用户体验。 2026-07-21 15:53:06 +08:00
geht
f24a21a41c app端新增原材料送检功能 2026-07-20 17:31:54 +08:00
geht
531cac7341 Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-07-20 14:10:52 +08:00
geht
395a52b4c6 APP文件新增 2026-07-20 14:10:39 +08:00
18ed2502c1 烘胶开关添加 2026-07-10 10:40:37 +08:00
dde35d639b 烘胶房库位新增 2026-07-03 15:21:10 +08:00
99987d9d2e 烘胶房及烘胶分类新增 2026-07-03 09:57:58 +08:00
2240e58355 胶料快检完善 2026-07-02 10:07:31 +08:00
765bc6aaa6 胶料快检曲线图展示优化 2026-06-30 17:56:35 +08:00
geht
e79698c19f 优化用户数据表列配置,移除不必要的可调整属性 2026-06-30 17:52:44 +08:00
geht
ec04a7e1ad 列宽调整 2026-06-30 17:45:15 +08:00
d55223a1c0 胶料快检添加离线模式 2026-06-30 14:43:48 +08:00
672259c94f Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-06-30 11:28:09 +08:00
840e68a450 胶料快检添加离线模式 2026-06-30 11:28:04 +08:00
51029374fd Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-06-29 13:33:16 +08:00
15cc5c2879 小料需求计划 2026-06-29 13:33:13 +08:00
efcd73a565 桌面端快检记录新增列表及同步mes 2026-06-22 17:38:49 +08:00
3bce685f3a Merge branch 'main' of http://27.223.88.102:33000/chenx/qhmes 2026-06-18 15:18:14 +08:00
c54d54b40f 桌面端密炼计划、胶料快检调整 2026-06-18 15:18:11 +08:00
998 changed files with 237652 additions and 863 deletions

View File

@@ -0,0 +1,106 @@
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const scopes = fs
.readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name.replace(/s$/, ''))
// precomputed scope
const scopeComplete = execSync('git status --porcelain || true')
.toString()
.trim()
.split('\n')
.find((r) => ~r.indexOf('M src'))
?.replace(/(\/)/g, '%%')
?.match(/src%%((\w|-)*)/)?.[1]
?.replace(/s$/, '')
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release',
],
],
},
prompt: {
/** @use `pnpm commit :f` */
alias: {
f: 'docs: fix typos',
r: 'docs: update README',
s: 'style: update code format',
b: 'build: bump dependencies',
c: 'chore: update config',
},
customScopesAlign: !scopeComplete ? 'top' : 'bottom',
defaultScope: scopeComplete,
scopes: [...scopes, 'mock'],
allowEmptyIssuePrefixs: false,
allowCustomIssuePrefixs: false,
// English
typesAppend: [
{ value: 'wip', name: 'wip: work in process' },
{ value: 'workflow', name: 'workflow: workflow improvements' },
{ value: 'types', name: 'types: type definition file changes' },
],
// 中英文对照版
// messages: {
// type: '选择你要提交的类型 :',
// scope: '选择一个提交范围 (可选):',
// customScope: '请输入自定义的提交范围 :',
// subject: '填写简短精炼的变更描述 :\n',
// body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
// breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
// footerPrefixsSelect: '选择关联issue前缀 (可选):',
// customFooterPrefixs: '输入自定义issue前缀 :',
// footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
// confirmCommit: '是否提交或修改commit ?',
// },
// types: [
// { value: 'feat', name: 'feat: 新增功能' },
// { value: 'fix', name: 'fix: 修复缺陷' },
// { value: 'docs', name: 'docs: 文档变更' },
// { value: 'style', name: 'style: 代码格式' },
// { value: 'refactor', name: 'refactor: 代码重构' },
// { value: 'perf', name: 'perf: 性能优化' },
// { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
// { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
// { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
// { value: 'revert', name: 'revert: 回滚 commit' },
// { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
// { value: 'wip', name: 'wip: 正在开发中' },
// { value: 'workflow', name: 'workflow: 工作流程改进' },
// { value: 'types', name: 'types: 类型定义文件修改' },
// ],
// emptyScopesAlias: 'empty: 不填写',
// customScopesAlias: 'custom: 自定义',
},
}

View File

@@ -0,0 +1,13 @@
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格tab | space
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪

View File

@@ -0,0 +1 @@
src/uni_modules/

View File

@@ -0,0 +1,102 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"computed": true,
"createApp": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"effectScope": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onAddToFavorites": true,
"onBackPress": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onError": true,
"onErrorCaptured": true,
"onHide": true,
"onLaunch": true,
"onLoad": true,
"onMounted": true,
"onNavigationBarButtonTap": true,
"onNavigationBarSearchInputChanged": true,
"onNavigationBarSearchInputClicked": true,
"onNavigationBarSearchInputConfirmed": true,
"onNavigationBarSearchInputFocusChanged": true,
"onPageNotFound": true,
"onPageScroll": true,
"onPullDownRefresh": true,
"onReachBottom": true,
"onReady": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onResize": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onShareAppMessage": true,
"onShareTimeline": true,
"onShow": true,
"onTabItemTap": true,
"onThemeChange": true,
"onUnhandledRejection": true,
"onUnload": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useGeoPosition": true,
"useId": true,
"useModel": true,
"usePageList": true,
"useRequest": true,
"useSlots": true,
"useTemplateRef": true,
"useUpload": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@@ -0,0 +1,97 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-essential',
// eslint-plugin-import 插件, @see https://www.npmjs.com/package/eslint-plugin-import
'plugin:import/recommended',
// eslint-config-airbnb-base 插件 已经改用 eslint-config-standard 插件
'standard',
// 1. 接入 prettier 的规则
'prettier',
'plugin:prettier/recommended',
'./.eslintrc-auto-import.json',
],
overrides: [
{
env: {
node: true,
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script',
},
},
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: [
'@typescript-eslint',
'vue',
// 2. 加入 prettier 的 eslint 插件
'prettier',
// eslint-import-resolver-typescript 插件,@see https://www.npmjs.com/package/eslint-import-resolver-typescript
'import',
],
rules: {
// 3. 注意要加上这一句,开启 prettier 自动修复的功能
'prettier/prettier': 'error',
// turn on errors for missing imports
'import/no-unresolved': 'off',
// 对后缀的检测,否则 import 一个ts文件也会报错需要手动添加'.ts', 增加了下面的配置后就不用了
'import/extensions': [
'error',
'ignorePackages',
{ js: 'never', jsx: 'never', ts: 'never', tsx: 'never' },
],
// 只允许1个默认导出关闭否则不能随意export xxx
'import/prefer-default-export': ['off'],
'no-console': ['off'],
// 'no-unused-vars': ['off'],
// '@typescript-eslint/no-unused-vars': ['off'],
// 解决vite.config.ts报错问题
'import/no-extraneous-dependencies': 'off',
'no-plusplus': 'off',
'no-shadow': 'off',
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-underscore-dangle': 'off',
'no-use-before-define': 'off',
'no-undef': 'off',
'no-unused-vars': 'off',
'no-param-reassign': 'off',
'@typescript-eslint/no-unused-vars': 'off',
// 避免 `eslint` 对于 `typescript` 函数重载的误报
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
},
// eslint-import-resolver-typescript 插件,@see https://www.npmjs.com/package/eslint-import-resolver-typescript
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {},
},
},
globals: {
$t: true,
uni: true,
UniApp: true,
wx: true,
WechatMiniprogram: true,
getCurrentPages: true,
UniHelper: true,
Page: true,
App: true,
NodeJS: true,
},
}

3
JeecgUniapp-master/.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.js linguist-language=vue
*.css linguist-language=vue
*.ts linguist-language=vue

15
JeecgUniapp-master/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
node_modules
dist
.cache
## ide
**/.idea
*.iml
## backend
**/target
**/logs
**/dist
## front
/yarn-error.log

View File

@@ -0,0 +1,16 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-android"
},
{
"app-plus" :
{
"launchtype" : "local"
},
"type" : "uniCloud"
}
]
}

View File

@@ -0,0 +1,6 @@
# registry = https://registry.npmjs.org
registry = https://registry.npmmirror.com
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=true

View File

@@ -0,0 +1,12 @@
# unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
auto-import.d.ts
# vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
uni-pages.d.ts
# 插件生成的文件
src/pages.json
src/manifest.json
# 忽略自动生成文件
src/service/app/**

View File

@@ -0,0 +1,19 @@
// @see https://prettier.io/docs/en/options
module.exports = {
singleQuote: true,
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: false,
trailingComma: 'all',
endOfLine: 'auto',
htmlWhitespaceSensitivity: 'ignore',
overrides: [
{
files: '*.json',
options: {
trailingComma: 'none',
},
},
],
}

View File

@@ -0,0 +1 @@
src/uni_modules/

View File

@@ -0,0 +1,58 @@
// .stylelintrc.cjs
module.exports = {
root: true,
extends: [
// stylelint-config-standard 替换成了更宽松的 stylelint-config-recommended
'stylelint-config-recommended',
// stylelint-config-standard-scss 替换成了更宽松的 stylelint-config-recommended-scss
'stylelint-config-recommended-scss',
'stylelint-config-recommended-vue/scss',
'stylelint-config-html/vue',
'stylelint-config-recess-order',
],
plugins: ['stylelint-prettier'],
overrides: [
// 扫描 .vue/html 文件中的<style>标签内的样式
{
files: ['**/*.{vue,html}'],
customSyntax: 'postcss-html',
},
{
files: ['**/*.{css,scss}'],
customSyntax: 'postcss-scss',
},
],
// 自定义规则
rules: {
'prettier/prettier': true,
// 允许 global 、export 、v-deep等伪类
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'export', 'v-deep', 'deep'],
},
],
'unit-no-unknown': [
true,
{
ignoreUnits: ['rpx'],
},
],
// 处理小程序page标签不认识的问题
'selector-type-no-unknown': [
true,
{
ignoreTypes: ['page'],
},
],
'comment-empty-line-before': 'never', // never|always|always-multi-line|never-multi-line
'custom-property-empty-line-before': 'never',
'no-empty-source': null,
'comment-no-empty': null,
'no-duplicate-selectors': null,
'scss/comment-no-empty': null,
'selector-class-pattern': null,
'font-family-no-missing-generic-family-keyword': null,
},
}

View File

@@ -0,0 +1,17 @@
{
"recommendations": [
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"antfu.unocss",
"antfu.iconify",
"evils.uniapp-vscode",
"uni-helper.uni-helper-vscode",
"uni-helper.uni-app-schemas-vscode",
"uni-helper.uni-highlight-vscode",
"uni-helper.uni-ui-snippets-vscode",
"uni-helper.uni-app-snippets-vscode",
"mrmlnc.vscode-json5",
"streetsidesoftware.code-spell-checker"
]
}

31
JeecgUniapp-master/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,31 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/vite.config.ts",
// "preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}

View File

@@ -0,0 +1,62 @@
{
// 默认格式化工具选择prettier
"editor.defaultFormatter": "esbenp.prettier-vscode",
// 保存的时候自动格式化
"editor.formatOnSave": false,
//开启自动修复
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
// 配置stylelint检查的文件类型范围
"stylelint.validate": ["css", "scss", "vue", "html"], // 与package.json的scripts对应
"stylelint.enable": true,
"css.validate": false,
"less.validate": false,
"scss.validate": false,
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[dotenv]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 配置语言的文件关联
"files.associations": {
"pages.json": "jsonc", // pages.json 可以写注释
"manifest.json": "jsonc" // manifest.json 可以写注释
},
"cSpell.words": [
"Aplipay",
"climblee",
"commitlint",
"dcloudio",
"iconfont",
"qrcode",
"refresherrefresh",
"scrolltolower",
"tabbar",
"Toutiao",
"uvui",
"vitepress",
"Wechat",
"WechatMiniprogram",
"Weixin"
],
"typescript.tsdk": "node_modules\\typescript\\lib",
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
".eslintrc.cjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,.stylelintrc.*,.eslintrc-auto-import.json,.editorconfig,.commitlint.cjs"
}
}

View File

@@ -0,0 +1,62 @@
{
"Print unibest Vue3 SFC": {
"scope": "vue",
"prefix": "v3",
"body": [
"<route lang=\"json5\" type=\"page\">",
"{",
" layout: 'default',",
" style: {",
" navigationBarTitleText: '',",
" navigationStyle: 'custom',",
" disableScroll: true, // 微信禁止页面滚动",
" 'app-plus': {",
" bounce: 'none', // 禁用 iOS 弹性效果",
" },",
" },",
"}",
"</route>\n",
"<template>",
" <PageLayout navTitle=\"页面标题\" backRouteName=\"index\"></PageLayout>",
"</template>\n",
"<script lang=\"ts\" setup>",
"import { ref, reactive } from 'vue'",
"import { onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'",
"import { useMessage, useToast } from 'wot-design-uni'",
"import { useRouter } from '@/plugin/uni-mini-router'",
"import { useUserStore } from '@/store/user'",
"import { http } from '@/utils/http'",
"import { useParamsStore } from '@/store/page-params'\n",
"defineOptions({",
" name: '组件名',",
" options: {",
" styleIsolation: 'shared',",
" },",
"})",
"const router = useRouter()",
"const paramsStore = useParamsStore()",
"const userStore = useUserStore()",
"const toast = useToast()",
"const message = useMessage()",
"</script>\n",
"<style lang=\"scss\" scoped>",
"//",
"</style>\n",
],
},
"Print unibest style": {
"scope": "vue",
"prefix": "st",
"body": ["<style lang=\"scss\" scoped>", "//", "</style>\n"],
},
"Print unibest script": {
"scope": "vue",
"prefix": "sc",
"body": ["<script lang=\"ts\" setup>", "//$3", "</script>\n"],
},
"Print unibest template": {
"scope": "vue",
"prefix": "te",
"body": ["<template>", " <view class=\"\">$1</view>", "</template>\n"],
},
}

214
JeecgUniapp-master/LICENSE Normal file
View File

@@ -0,0 +1,214 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2019 <a href="http://www.jeecg.com">Jeecg Boot</a> All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
JeecgUniapp 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京地址中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱jeecgos@163.com
本软件受适用的国家软件著作权法(包括国际条约)和开源协议 双重保护许可。
开源协议中文释意如下:
1.JeecgUniapp开源版本无任何限制在遵循本开源协议条款下允许商用使用不会造成侵权行为。
2.允许基于本平台软件开展业务系统开发。
3.在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件。
最终解释权归http://www.jeecg.com

View File

@@ -0,0 +1,188 @@
# 项目介绍
JeecgUniapp 是`JeecgBoot低代码平台`的配套`APP移动框架`,项目采用 Uniapp、Vue3.0、Vite、 Wot-design-uni、TypeScript 等最新技术栈,包括二次封装组件、路由拦截、请求拦截等功能。实现了与 `JeecgBoot` 完美对接:
目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能提供了丰富的组件!
当前最新版本: 3.0.0发布日期2025-03-10
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.0.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
### 视频介绍
[![](https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/jeecguniappvideo.png)](https://www.bilibili.com/video/BV15QQeYoEfQ)
### 新版特点
- 一份代码多终端适配小程序、H5、安卓、ios、鸿蒙Next。
- 学习成本低、组件丰富、兼容性好、支持iframe嵌入。
- 新版APP具备低代码能力包括表单设计、仪表盘设计等。
- 新版最大亮点是架构升级到 Vue3适配鸿蒙 Next。
- 支持使用 VSCode 和 IntelliJ IDEA 开发,不再必须 HBuilderX这显著提升了开发体验和效率。
- 支持低代码能力例如Online表单可以在APP端展示和进行数据的添加与修改仪表盘和大屏也支持移动端展示。
### 前端技术栈
- 前端 IDE 建议Vscode、HBuilderX、Intellij IDEA
- 最新技术栈Uniapp + Vue3.0 + Vite + TypeScript + Wot-design-uni + pinia + unocss
- 依赖管理node、pnpm
### 环境要求
- 本地环境安装 node18+、pnpm 7.3+
### 后台源码
- https://github.com/jeecgboot/jeecg-boot
### 技术交流
- 产品官网: [http://jeecg.com/appIndex](http://jeecg.com/appIndex)
- 开发文档: [https://help.jeecg.com/uniapp3](https://help.jeecg.com/uniapp3)
- 官方支持: 遇到BUG可以在github上 [发Issue](https://github.com/jeecgboot/jeecg-uniapp/issues/new)
- QQ交流群 716488839
### 快速启动
#### 配置接口地址
> 配置文件:`env/.env.development`
请把 http://localhost:8080/jeecg-boot 替换成自己地址,其他不用改。
```javascript
// 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development';
// 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false;
// 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true;
// 后台接口全路径地址(必填)
VITE_SERVER_BASEURL = 'http://localhost:8080/jeecg-boot';
```
#### 启动项目
执行命令安装依赖
```
pnpm i
```
运行启动命令,运行 `H5`
```
pnpm run dev
```
### 新旧版本对比
| 特性 | 旧版 | 新版 |
| -------------------- | -------------- |------------------------------------------------|
| **技术栈** | Vue2技术陈旧 | Vue3现代化开发体验 |
| **UI 框架** | 未集成 | 集成`wot-design-uni` |
| **编辑器** | 仅限 HbuilderX | 支持 VSCode、Intellij IDEA 等主流编辑器 |
| **鸿蒙** | 不支持 | 支持鸿蒙系统 |
| **构建工具** | Webpack | Vite构建更快 |
| **Unocss 原子化** | 不支持 | 支持 Unocss 原子化 |
| **TypeScript 支持** | 不支持 | 支持 TS提供类型提示 |
| **基础组件封装** | 较少 | 封装丰富组件用户、部门、分类字典树、自定义树、popup、popupDict、导航组件等 |
| **代码片段快捷创建** | 无 | 支持 v3 快速创建页面片段 |
### 入门必备
本项目需要一定前端基础知识,请确保掌握 Vue 的基础知识,以便能处理一些常见的问题。 建议在开发前先学一下以下内容,提前了解和学习这些知识,会对项目理解非常有帮助:
- [Vue3 文档](https://cn.vuejs.org)
- [Uniapp](https://uniapp.dcloud.net.cn/)
- [Wot-design-uni](https://wot-design-uni.cn)
- [Unibest 文档](https://www.unibest.tech)
- [TypeScript](https://www.typescriptlang.org)
- [Es6](https://es6.ruanyifeng.com/)
- [Vitejs](https://vitejs.dev)
- [Pinia(vuex 替代方案)](https://pinia.esm.dev/introduction.html)
- [Vue-RFCS](https://github.com/vuejs/rfcs)
- [UnoCSS](https://unocss.dev)
### 效果预览
- 基础功能
![](https://oscimg.oschina.net/oscnet/up-9fb74025440e6066651599d78b4bc78f2cd.png)
![](https://oscimg.oschina.net/oscnet/up-7605e213638a559bba64279b6db93af3ed0.png)
![](https://oscimg.oschina.net/oscnet/up-43ddd52486509ab06a920c3f99f42b8b432.png)
![](https://oscimg.oschina.net/oscnet/up-02d83a8fe3fab4c0153862a9084f8a94cbb.png)
![](https://oscimg.oschina.net/oscnet/up-937a63d5e13869c40e6f1437452171d8235.png)
- 聊天功能
![](https://oscimg.oschina.net/oscnet/up-64bea8f521888f1c2b192c140ad3ba47024.png)
![](https://oscimg.oschina.net/oscnet/up-58d50a5ca1e3c15e628ede2dc59e87bca97.png)
![](https://oscimg.oschina.net/oscnet/up-800a03ecb4238a788ed32ecceefa809895b.png)
![](https://oscimg.oschina.net/oscnet/up-e840f754aa5927584a8e482662758b45218.png)
![](https://oscimg.oschina.net/oscnet/up-941cbcc10b4b6fc02d1f62b446764a465c3.png)
- Online表单
![](https://oscimg.oschina.net/oscnet/up-051105a465e568b6a8935100a5a1af6de69.png)
![](https://oscimg.oschina.net/oscnet/up-942a0398a8cae684ee2bf8203c3ec96b709.png)
![](https://oscimg.oschina.net/oscnet/up-dbc6687d6c809f2682c5b2fb013ee6475e5.png)
- 仪表盘
![](https://oscimg.oschina.net/oscnet/up-c31fb30bce7784318b7c3487e44cd3d928b.png)
![](https://oscimg.oschina.net/oscnet/up-b945e4d0fed55091cc30b63044cbd6cdee4.png)
- 动画展示
![](https://oscimg.oschina.net/oscnet/up-49e27699eb278c7c6b6748bfeaeb6c13b72.gif)
### 功能模块
```
├─框架实现
│ ├─APP开发框架搭建
│ ├─登录对接
│ ├─TOKEN接口机制
│ ├─热更新\覆盖更新
├─基础功能
│ ├─菜单栏目
│ ├─登录页面
│ ├─移动首页
│ ├─个人信息设置
├─消息中心
│ ├─通讯录
│ ├─系统公告
│ ├─消息推送
│ ├─在线聊天
├─低代码功能
│ ├─Online表单列表+表单渲染)
│ ├─仪表盘(移动展示)
├─示例代码
│ ├─调用摄像头扫码(扫码)
│ ├─获取地理位置(定位)
├─新增组件
│ ├─页面滚动
│ ├─日历
│ ├─时间选择
│ ├─下拉选择
│ ├─图片上传
├─。。。
```

View File

@@ -0,0 +1,136 @@
# DT50 扫描识别 —— 广播参数与接入说明
> 面向 APP「协作」页「扫描识别」功能。
> 设备:优博讯 UROVO **DT50**Android 11
> 参数来源:**设备实测 adb logcat 抓真实扫码** + **优博讯官方 ScanManager 文档** + **DCloud UniApp 官方示例** 三方交叉验证。
> 记录日期2026-07-20。
---
## 一、核心参数(最重要)
| 项目 | 值 | 说明 |
|---|---|---|
| 输出方式 | **Intent / 广播** | 设备当前**已是此模式**,无需在设置页切换 |
| 广播 Action | `android.intent.ACTION_DECODE_DATA` | 实测 + 官方一致 |
| 条码文本String | `barcode_string` | 官方常量 `BARCODE_STRING_TAG`**首选** |
| 原始字节byte[] | `barcode` | 官方常量 `DECODE_DATA_TAG`,兜底用 |
| 长度int | `length` | 官方常量 `BARCODE_LENGTH_TAG` |
| 码制int/String | `barcodeType` | 官方常量 `BARCODE_TYPE_TAG`,实测读出 `Code128` |
| 扫描键 keycode | `523` | KEYCODE_SCAN机身侧键 |
## 二、设备侧验证结论
- **硬件**:成像头正常,成功解出 Code128 条码 ✅
- **触发**:侧键 keycode = 523按下发 `ACTION_KEYCODE_SCAN_PRESSED`
- **广播**:成功解码时实测发出 `android.intent.ACTION_DECODE_DATA (has extras)` 并被接收 ✅
- **模式**:当前即广播输出(非键盘输出),**直接 registerReceiver 即可**
- **无需**:原生 `.aar` 插件 / 自定义基座 / USB 驱动 / 完整 AndroidStudio 套壳;标准基座即可
相关系统组件(备查):
- 扫描设置 App`com.ubx.barcodescanner.tool`
- 扫描服务:`com.ubx.uscanner`
- DataWedge 式配置:`com.ubx.datawedge`
## 三、UniApp 监听代码(标准基座,无需插件)
```js
let scanReceiver = null;
/** 开始监听 DT50 扫描广播 */
function startScanListener(onBarcode) {
const main = plus.android.runtimeMainActivity();
const IntentFilter = plus.android.importClass('android.content.IntentFilter');
const filter = new IntentFilter();
filter.addAction('android.intent.ACTION_DECODE_DATA'); // DT50 广播 Action
scanReceiver = plus.android.implements('io.dcloud.feature.internal.reflect.BroadcastReceiver', {
onReceive: function (context, intent) {
plus.android.importClass(intent);
// 首选直接取字符串
let code = intent.getStringExtra('barcode_string');
// 兜底:部分场景用字节数组
if (!code) {
const bytes = intent.getByteArrayExtra('barcode');
if (bytes) code = plus.android.newObject('java.lang.String', bytes);
}
const type = intent.getStringExtra('barcodeType'); // 码制,可选
if (code) onBarcode(code, type);
}
});
main.registerReceiver(scanReceiver, filter);
}
/** 停止监听(务必在页面卸载时调用,避免重复注册 / 内存泄漏) */
function stopScanListener() {
if (scanReceiver) {
plus.android.runtimeMainActivity().unregisterReceiver(scanReceiver);
scanReceiver = null;
}
}
```
页面接入:
```js
onLoad() {
startScanListener((code, type) => {
this.barcode = code;
// TODO: 调 MES 后端接口 / 查料
});
},
onUnload() {
stopScanListener();
}
```
> 注意:仅 Appplus 环境下可用H5/小程序无 `plus`,接入时需做平台判断(`#ifdef APP-PLUS`)。
## 四、故障排查
| 现象 | 原因 / 处理 |
|---|---|
| 收不到广播 | 确认扫描设置里输出为「Intent/广播」而非「键盘/输入法」;确认 Action 拼写 |
| 扫描后文本框被输入内容 | 键盘输出模式也开了,去扫描设置关掉键盘输出 |
| `barcode_string` 为空 | 用兜底的 `barcode`(byte[]) 转 String |
| 一直解码失败(红光有但不出码) | 距离 15~20cm红光完整压住条码拿稳 1~2 秒;避免手机屏幕反光码 |
## 五、参考
- 优博讯官方 ScanManager 文档https://en.urovo.com/developer/android/device/ScanManager.html
- DCloud UniApp 监听优博讯扫码广播含示例https://ask.dcloud.net.cn/article/36468
## 六、本仓库接入位置2026-07-20
| 项 | 位置 |
|---|---|
| 菜单入口 | `src/common/work.ts` → 其他服务「合同」后「扫描识别」 |
| 页面 | `src/pages/scanIdentify/scanIdentify.vue` |
| 广播工具 | `src/utils/dt50Scan.ts` |
| 行为 | 仅广播展示条码/码制/时间,不调后端、不弹输入法 |
| 生命周期 | `onShow` 注册 / `onHide`+`onUnload` 注销;同码防抖 |
## 七、键盘输出 vs 广播输出
| | 键盘输出Keyboard / 楔入) | 广播输出Intent / Broadcast |
|---|---|---|
| 原理 | 把条码当成「敲键盘」打进当前焦点输入框 | 系统发广播App 用 `registerReceiver` 收 |
| App 是否要抢焦点 | 要,通常还会弹出软键盘 | **不需要**输入框焦点 |
| 适合场景 | 记事本、任意输入框快速验证 | 业务页稳定取码、拿码制等扩展字段 |
| 本页策略 | 已关闭楔入,避免弹输入法 | **本页只走广播** |
设备设置建议:扫描设置 → 关闭「键盘输出」开启「Intent/广播输出」。
本页进入时会主动 `hideKeyboard`,不自动聚焦任何输入框。
## 附:参数是如何抓到的(可复现)
```bash
# adb 路径(本机 Android SDK
ADB="C:/Users/hp/AppData/Local/Android/Sdk/platform-tools/adb.exe"
$ADB devices -l # 确认 DT50 已连USB 调试)
$ADB logcat -c # 清空日志缓冲
$ADB shell "timeout 40 logcat -v threadtime '*:V'" > scan.log # 抓 40s
# 抓取期间用 DT50 侧键扫几次条码,然后:
grep -E "ACTION_DECODE_DATA|barcodeType|DECODER" scan.log
```

29
JeecgUniapp-master/env/.env vendored Normal file
View File

@@ -0,0 +1,29 @@
VITE_APP_TITLE = 'JeecgBoot-uniapp'
VITE_APP_PORT = 9000
VITE_UNI_APPID = '__UNI__E0CC280'
VITE_WX_APPID = 'wx8e287639924edb51'
# h5部署网站的base配置到 manifest.config.ts 里的 h5.router.base
VITE_APP_PUBLIC_BASE=/
VITE_SERVER_BASEURL = ''
# 上传接口
VITE_UPLOAD_BASEURL = ''
# 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
# 下面的变量如果没有设置,会默认使用 VITE_SERVER_BASEURL or VITE_UPLOAD_BASEURL
VITE_SERVER_BASEURL__WEIXIN_DEVELOP = ''
VITE_SERVER_BASEURL__WEIXIN_TRIAL = ''
VITE_SERVER_BASEURL__WEIXIN_RELEASE = ''
VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = ''
VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = ''
VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = ''
# h5是否需要配置代理
VITE_APP_PROXY= true
VITE_APP_PROXY_PREFIX = '/api'
# 是否启用mock (1.仅支持h5 2.启用必须要开启代理,否则生效。)
VITE_USE_MOCK = false

View File

@@ -0,0 +1,8 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false
# 是否开启sourcemapApp 真机编译易 OOM开发期先关闭
VITE_SHOW_SOURCEMAP = false
VITE_SERVER_BASEURL = 'http://192.168.1.3:8888/jeecg-boot'

View File

@@ -0,0 +1,8 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = false
VITE_SERVER_BASEURL = 'https://api3.boot.jeecg.com'

4
JeecgUniapp-master/env/.env.test vendored Normal file
View File

@@ -0,0 +1,4 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html build-time="%BUILD_TIME%">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<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>JeecgBoot-uniapp3</title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,168 @@
// manifest.config.ts
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
import path from 'node:path'
import { loadEnv } from 'vite'
// 获取环境变量的范例
const env = loadEnv(process.env.NODE_ENV!, path.resolve(process.cwd(), 'env'))
const {
VITE_APP_TITLE,
VITE_UNI_APPID,
VITE_WX_APPID,
VITE_APP_PUBLIC_BASE,
VITE_FALLBACK_LOCALE,
} = env
export default defineManifestConfig({
name: VITE_APP_TITLE,
appid: VITE_UNI_APPID,
description: '',
versionName: '1.0.0',
versionCode: '100',
transformPx: false,
locale: VITE_FALLBACK_LOCALE, // 'zh-Hans'
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
webView: {
render: 'always',
userAgent: '',
},
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
compatible: {
ignoreVersion: true,
},
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {
Maps: {},
Messaging: {},
Contacts: {},
Camera: {},
},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
minSdkVersion: 26,
targetSdkVersion: 30,
abiFilters: ['armeabi-v7a', 'arm64-v8a'],
permissions: [
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
'<uses-permission android:name="android.permission.VIBRATE"/>',
'<uses-permission android:name="android.permission.READ_LOGS"/>',
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.CAMERA"/>',
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
'<uses-feature android:name="android.hardware.camera"/>',
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
],
},
/* ios打包配置 */
ios: {},
/* SDK配置 */
sdkConfigs: {
maps: {
amap: {
name: 'amap_15931993294Bqxlq8EgG',
appkey_ios: 'c913e46ffdf548ebc56ac1cf4d883e7e',
appkey_android: 'c913e46ffdf548ebc56ac1cf4d883e7e',
},
},
},
/* 图标配置 */
icons: {
android: {
hdpi: 'src/static/app/icons/72x72.png',
xhdpi: 'src/static/app/icons/96x96.png',
xxhdpi: 'src/static/app/icons/144x144.png',
xxxhdpi: 'src/static/app/icons/192x192.png',
},
ios: {
appstore: 'src/static/app/icons/1024x1024.png',
ipad: {
app: 'src/static/app/icons/76x76.png',
'app@2x': 'src/static/app/icons/152x152.png',
notification: 'src/static/app/icons/20x20.png',
'notification@2x': 'src/static/app/icons/40x40.png',
'proapp@2x': 'src/static/app/icons/167x167.png',
settings: 'src/static/app/icons/29x29.png',
'settings@2x': 'src/static/app/icons/58x58.png',
spotlight: 'src/static/app/icons/40x40.png',
'spotlight@2x': 'src/static/app/icons/80x80.png',
},
iphone: {
'app@2x': 'src/static/app/icons/120x120.png',
'app@3x': 'src/static/app/icons/180x180.png',
'notification@2x': 'src/static/app/icons/40x40.png',
'notification@3x': 'src/static/app/icons/60x60.png',
'settings@2x': 'src/static/app/icons/58x58.png',
'settings@3x': 'src/static/app/icons/87x87.png',
'spotlight@2x': 'src/static/app/icons/80x80.png',
'spotlight@3x': 'src/static/app/icons/120x120.png',
},
},
},
},
},
/* 快应用特有相关 */
quickapp: {},
/* 小程序特有相关 */
'mp-weixin': {
appid: VITE_WX_APPID,
setting: {
urlCheck: false,
minified: true,
es6: true,
},
usingComponents: true,
lazyCodeLoading: 'requiredComponents',
// __usePrivacyCheck__: true,
},
'mp-alipay': {
usingComponents: true,
styleIsolation: 'shared',
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
h5: {
router: {
base: VITE_APP_PUBLIC_BASE,
},
sdkConfigs: {
maps: {
amap: {
key: '20854e7d231ee339bfa3b277c840070c',
securityJsCode: '7a542edee4a82e56ed88fef8ef42b5a5',
serviceHost: '',
},
},
},
},
'app-harmony': {
distribute: {
bundleName: 'uniapp.demo.test',
},
},
uniStatistics: {
enable: false,
},
vueVersion: '3',
})

View File

@@ -0,0 +1,117 @@
// mock/getChatLists.js
import Mock from 'mockjs'
import { readFileSync } from 'fs'
import path from 'path'
import { parse } from 'dotenv'
// 指定 .env 文件路径
const envPath = path.resolve(__dirname, '../env/.env')
const envContent = readFileSync(envPath, 'utf8')
const parsedEnv = parse(envContent)
const proxyPrefix = parsedEnv.VITE_APP_PROXY_PREFIX ?? '/api'
export default [
{
url: `${proxyPrefix}/eoa/im/newApi/getChatList`,
method: 'get', // 或 post
response: () => {
return Mock.mock({
code: 200,
success: true,
result: {
'logVoList|8-10': [
{
'id|+1': 1,
fromUserName: '@cname',
sendTime: Mock.mock('@date("yyyy-MM-dd")'),
fromAvatar: () => {
const sentences = [
'https://picsum.photos/100/100',
'https://random.imagecdn.app/100/100',
'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=100',
'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
'https://dummyimage.com/100x100/f37b1d/fff&text=%E7%8E%8B%E4%BA%94',
'https://dummyimage.com/100x100/59c7b8/fff&text=%E5%85%AD%E5%AD%90',
]
return sentences[Math.floor(Math.random() * sentences.length)]
},
'type|1': ['friend', 'discussion', 'group'],
'izTop|1': [1, 0],
'status|1': ['offline', 'online'],
'msgFrom|+12': 4000,
'msgTo|+34': 100,
},
],
},
})
},
},
{
url: `${proxyPrefix}/eoa/im/newApi/creatFriendSession`,
method: 'post', // 或 post
response: () => {
return Mock.mock({
code: 200,
success: true,
result: {
'accountId|+100': 4000,
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=100',
email: '@email',
'id|12': 112,
'msgTo|+34': 100,
'phone|9': 123,
},
})
},
},
{
url: '/api/eoa/im/newApi/records',
method: 'get', // 或 post
response: () => {
return Mock.mock({
code: 200,
success: true,
result: {
'records|8-10': [
{
'id|+1': 1,
fromUserName: '@cname',
sendTime: Mock.mock('@date("yyyy-MM-dd")'),
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
'type|1': ['friend', 'discussion', 'group'],
'izTop|1': [1, 0],
'status|1': ['offline', 'online'],
'msgFrom|+12': 4000,
'msgTo|+34': 100,
// msgData: Mock.mock('@cparagraph()'),
msgData: () => Mock.mock('@cparagraph()'),
userId: '1678948772039729154',
msgType: 'text',
},
],
},
})
},
},
{
url: `${proxyPrefix}/eoa/im/newApi/creatFriendSession`,
method: 'post', // 或 post
response: () => {
return Mock.mock({
code: 200,
success: true,
result: 'success',
})
},
},
{
url: `${proxyPrefix}/eoa/im/newApi/sendMessage`,
method: 'post', // 或 post
response: () => {
return Mock.mock({
code: 200,
success: true,
result: 'success',
})
},
},
]

View File

@@ -0,0 +1,13 @@
import type { GenerateServiceProps } from 'openapi-ts-request'
export default [
{
schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
serversPath: './src/service/app',
requestLibPath: `import request from '@/utils/request';\n import { CustomRequestOptions } from '@/interceptors/request';`,
requestOptionsType: 'CustomRequestOptions',
isGenReactQuery: true,
reactQueryMode: 'vue',
isGenJavaScript: false,
},
] as GenerateServiceProps[]

View File

@@ -0,0 +1,157 @@
{
"name": "JeecgUniapp3",
"type": "commonjs",
"version": "3.0.0",
"description": "JeecgBoot配套APP移动框架一份代码多终端适配支持小程序、H5、APP、ios、安卓、鸿蒙",
"engines": {
"node": ">=18",
"pnpm": ">=7.30"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"uvm": "npx @dcloudio/uvm@latest",
"uvm-rm": "node ./scripts/postupgrade.js",
"postuvm": "echo upgrade uni-app success!",
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev": "uni",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp": "uni -p mp-weixin",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-xhs": "uni -p mp-xhs",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp": "uni build -p mp-weixin",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-xhs": "uni build -p mp-xhs",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit",
"release": "standard-version",
"cz": "czg",
"openapi-ts-request": "openapi-ts"
},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4030620241128001",
"@dcloudio/uni-app-harmony": "3.0.0-4030620241128001",
"@dcloudio/uni-app-plus": "3.0.0-4030620241128001",
"@dcloudio/uni-components": "3.0.0-4030620241128001",
"@dcloudio/uni-h5": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-alipay": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-baidu": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-jd": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-lark": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-qq": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-weixin": "3.0.0-4030620241128001",
"@dcloudio/uni-mp-xhs": "3.0.0-4030620241128001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4030620241128001",
"@tanstack/vue-query": "^5.62.16",
"@vant/area-data": "^2.0.0",
"abortcontroller-polyfill": "^1.7.8",
"dayjs": "1.11.10",
"echarts": "^5.6.0",
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"qs": "6.5.3",
"uni-parse-pages": "^0.0.1",
"vue": "3.4.21",
"vue-i18n": "9.1.9",
"wot-design-uni": "^1.8.0",
"z-paging": "^2.8.4"
},
"devDependencies": {
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.3",
"@dcloudio/types": "^3.4.14",
"@dcloudio/uni-automator": "3.0.0-4030620241128001",
"@dcloudio/uni-cli-shared": "3.0.0-4030620241128001",
"@dcloudio/uni-stacktracey": "3.0.0-4030620241128001",
"@dcloudio/uni-uts-v1": "3.0.0-4030620241128001",
"@dcloudio/vite-plugin-uni": "3.0.0-4030620241128001",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@iconify-json/carbon": "^1.2.4",
"@rollup/rollup-darwin-x64": "^4.28.0",
"@types/node": "^20.17.9",
"@types/wechat-miniprogram": "^3.4.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@uni-helper/uni-types": "1.0.0-alpha.3",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.10",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.7",
"@uni-helper/vite-plugin-uni-pages": "0.2.20",
"@uni-helper/vite-plugin-uni-platform": "^0.0.4",
"@unocss/preset-legacy-compat": "^0.59.4",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.20",
"commitlint": "^18.6.1",
"czg": "^1.9.4",
"dotenv": "^16.4.7",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"mockjs": "^1.1.0",
"openapi-ts-request": "^1.1.2",
"postcss": "^8.4.49",
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "1.77.6",
"standard-version": "^9.5.0",
"stylelint": "^16.11.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended": "^14.0.1",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-prettier": "^5.0.2",
"terser": "^5.36.0",
"typescript": "^5.7.2",
"unocss": "^0.58.9",
"unocss-applet": "^0.7.8",
"unplugin-auto-import": "^0.17.8",
"vite": "5.2.8",
"vite-plugin-mock": "^3.0.2",
"vite-plugin-restart": "^0.4.2",
"vitepress": "^1.5.0",
"vue-tsc": "^1.0.24"
}
}

View File

@@ -0,0 +1,55 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
globalStyle: {
navigationStyle: 'default',
navigationBarTitleText: 'uniapp',
navigationBarBackgroundColor: '#f8f8f8',
navigationBarTextStyle: 'white',
backgroundColor: '#000000',
},
easycom: {
autoscan: true,
custom: {
'^wd-(.*)': 'wot-design-uni/components/wd-$1/wd-$1.vue',
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
'z-paging/components/z-paging$1/z-paging$1.vue',
},
},
tabBar: {
color: '#aaa',
selectedColor: '#39b54a',
backgroundColor: '#F8F8F8',
borderStyle: 'black',
height: '50px',
fontSize: '11px',
iconWidth: '24px',
spacing: '3px',
list: [
{
iconPath: 'static/tabbar/tabbar-message-2.png',
selectedIconPath: 'static/tabbar/tabbar-message.png',
pagePath: 'pages/message/message',
text: '消息',
},
{
iconPath: 'static/tabbar/tabbar-home-2.png',
selectedIconPath: 'static/tabbar/tabbar-home.png',
pagePath: 'pages/index/index',
text: '生产',
},
// {
// iconPath: 'static/tabbar/tabbar-workHome-2.png',
// selectedIconPath: 'static/tabbar/tabbar-workHome.png',
// pagePath: 'pages-super/flow/flowIndex',
// text: '审批',
// },
{
iconPath: 'static/tabbar/tabbar-user-2.png',
selectedIconPath: 'static/tabbar/tabbar-user.png',
pagePath: 'pages/user/people',
text: '个人',
},
],
},
})

15205
JeecgUniapp-master/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
// # 执行 `pnpm upgrade` 后会升级 `uniapp` 相关依赖
// # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
// # 只需要执行下面的命令即可
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { exec } = require('child_process')
// 定义要执行的命令
const dependencies = [
'@dcloudio/uni-app-harmony',
// TODO: 如果需要某个平台的小程序,请手动删除或注释掉
'@dcloudio/uni-mp-alipay',
'@dcloudio/uni-mp-baidu',
'@dcloudio/uni-mp-jd',
'@dcloudio/uni-mp-kuaishou',
'@dcloudio/uni-mp-lark',
'@dcloudio/uni-mp-qq',
'@dcloudio/uni-mp-toutiao',
'@dcloudio/uni-mp-xhs',
'@dcloudio/uni-quickapp-webview',
// i18n模板要注释掉下面的
'vue-i18n',
]
// 使用exec执行命令
exec(`pnpm un ${dependencies.join(' ')}`, (error, stdout, stderr) => {
if (error) {
// 如果有错误,打印错误信息
console.error(`执行出错: ${error}`)
return
}
// 打印正常输出
console.log(`stdout: ${stdout}`)
// 如果有错误输出,也打印出来
console.error(`stderr: ${stderr}`)
})

View File

@@ -0,0 +1,107 @@
<script lang="ts">
import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
import { beforEach } from '@/router/index'
// #ifdef APP-PLUS
import appUpdate from "@/common/appUpdate";
// #endif
export default {
onLaunch: function (options) {
console.log('App Launch')
console.log('应用启动路径:', options.path)
// #ifdef APP-PLUS
// 检测升级
appUpdate()
// #endif
},
onShow: function (options) {
console.log('App Show')
console.log('应用启动路径:', options.path)
// 首次进入页面时路由拦截
setTimeout(() => {
const currentPage = options.path
beforEach({ path: '/' }, { path: currentPage, fullPath: currentPage }, (data) => {
if (data?.path) {
uni.redirectTo({ url: data.path })
}
})
}, 100)
},
onHide: function () {
console.log('App Hide')
},
// 全局变量
globalData: {
isLocalConfig: true,
systemInfo: uni.getSystemInfoSync(),
// 导航的高度
navHeight: 44,
},
}
</script>
<style lang="scss">
:root,
page {
font-size: 14px;
color: #333333;
font-family:
Helvetica Neue,
Helvetica,
sans-serif;
}
uni-page-body {
height: 100%;
& > uni-view {
height: 100%;
}
}
.shadow-warp {
position: relative;
box-shadow: 0 0 5px rgba(168, 92, 92, 0.1);
}
/* stylelint-disable selector-type-no-unknown */
button::after {
border: none;
}
swiper,
scroll-view {
flex: 1;
height: 100%;
overflow: hidden;
}
image {
width: 100%;
height: 100%;
vertical-align: middle;
}
// 单行省略,优先使用 unocss: text-ellipsis
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 两行省略
.ellipsis-2 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-all;
}
// 三行省略
.ellipsis-3 {
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
</style>

View File

@@ -0,0 +1,103 @@
//APP更新
export default function appUpdate() {
console.log('plus.runtime.appid', plus.runtime.appid)
console.log('plus.runtime.version', plus.runtime.version)
console.log('plus.device.imei', plus.device.imei)
uni.request({
url: 'http://app.myhjdc.cn/update.json', //检查更新的服务器地址
data: {
appid: plus.runtime.appid,
version: plus.runtime.version,
imei: plus.device.imei,
},
success: (res: any) => {
plus.runtime.getProperty(plus.runtime.appid, function (wgtinfo) {
console.log('客户端版本信息 wgtinfo', wgtinfo)
console.log('服务端版本信息 res', res)
// 客户端版本
let client_version = wgtinfo.version
//------------------------前两位大版本有改动-----------------------------------------------
// 大版本是否一致 版本号1.1.0,前两位如果有改动,就是大版本更新;否则就是小版本热更新
const flag_update =
client_version.split('.').splice(0, 2).join('.') !=
res.data.version.split('.').splice(0, 2).join('.')
//------------------------前两位大版本有改动-----------------------------------------------
//------------------------第三位小版本有改动-热更新-----------------------------------------------
// 客户端版本号
let client_version_number = Number(client_version.split('.')[2])
// 服务器版本号
let current_version_number = Number(res.data.version.split('.')[2])
// 如果客户端版本号小于服务器版本号,则提醒用户更新
const flag_hot = !flag_update && client_version_number < current_version_number
//------------------------第三位小版本有改动-热更新-----------------------------------------------
console.log('客户端版本 client_version', client_version)
console.log('客户端版本号 client_version_number', client_version_number)
console.log('服务器版本号 current_version_number', current_version_number)
console.log('升级更新 flag_update', flag_update)
console.log('热更新 flag_hot', flag_hot)
if (flag_update) {
console.log('*****开始大版本更新*****')
// 提醒用户更新
uni.showModal({
title: '更新提示',
content: res.data.note,
success: (showResult) => {
if (showResult.confirm) {
plus.nativeUI.toast('正在准备环境,请稍后!')
console.log(res.data.url)
const downloadTask = plus.downloader.createDownload(
res.data.url,
{
method: 'GET',
filename: '_doc/update/',
},
function (d, status) {
if (status == 200) {
const path = d.filename; //下载apk
plus.runtime.install(path) // 自动安装apk文件
} else {
plus.nativeUI.alert('版本更新失败:' + status)
}
},
)
downloadTask.start()
}
},
})
} else if (flag_hot) {
console.log('*****开始小版本热更新*****')
uni.downloadFile({
url: res.data.wgtUrl,
success: (downloadResult) => {
console.log("热更新downloadResult",downloadResult.tempFilePath)
if (downloadResult.statusCode === 200) {
plus.nativeUI.toast(`正在热更新!${res.data.versionCode}`)
plus.runtime.install(
downloadResult.tempFilePath,
{
force: false,
},
function () {
plus.nativeUI.toast('热更新成功')
plus.runtime.restart()
},
function (e) {
console.log(e)
plus.nativeUI.toast(`热更新失败:${e.message}`)
},
)
}
},
})
}
})
},
})
}

View File

@@ -0,0 +1,179 @@
import {pcaa as REGION_DATA} from "./pcaUtils";
/**
* Area 属性all的类型
*/
interface PlainPca {
id: string;
text: string;
pid: string;
index: Number;
}
/**
* 省市区工具类 -解决列表省市区组件的翻译问题
*/
class Area {
all: PlainPca[];
/**
* 构造器
* @param pcaa
*/
constructor(pcaa?) {
if (!pcaa) {
pcaa = REGION_DATA;
}
let arr: PlainPca[] = [];
const province = pcaa['86'];
Object.keys(province).map((key) => {
arr.push({ id: key, text: province[key], pid: '86', index: 1 });
const city = pcaa[key];
Object.keys(city).map((key2) => {
arr.push({ id: key2, text: city[key2], pid: key, index: 2 });
const qu = pcaa[key2];
if (qu) {
Object.keys(qu).map((key3) => {
arr.push({ id: key3, text: qu[key3], pid: key2, index: 3 });
});
}
});
});
this.all = arr;
}
getPca() {
return this.all;
}
getCode(text) {
if (!text || text.length == 0) {
return '';
}
for (let item of this.all) {
if (item.text === text) {
return item.id;
}
}
}
//update-begin-author:liusq---date:20230404--for: [issue/382]省市区组件JAreaLinkage数据不回显---
getText(code,index=3) {
if (!code || code.length == 0) {
return '';
}
let arr = [];
this.getAreaBycode(code, arr, index);
return arr.join('/');
}
//update-end-author:liusq---date:20230404--for: [issue/382]省市区组件JAreaLinkage数据不回显---
getRealCode(code) {
let arr = [];
this.getPcode(code, arr, 3);
return arr;
}
getPcode(id, arr, index) {
for (let item of this.all) {
if (item.id === id && item.index == index) {
arr.unshift(id);
if (item.pid != '86') {
this.getPcode(item.pid, arr, --index);
}
}
}
}
getAreaBycode(code, arr, index) {
for (let item of this.all) {
if (item.id === code && item.index == index) {
arr.unshift(item.text);
if (item.pid != '86') {
this.getAreaBycode(item.pid, arr, --index);
}
}
}
}
}
interface RegionItem {
id: string;
text: string;
pid: string;
index: number;
}
interface TransformedItem {
label: string;
value: string;
}
type TransformedData = Record<string, TransformedItem[]>;
export function transformRegionData(originalData: RegionItem[]): TransformedData {
const result: TransformedData = {};
// 首先处理省级数据 (pid 为 '86' 的项)
const provinces = originalData.filter(item => item.pid === '86');
result['0'] = provinces.map(province => ({
label: province.text,
value: province.id
}));
// 然后处理市级数据
const cities = originalData.filter(item =>
provinces.some(province => province.id === item.pid)
);
cities.forEach(city => {
if (!result[city.pid]) {
result[city.pid] = [];
}
result[city.pid].push({
label: city.text,
value: city.id
});
});
// 最后处理区县级数据
const districts = originalData.filter(item =>
cities.some(city => city.id === item.pid)
);
districts.forEach(district => {
if (!result[district.pid]) {
result[district.pid] = [];
}
result[district.pid].push({
label: district.text,
value: district.id
});
});
return result;
}
const jeecgAreaData = new Area();
// 根据code找文本
const getAreaTextByCode = function (code) {
let index = 3;
//update-begin-author:liusq---date:20220531--for: 判断code是否是多code逗号分割的字符串是的话获取最后一位的code ---
if (code && code.includes(',')) {
index = code.split(",").length;
code = code.substr(code.lastIndexOf(',') + 1);
}
//update-end-author:liusq---date:20220531--for: 判断code是否是多code逗号分割的字符串是的话获取最后一位的code ---
return jeecgAreaData.getText(code,index);
};
// 根据code找文本
const getAreaArrByCode = function (code) {
return jeecgAreaData.getRealCode(code);
};
// 获取下拉地图option
const getPcaOptionData = function () {
let pca:any = jeecgAreaData.getPca();
return transformRegionData(pca);
};
export { getAreaTextByCode,getAreaArrByCode,getPcaOptionData };

View File

@@ -0,0 +1,57 @@
import {areaList} from '@vant/area-data'
// 扁平化的省市区数据
export const pcaa = freezeDeep(usePlatPcaaData())
/**
* 获取扁平化的省市区数据
*/
function usePlatPcaaData() {
const {city_list: city, county_list: county, province_list: province} = areaList;
const dataMap = new Map<string, Recordable>()
const flatData: Recordable = {'86': province}
// 省
Object.keys(province).forEach((code) => {
flatData[code] = {}
dataMap.set(code.slice(0, 2), flatData[code])
})
// 市区
Object.keys(city).forEach((code) => {
flatData[code] = {}
dataMap.set(code.slice(0, 4), flatData[code])
// 填充上一级
const getProvince = dataMap.get(code.slice(0, 2))
if (getProvince) {
getProvince[code] = city[code]
}
});
// 县
Object.keys(county).forEach((code) => {
// 填充上一级
const getCity = dataMap.get(code.slice(0, 4))
if (getCity) {
getCity[code] = county[code]
}
});
return flatData
}
/**
*
* 深度冻结对象
* @param obj Object or Array
*/
export function freezeDeep(obj: Recordable | Recordable[]) {
if (obj != null) {
if (Array.isArray(obj)) {
obj.forEach(item => freezeDeep(item))
} else if (typeof obj === 'object') {
Object.values(obj).forEach(value => {
freezeDeep(value)
})
}
Object.freeze(obj)
}
return obj
}

View File

@@ -0,0 +1,140 @@
export const ACCESS_TOKEN = 'Access-Token'
export const USER_NAME = 'login_username'
export const USER_INFO = 'login_user_info'
export const NAV_BAR_COLOR = 'bg-gradual-blue'
export const APP_ROUTE = 'app_route_list'
export const APP_CONFIG = 'app_config'
export const X_TENANT_ID = 'X-Tenant-Id'
export const X_Low_App_ID = 'X-Low-App-ID'
export const TENANT_LIST = 'tenant_list'
export const ROUTE_PARAMS = "cacheRouteParams"
export const HOME_PAGE = "/pages/message/message"
/**
* 组件名称前缀
*/
export const COMP_NAME_PREFIX = 'jeecg-drag';
//首页配置项缓存时间10分钟
export const HOME_CONFIG_EXPIRED_TIME = 10*60
export const phone = '---'
export const email = '---'
export const company = '---'
const STORAGE_OPTIONS = {
namespace: 'pro__', // key prefix
name: 'ls', // name variable Vue.[ls] or this.[$ls],
storage: 'local', // storage name session, local, memory
}
export default STORAGE_OPTIONS;
//类型条件
export const conditionObj = {
input:[{label:"包含",value:"like"},{label:"以...开始",value:"right_like"},{label:"以...结尾",value:"left_like"},{label:"在...中",value:"in"}],
number:[{label:"大于",value:"gt"},{label:"大于等于",value:"ge"},{label:"小于",value:"lt"},{label:"小于等于",value:"le"}],
date:[{label:"大于",value:"gt"},{label:"大于等于",value:"ge"},{label:"小于",value:"lt"},{label:"小于等于",value:"le"}],
select:[],
checkbox:[{label:"多词匹配",value:"elemMatch"}],
}
/**
* 颜色板
* classic经典
* technology科技
* business商务
* botany植物
* natural自然
* colour彩色
*/
export const colorPanel = {
classic:["#64b5f6","#4db6ac","#ffb74d","#e57373","#9575cd","#a1887f","#90a4ae","#4dd0e1","#81c784","#ff8a65"],
technology:["#3a5b84","#4d6e98","#7594b9","#bfd7f2","#18619f","#408aca","#5ea8e9","#81c3fc","#71a5cb","#a1cae4"],
business:["#ccedf7","#b9dcf0","#12a0e7","#0663a4","#458890","#97d9cd","#4bb8bf","#20899c","#f44336 ","#a2c7d9"],
botany:["#34b392","#4ac2a6","#8ed1c0","#ccdec6","#61bdb5","#7993a1","#93a889","#5e8d83","#115040","#bcc5b4"],
natural:["#85cacd","#a7d676","#fee159","#fbc78e","#ef918b","#a9b5ff","#e7daca","#fc803a","#fea1ac","#c2a3cd"],
colour:["#fddb9c","#f9ae91","#f59193","#d47f97","#bd86a6","#f595a1","#624772","#fe7156","#ffbda3","#877fa8"]
};
//所有条件
export const allCondition = [
{label:"包含",value:"like"},
{label:"以...开始",value:"right_like"},
{label:"以...结尾",value:"left_like"},
{label:"在...中",value:"in"},
{label:"大于",value:"gt"},
{label:"大于等于",value:"ge"},
{label:"小于",value:"lt"},
{label:"小于等于",value:"le"},
{label:"多词匹配",value:"elemMatch"},
{label:"等于",value:"eq"},
{label:"不等于",value:"ne"},
{label:"为空",value:"empty"},
{label:"不为空",value:"not_empty"}
]
//仪表盘组件
export const compList = [
"JBar",
"JStackBar",
"JMultipleBar",
"JNegativeBar",
"JProgress",
"JLine",
"JMultipleLine",
"DoubleLineBar",
"JPie",
"JRing",
"JFunnel",
"JPyramidFunnel",
"JRadar",
"JCircleRadar",
"JGauge",
"JColorGauge",
"JScatter",
"JBubble",
"JDragEditor",
"JCarousel",
"JIframe",
"JNumber",
"JCustomButton",
"JPivotTable",
"JBubbleMap",
"JBarMap",
"JHeatMap",
];
//不包含操作的组件
export const noActionList = [
"JCustomButton",
"JIframe",
"JCarousel",
"JDragEditor",
];
//系统字段
export const systemFields = [{
dataIndex:"create_time",
key:"create_time",
title:"创建时间",
type:'date',
width:200
},{
dataIndex:"create_by",
key:"create_by",
title:"创建人",
width:150
},{
dataIndex:"update_time",
key:"update_time",
title:"修改时间",
type:'date',
width:200
},{
dataIndex:"update_by",
key:"update_by",
title:"修改人",
width:150
}]

View File

@@ -0,0 +1,108 @@
const toString = Object.prototype.toString
export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`
}
export function isDef<T = unknown>(val?: T): val is T {
return typeof val !== 'undefined'
}
export function isUnDef<T = unknown>(val?: T): val is T {
return !isDef(val)
}
export function isObject(val: any): val is Record<any, any> {
return val !== null && is(val, 'Object')
}
export function isEmpty<T = unknown>(val: T): val is T {
if (isArray(val) || isString(val)) {
return val.length === 0
}
if (val instanceof Map || val instanceof Set) {
return val.size === 0
}
if (isObject(val)) {
return Object.keys(val).length === 0
}
return false
}
export function isDate(val: unknown): val is Date {
return is(val, 'Date')
}
export function isNull(val: unknown): val is null {
return val === null
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val)
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val)
}
export function isNumber(val: unknown): val is number {
return is(val, 'Number')
}
export function isPromise<T = any>(val: any): val is Promise<T> {
// update-begin--author:sunjianlei---date:20211022---for: 不能既是 Promise 又是 Object --------
return is(val, 'Promise') && isFunction(val.then) && isFunction(val.catch)
// update-end--author:sunjianlei---date:20211022---for: 不能既是 Promise 又是 Object --------
}
export function isString(val: unknown): val is string {
return is(val, 'String')
}
export function isJsonObjectString(val: string): val is string {
if (!val) {
return false
}
return val.startsWith('{') && val.endsWith('}')
}
export function isFunction(val: unknown): val is Function {
return typeof val === 'function'
}
export function isBoolean(val: unknown): val is boolean {
return is(val, 'Boolean')
}
export function isRegExp(val: unknown): val is RegExp {
return is(val, 'RegExp')
}
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val)
}
export function isWindow(val: any): val is Window {
return typeof window !== 'undefined' && is(val, 'Window')
}
export function isElement(val: unknown): val is Element {
return isObject(val) && !!val.tagName
}
export function isMap(val: unknown): val is Map<any, any> {
return is(val, 'Map')
}
export const isServer = typeof window === 'undefined'
export const isClient = !isServer
export function isUrl(path: string): boolean {
const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}

View File

@@ -0,0 +1,18 @@
//获取地图
export default function MapLoader() {
return new Promise((resolve, reject) => {
if (window.AMap) {
resolve(window.AMap);
} else {
var script = document.createElement('script');
script.type = "text/javascript";
script.async = true;
script.src = "https://webapi.amap.com/maps?v=1.4.15&key=21f194a0d33197f874f7bbdd198419be&callback=initAMap";
script.onerror = reject;
document.head.appendChild(script);
}
window.initAMap = () => {
resolve(window.AMap);
};
})
}

View File

@@ -0,0 +1,418 @@
function AMapWX(a) {
;(this.key = a.key),
(this.requestConfig = {
key: a.key,
s: 'rsx',
platform: 'WXJS',
appname: a.key,
sdkversion: '1.2.0',
logversion: '2.0',
})
}
;(AMapWX.prototype.getWxLocation = function (a, b) {
wx.getLocation({
type: 'gcj02',
success: function (a) {
var c = a.longitude + ',' + a.latitude
wx.setStorage({ key: 'userLocation', data: c }), b(c)
},
fail: function (c) {
wx.getStorage({
key: 'userLocation',
success: function (a) {
a.data && b(a.data)
},
}),
a.fail({ errCode: '0', errMsg: c.errMsg || '' })
},
})
}),
(AMapWX.prototype.getRegeo = function (a) {
function c(c) {
var d = b.requestConfig
wx.request({
url: 'https://restapi.amap.com/v3/geocode/regeo',
data: {
key: b.key,
location: c,
extensions: 'all',
s: d.s,
platform: d.platform,
appname: b.key,
sdkversion: d.sdkversion,
logversion: d.logversion,
},
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
var d, e, f, g, h, i, j, k, l
b.data.status && '1' == b.data.status
? ((d = b.data.regeocode),
(e = d.addressComponent),
(f = []),
(g = ''),
d && d.roads[0] && d.roads[0].name && (g = d.roads[0].name + '附近'),
(h = c.split(',')[0]),
(i = c.split(',')[1]),
d.pois &&
d.pois[0] &&
((g = d.pois[0].name + '附近'),
(j = d.pois[0].location),
j && ((h = parseFloat(j.split(',')[0])), (i = parseFloat(j.split(',')[1])))),
e.provice && f.push(e.provice),
e.city && f.push(e.city),
e.district && f.push(e.district),
e.streetNumber && e.streetNumber.street && e.streetNumber.number
? (f.push(e.streetNumber.street), f.push(e.streetNumber.number))
: ((k = ''),
d && d.roads[0] && d.roads[0].name && (k = d.roads[0].name),
f.push(k)),
(f = f.join('')),
(l = [
{
iconPath: a.iconPath,
width: a.iconWidth,
height: a.iconHeight,
name: f,
desc: g,
longitude: h,
latitude: i,
id: 0,
regeocodeData: d,
},
]),
a.success(l))
: a.fail({ errCode: b.data.infocode, errMsg: b.data.info })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}
var b = this
a.location
? c(a.location)
: b.getWxLocation(a, function (a) {
c(a)
})
}),
(AMapWX.prototype.getWeather = function (a) {
function d(d) {
var e = 'base'
a.type && 'forecast' == a.type && (e = 'all'),
wx.request({
url: 'https://restapi.amap.com/v3/weather/weatherInfo',
data: {
key: b.key,
city: d,
extensions: e,
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
},
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
function c(a) {
var b = {
city: { text: '城市', data: a.city },
weather: { text: '天气', data: a.weather },
temperature: { text: '温度', data: a.temperature },
winddirection: { text: '风向', data: a.winddirection + '风' },
windpower: { text: '风力', data: a.windpower + '级' },
humidity: { text: '湿度', data: a.humidity + '%' },
}
return b
}
var d, e
b.data.status && '1' == b.data.status
? b.data.lives
? ((d = b.data.lives),
d && d.length > 0 && ((d = d[0]), (e = c(d)), (e['liveData'] = d), a.success(e)))
: b.data.forecasts &&
b.data.forecasts[0] &&
a.success({ forecast: b.data.forecasts[0] })
: a.fail({ errCode: b.data.infocode, errMsg: b.data.info })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}
function e(e) {
wx.request({
url: 'https://restapi.amap.com/v3/geocode/regeo',
data: {
key: b.key,
location: e,
extensions: 'all',
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
},
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
var c, e
b.data.status && '1' == b.data.status
? ((e = b.data.regeocode),
e.addressComponent
? (c = e.addressComponent.adcode)
: e.aois && e.aois.length > 0 && (c = e.aois[0].adcode),
d(c))
: a.fail({ errCode: b.data.infocode, errMsg: b.data.info })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}
var b = this,
c = b.requestConfig
a.city
? d(a.city)
: b.getWxLocation(a, function (a) {
e(a)
})
}),
(AMapWX.prototype.getPoiAround = function (a) {
function d(d) {
var e = {
key: b.key,
location: d,
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
}
a.querytypes && (e['types'] = a.querytypes),
a.querykeywords && (e['keywords'] = a.querykeywords),
wx.request({
url: 'https://restapi.amap.com/v3/place/around',
data: e,
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
var c, d, e, f
if (b.data.status && '1' == b.data.status) {
if (((b = b.data), b && b.pois)) {
for (c = [], d = 0; d < b.pois.length; d++)
(e = 0 == d ? a.iconPathSelected : a.iconPath),
c.push({
latitude: parseFloat(b.pois[d].location.split(',')[1]),
longitude: parseFloat(b.pois[d].location.split(',')[0]),
iconPath: e,
width: 22,
height: 32,
id: d,
name: b.pois[d].name,
address: b.pois[d].address,
})
;(f = { markers: c, poisData: b.pois }), a.success(f)
}
} else a.fail({ errCode: b.data.infocode, errMsg: b.data.info })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}
var b = this,
c = b.requestConfig
a.location
? d(a.location)
: b.getWxLocation(a, function (a) {
d(a)
})
}),
(AMapWX.prototype.getStaticmap = function (a) {
function f(b) {
c.push('location=' + b),
a.zoom && c.push('zoom=' + a.zoom),
a.size && c.push('size=' + a.size),
a.scale && c.push('scale=' + a.scale),
a.markers && c.push('markers=' + a.markers),
a.labels && c.push('labels=' + a.labels),
a.paths && c.push('paths=' + a.paths),
a.traffic && c.push('traffic=' + a.traffic)
var e = d + c.join('&')
a.success({ url: e })
}
var e,
b = this,
c = [],
d = 'https://restapi.amap.com/v3/staticmap?'
c.push('key=' + b.key),
(e = b.requestConfig),
c.push('s=' + e.s),
c.push('platform=' + e.platform),
c.push('appname=' + e.appname),
c.push('sdkversion=' + e.sdkversion),
c.push('logversion=' + e.logversion),
a.location
? f(a.location)
: b.getWxLocation(a, function (a) {
f(a)
})
}),
(AMapWX.prototype.getInputtips = function (a) {
var b = this,
c = b.requestConfig,
d = {
key: b.key,
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
}
a.location && (d['location'] = a.location),
a.keywords && (d['keywords'] = a.keywords),
a.type && (d['type'] = a.type),
a.city && (d['city'] = a.city),
a.citylimit && (d['citylimit'] = a.citylimit),
wx.request({
url: 'https://restapi.amap.com/v3/assistant/inputtips',
data: d,
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
b && b.data && b.data.tips && a.success({ tips: b.data.tips })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}),
(AMapWX.prototype.getDrivingRoute = function (a) {
var b = this,
c = b.requestConfig,
d = {
key: b.key,
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
}
a.origin && (d['origin'] = a.origin),
a.destination && (d['destination'] = a.destination),
a.strategy && (d['strategy'] = a.strategy),
a.waypoints && (d['waypoints'] = a.waypoints),
a.avoidpolygons && (d['avoidpolygons'] = a.avoidpolygons),
a.avoidroad && (d['avoidroad'] = a.avoidroad),
wx.request({
url: 'https://restapi.amap.com/v3/direction/driving',
data: d,
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
b &&
b.data &&
b.data.route &&
a.success({ paths: b.data.route.paths, taxi_cost: b.data.route.taxi_cost || '' })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}),
(AMapWX.prototype.getWalkingRoute = function (a) {
var b = this,
c = b.requestConfig,
d = {
key: b.key,
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
}
a.origin && (d['origin'] = a.origin),
a.destination && (d['destination'] = a.destination),
wx.request({
url: 'https://restapi.amap.com/v3/direction/walking',
data: d,
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
b && b.data && b.data.route && a.success({ paths: b.data.route.paths })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}),
(AMapWX.prototype.getTransitRoute = function (a) {
var b = this,
c = b.requestConfig,
d = {
key: b.key,
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
}
a.origin && (d['origin'] = a.origin),
a.destination && (d['destination'] = a.destination),
a.strategy && (d['strategy'] = a.strategy),
a.city && (d['city'] = a.city),
a.cityd && (d['cityd'] = a.cityd),
wx.request({
url: 'https://restapi.amap.com/v3/direction/transit/integrated',
data: d,
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
if (b && b.data && b.data.route) {
var c = b.data.route
a.success({
distance: c.distance || '',
taxi_cost: c.taxi_cost || '',
transits: c.transits,
})
}
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
}),
(AMapWX.prototype.getRidingRoute = function (a) {
var b = this,
c = b.requestConfig,
d = {
key: b.key,
s: c.s,
platform: c.platform,
appname: b.key,
sdkversion: c.sdkversion,
logversion: c.logversion,
}
a.origin && (d['origin'] = a.origin),
a.destination && (d['destination'] = a.destination),
wx.request({
url: 'https://restapi.amap.com/v4/direction/bicycling',
data: d,
method: 'GET',
header: { 'content-type': 'application/json' },
success: function (b) {
b && b.data && b.data.data && a.success({ paths: b.data.data.paths })
},
fail: function (b) {
a.fail({ errCode: '0', errMsg: b.errMsg || '' })
},
})
})
// 修改导出方式
const amap = {
AMapWX: AMapWX
};
// ES6 导出方式
export default amap;

View File

@@ -0,0 +1,63 @@
import { h } from 'vue'
import { filterMultiDictText, getFileAccessHttpUrl } from '@/common/uitls'
const render = {
/**
* 渲染图片
* @param text
*/
renderImage: ({ text }) => {
if (!text) {
return ;
}
let avatarList = text.split(',')
return h(
'span',
avatarList.map((item) => {
return h(Image, {
src: getFileAccessHttpUrl(item),
width: 30,
height: 30,
style: { marginRight: '5px' },
})
}),
)
//update-end-author:taoyan date:2022-5-24 for: VUEN-1084 【vue3】online表单测试发现的新问题 41、生成的代码树默认图大小未改
},
/**
* 渲染a标签
* @param text
*/
renderHref: ({ text }) => {
if (!text) {
return ''
}
const len = 20
if (text.length > len) {
text = text.substr(0, len)
}
return h('a', { href: text, target: '_blank' }, text)
},
/**
* 根据字典渲染
* @param v
* @param array
*/
renderDictNative: (v, array, renderTag = false) => {
let text = ''
let color = ''
let obj = array.filter((item) => {
return item.value == v
})
if (obj.length > 0) {
text = obj[0].label
color = obj[0].color
}
return h('span', text)
},
renderSwitch: (text, arr) => {
return text ? filterMultiDictText(arr, text) : ''
},
}
export { render }

View File

@@ -0,0 +1,166 @@
// @ts-nocheck
import md5 from 'md5'
import { useUserStore } from '@/store/user'
const baseUrl = import.meta.env.VITE_SERVER_BASEURL
class socket {
constructor() {
this.socketUrl = baseUrl
this.socketStart = false
this.socketType = 'websocket'
this._heartbeatTimer = null
this.monitorSocketError()
this.monitorSocketClose()
this.socketReceive()
}
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】系统 WebSocket 地址与 PC 一致-----------
/** 构建系统 WebSocket 地址:/websocket/{userId}_{md5(token)} */
buildSystemWebSocketUrl() {
const userStore = useUserStore()
const userId = userStore.userInfo.userid
const token = userStore.userInfo.token || ''
if (!userId || !this.socketUrl) {
return ''
}
const wsClientId = md5(token)
const base = this.socketUrl.replace('https://', 'wss://').replace('http://', 'ws://').replace(/\/$/, '')
return `${base}/websocket/${userId}_${wsClientId}`
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】系统 WebSocket 地址与 PC 一致-----------
init(socket_type, callback?) {
const userStore = useUserStore()
if (!baseUrl) {
console.log('config/baseUrl socketUrl为空')
return
}
if (this.socketStart) {
console.log('webSocket已经启动了')
callback && callback()
return
}
this.socketType = socket_type || 'websocket'
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】连接原生 /websocket-----------
let url = this.buildSystemWebSocketUrl()
if (this.socketType !== 'websocket') {
// 兼容旧路径(如历史业务仍传入自定义 type
url =
this.socketUrl.replace('https://', 'wss://').replace('http://', 'ws://') +
'/' +
this.socketType +
'/' +
userStore.userInfo.userid +
'_app'
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】连接原生 /websocket-----------
if (!url) {
console.log('WebSocket url 为空,跳过连接')
return
}
console.log('启动this.socketUrl连接地址', url)
const token = userStore.userInfo.token
uni.connectSocket({
url: url,
method: 'GET',
protocols: [token],
})
uni.onSocketOpen((res) => {
this.socketStart = true
callback && callback()
console.log('WebSocket连接已打开')
this.startHeartbeat()
})
}
// Socket给服务器发送消息
send(data, callback) {
const userStore = useUserStore()
if (userStore.userInfo.userid && typeof data === 'object' && data !== null) {
data.userUid = userStore.userInfo.userid
}
const payload = typeof data === 'string' ? data : JSON.stringify(data)
uni.sendSocketMessage({
data: payload,
success: () => {
callback && callback(true)
},
fail: () => {
callback && callback(false)
},
})
}
acceptMessage(msg) {
console.log(msg)
}
// Socket接收服务器发送过来的消息
socketReceive() {
uni.onSocketMessage((res) => {
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】忽略心跳并解析 JSON-----------
if (res.data === 'ping' || res.data === 'pong') {
return
}
let data
try {
data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
} catch (e) {
console.log('WebSocket 非 JSON 消息:', res.data)
return
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】忽略心跳并解析 JSON-----------
this.acceptMessage && this.acceptMessage(data)
})
}
// 关闭Socket
closeSocket() {
this.stopHeartbeat()
uni.closeSocket()
this.socketStart = false
}
// 监听Socket关闭
monitorSocketClose() {
uni.onSocketClose((res) => {
console.log('WebSocket 已关闭!')
this.socketStart = false
this.stopHeartbeat()
setTimeout(() => {
this.init(this.socketType)
}, 3000)
})
}
// 监听Socket错误
monitorSocketError() {
uni.onSocketError((res) => {
this.socketStart = false
console.log('WebSocket连接打开失败请检查')
})
}
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】心跳 ping-----------
startHeartbeat() {
this.stopHeartbeat()
this._heartbeatTimer = setInterval(() => {
if (!this.socketStart) {
return
}
this.send('ping')
}, 55000)
}
stopHeartbeat() {
if (this._heartbeatTimer) {
clearInterval(this._heartbeatTimer)
this._heartbeatTimer = null
}
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】心跳 ping-----------
}
const mySocket = new socket()
export default mySocket

View File

@@ -0,0 +1,132 @@
/**
* 提示与加载工具类
*/
export default class Tips {
// 声明 isLoading 属性
static isLoading: boolean
// 移除无用的构造函数,因为静态属性不需要在构造函数中初始化
/**
* 弹出提示框
*/
static success(title, duration = 1000) {
setTimeout(() => {
uni.showToast({
title,
icon: 'success',
mask: true,
duration,
})
}, 300)
if (duration > 0) {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve()
}, duration)
})
}
}
/**
* 弹出确认窗口
*/
static confirm(text, showCancel, payload = {}, title = '提示') {
return new Promise((resolve, reject) => {
uni.showModal({
title,
content: text,
showCancel,
success: (res) => {
if (res.confirm) {
resolve(payload)
} else if (res.cancel) {
reject(payload)
}
},
fail: (res) => {
reject(payload)
},
})
})
}
static toast(title, onHide, icon = 'none') {
setTimeout(() => {
uni.showToast({
title,
// 确保 icon 的值是合法的
icon: icon as 'success' | 'loading' | 'error' | 'none' | 'fail' | 'exception',
mask: true,
duration: 1000,
})
}, 300)
// 隐藏结束回调
if (onHide) {
setTimeout(() => {
onHide()
}, 500)
}
}
/**
* 警告框
*/
static alert(title) {
uni.showToast({
title,
image: '/static/alert.png',
mask: true,
duration: 1500,
})
}
/**
* 错误框
*/
static error(title, onHide) {
uni.showToast({
title,
image: '/static/error.png',
mask: true,
duration: 1500,
})
// 隐藏结束回调
if (onHide) {
setTimeout(() => {
onHide()
}, 500)
}
}
/**
* 弹出加载提示
*/
static loading(title = '加载中') {
if (Tips.isLoading) {
return
}
Tips.isLoading = true
uni.showLoading({
title,
mask: true,
})
}
/**
* 加载完毕
*/
static loaded() {
if (Tips.isLoading) {
Tips.isLoading = false
uni.hideLoading()
}
}
}
/**
* 静态变量,是否加载中
*/
Tips.isLoading = false

View File

@@ -0,0 +1,756 @@
import pagesJson from '../pages.json'
// 引入uni-parse-pages
import pagesJsonToRoutes from 'uni-parse-pages'
import { colorPanel } from './constants'
import tip from './tip'
import { platform, isH5, isApp, isMp, isMpWeixin, isMpAplipay, isMpToutiao } from '@/utils/platform'
import { isString } from './is'
/**
* 缓存,默认有效期2小时
* @param key 缓存key
* @param value 缓存值
* @param seconds 缓存时间(秒)
* @returns {*}
*/
export function cache(key, value = null, seconds = 2 * 3600) {
const timestamp = +new Date() / 1000
if (key && value === null) {
// 获取缓存
const val = uni.getStorageSync(key)
if (val && val.length > 0) {
const tmp = val.split('|')
if (!tmp[2] || timestamp >= tmp[2]) {
console.log('key已失效')
// 删除缓存
uni.removeStorageSync(key)
return ''
} else {
console.log('key未失效')
if (tmp[1] == 'json') {
return JSON.parse(tmp[0])
}
return tmp[0]
}
}
} else if (key && value) {
// 设置缓存
const expire = timestamp + seconds
console.log('typeof value', typeof value)
if (typeof value === 'object') {
value = JSON.stringify(value) + '|json|' + expire
} else {
value = value + '|string|' + expire
}
uni.setStorageSync(key, value)
} else {
console.log('key不能空')
}
}
// 获取静态文件地址
export const getStaticDomainURL = () => {
return import.meta.env.VITE_SERVER_BASEURL + '/sys/common/static'
}
export const getFileAccessHttpUrl = function (avatar, subStr?) {
if (!avatar) return ''
if (!subStr) subStr = 'http'
if (avatar) {
avatar = avatar.replace(/user_imgs\\/, 'user_imgs/')
}
if (avatar && avatar.startsWith(subStr)) {
return avatar
} else {
return getStaticDomainURL() + '/' + avatar
}
}
interface hasRouteType {
name?: string
path?: string
routeList?: any
}
// 判断路由是否存在
export const hasRoute = ({ name, path, routeList }: hasRouteType) => {
routeList = routeList ?? pagesJsonToRoutes(pagesJson)
if (path) {
return !!routeList.find((item) => item.path === path)
}
if (name) {
return !!routeList.find((item) => item.path.split('/').pop() === name)
}
}
/**
* 人性化显示时间
*
* @param {Object} datetime
*/
export function beautifyTime(datetime = '') {
if (datetime == null) {
return ''
}
datetime = datetime.toString().replace(/-/g, '/')
const time = new Date()
let outTime = new Date(datetime)
if (/^[1-9]\d*$/.test(datetime)) {
outTime = new Date(parseInt(datetime))
}
if (time.getTime() < outTime.getTime()) {
return parseTime(outTime, '{y}/{m}/{d}')
}
if (time.getFullYear() != outTime.getFullYear()) {
return parseTime(outTime, '{y}/{m}/{d}')
}
if (time.getMonth() != outTime.getMonth()) {
return parseTime(outTime, '{m}/{d}')
}
if (time.getDate() != outTime.getDate()) {
const day = outTime.getDate() - time.getDate()
if (day == -1) {
return parseTime(outTime, '昨天 {h}:{i}')
}
if (day == -2) {
return parseTime(outTime, '前天 {h}:{i}')
}
return parseTime(outTime, '{m}-{d}')
}
if (time.getHours() != outTime.getHours()) {
return parseTime(outTime, '{h}:{i}')
}
let minutes = outTime.getMinutes() - time.getMinutes()
if (minutes == 0) {
return '刚刚'
}
minutes = Math.abs(minutes)
return `${minutes}分钟前`
}
/**
* 格式化时间
* @param {Object} time
* @param {Object} cFormat
*/
export function parseTime(time, cFormat) {
if (arguments.length === 0) {
return null
}
let date
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
if (typeof time === 'object') {
date = time
} else {
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
time = parseInt(time)
} else {
time = new Date(time)
}
date = new Date(time.toString().replace(/-/g, '/'))
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay(),
}
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value]
}
return value.toString().padStart(2, '0')
})
return time_str
}
/**
* 随机生成字符串
* @param length 字符串的长度
* @param chats 可选字符串区间(只会生成传入的字符串中的字符)
* @return string 生成的字符串
*/
export function randomString(length, chats?) {
if (!length) length = 1
if (!chats) chats = '0123456789qwertyuioplkjhgfdsazxcvbnm'
let str = ''
for (let i = 0; i < length; i++) {
// @ts-ignore
const num = randomNumber(0, chats.length - 1)
str += chats[num]
}
return str
}
/**
* 随机生成数字
*
* 示例:生成长度为 12 的随机数randomNumber(12)
* 示例:生成 3~23 之间的随机数randomNumber(3, 23)
*
* @param1 最小值 | 长度
* @param2 最大值
* @return int 生成后的数字
*/
export function randomNumber() {
// 生成 最小值 到 最大值 区间的随机数
const random = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min)
}
if (arguments.length === 1) {
// @ts-ignore
const [length] = arguments
// 生成指定长度的随机数字,首位一定不是 0
// @ts-ignore
const nums = [...Array(length).keys()].map((i) => (i > 0 ? random(0, 9) : random(1, 9)))
return parseInt(nums.join(''))
} else if (arguments.length >= 2) {
// @ts-ignore
const [min, max] = arguments
return random(min, max)
} else {
return Number.NaN
}
}
/**
* 时间格式化
* @param value
* @param fmt
* @returns {*}
*/
export function formatDate(value, fmt) {
const regPos = /^\d+(\.\d+)?$/
if (regPos.test(value)) {
// 如果是数字
const getDate = new Date(value)
const o = {
'M+': getDate.getMonth() + 1,
'd+': getDate.getDate(),
'h+': getDate.getHours(),
'H+': getDate.getHours(),
'm+': getDate.getMinutes(),
's+': getDate.getSeconds(),
'q+': Math.floor((getDate.getMonth() + 3) / 3),
S: getDate.getMilliseconds(),
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (getDate.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length),
)
}
}
return fmt
} else {
// TODO
if (value && value.length > 0) {
value = value.trim()
return value.substr(0, fmt.length)
}
return value
}
}
// 通过时间或者时间戳获取对应antd的年、月、周、季度。
export function getWeekMonthQuarterYear(date) {
// 获取 ISO 周数的函数
const getISOWeek = (date) => {
const jan4 = new Date(date.getFullYear(), 0, 4)
const oneDay = 86400000 // 一天的毫秒数
return Math.ceil(((date - jan4.getTime()) / oneDay + jan4.getDay() + 1) / 7)
}
// 将时间戳转换为日期对象
const dateObj = new Date(date)
// 计算周
const week = getISOWeek(dateObj)
// 计算月
const month = dateObj.getMonth() + 1 // 月份是从0开始的所以要加1
// 计算季度
const quarter = Math.floor(dateObj.getMonth() / 3) + 1
// 计算年
const year = dateObj.getFullYear()
return {
year: `${year}`,
month: `${year}-${month.toString().padStart(2, '0')}`,
week: `${year}-${week}`,
quarter: `${year}-Q${quarter}`,
}
}
// 生成 1 到 10 之间的随机整数
export function getRandomIntBetweenOneAndTen() {
return Math.floor(Math.random() * 10) + 1
}
/**
* 获取随机颜色
* @param {any} color
* 颜色板
* classic经典
* technology科技
* business商务
* botany植物
* natural自然
* colour彩色
* @return
*/
export function getRandomColor() {
const colorType = ['classic', 'technology', 'business', 'botany', 'natural', 'colour']
// 生成一个随机索引,范围是从 0 到数组长度减 1
const randomIndex = Math.floor(Math.random() * colorType.length)
// 根据随机索引从数组中获取一个随机类型
const randomColorType = colorType[randomIndex]
return colorPanel.natural[getRandomIntBetweenOneAndTen()] || '#00bcd4'
}
// 消除后缀:
export const getPlaceholder = (attrs: any = {}) => {
let label = attrs.label
if (label.endsWith('') || label.endsWith(':')) {
label = label.substr(0, label.length - 1)
}
return `请选择${label}`
}
/**
* 日期格式化
* @param text
*/
export function getFormatDate(text, column) {
if (!text) {
return ''
}
let a = text
if (a.length > 10) {
a = a.substring(0, 10)
}
let fieldExtendJson = column?.fieldExtendJson
console.log('getFormat Datetext', text)
console.log('getFormatDate fieldExtendJson', fieldExtendJson)
if (fieldExtendJson) {
fieldExtendJson = JSON.parse(fieldExtendJson)
if (fieldExtendJson.picker && fieldExtendJson.picker != 'default') {
const result = getWeekMonthQuarterYear(a)
return result[fieldExtendJson.picker]
}
}
return a
}
/**
* 字典值替换文本通用方法(多选)
* @param dictOptions 字典数组
* @param text 字典值
* @return String
*/
export function filterMultiDictText(dictOptions, text) {
// js “!text” 认为0为空所以做提前处理
if (text === 0 || text === '0') {
if (dictOptions) {
for (const dictItem of dictOptions) {
if (text == dictItem.value) {
return dictItem.text
}
}
}
}
if (!text || text == 'undefined' || text == 'null' || !dictOptions || dictOptions.length == 0) {
return ''
}
let re = ''
text = text.toString()
const arr = text.split(',')
dictOptions.forEach(function (option) {
if (option) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === option.value) {
re += option.text + ','
break
}
}
}
})
if (re == '') {
return text
}
return re.substring(0, re.length - 1)
}
/** 获取url参数
* @param {Object} url
*/
export function getQueryVariable(url) {
if (!url) return
let t
let n
let r
const i = url.split('?')[1]
const s = {}
;(t = i.split('&')), (r = null), (n = null)
for (const o in t) {
const u = t[o].indexOf('=')
u !== -1 && ((r = t[o].substr(0, u)), (n = t[o].substr(u + 1)), (s[r] = n))
}
return s
}
/**
* 是否oauth环境
*/
export function isOAuth2AppEnv() {
let isOAuthEnv = false
// #ifdef H5
isOAuthEnv = /wxwork|dingtalk/i.test(navigator.userAgent)
// #endif
return isOAuthEnv
}
/**
* 获取url中的参数
* @param url
*/
export const getUrlParams = (url) => {
const result = {
url: '',
params: {},
}
const list = url.split('?')
result.url = list[0]
const params = list[1]
if (params) {
const list = params.split('&')
list.forEach((ele) => {
const dic = ele.split('=')
const label = dic[0]
const value = dic[1]
result.params[label] = value
})
}
return result
}
/**
* 判断文件地址是否支持存在
* @param url
* @returns {Promise<any>}
*/
export function downloadAbled(url) {
return new Promise((resolve, reject) => {
let xmlHttp
if (window.XMLHttpRequest) {
xmlHttp = new XMLHttpRequest() // 其他浏览器
} else if ((window as any).ActiveXObject) {
// try {
// xmlHttp = new window.ActiveXObject("Microsoft.XMLHTTP");//IE
// }catch (e) {
// console.log('创建xmlHttp失败',e)
// }
}
if (!xmlHttp) {
reject('创建xmlHttp失败')
}
xmlHttp.open('GET', url, false)
xmlHttp.send()
if (xmlHttp.readyState == 4) {
if (xmlHttp.status == 200) {
resolve(true)
} else if (xmlHttp.status == 404) {
reject('文件不存在!')
} else {
reject('请求失败status' + xmlHttp.status)
}
} else {
reject('请求失败readyState:' + xmlHttp.readyState)
}
})
}
/**
* app获取url地址的传参问题
* @param url
* @returns {Object}
*/
export function appGetUrlParams(url) {
const theRequest = new Object()
const index = url.indexOf('?')
if (index != -1) {
const str = url.substring(index + 1)
const strs = str.split('&')
for (let i = 0; i < strs.length; i++) {
theRequest[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1])
}
}
return theRequest
}
/**
*
* @param lat1 纬度1
* @param lng1 经度1
* @param lat2 纬度2
* @param lng2 经度2
* 返回 米
*/
export function geoDistance(lng1, lat1, lng2, lat2) {
let radLat1 = rad(lat1)
let radLat2 = rad(lat2)
let a = radLat1 - radLat2
let b = rad(lng1) - rad(lng2)
let s =
2 *
Math.asin(
Math.sqrt(
Math.pow(Math.sin(a / 2), 2) +
Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2),
),
)
s = s * 6378.137 // EARTH_RADIUS;
s = Math.round(s * 10000) / 10
return s
}
//经纬度转换成三角函数中度分表形式。
function rad(d) {
return (d * Math.PI) / 180.0
}
export function downloadFile(obj) {
let url = ''
if (isMp) {
if (obj.currentTarget) {
url = encodeURI(obj.currentTarget.dataset.url)
downloadNH5(url)
} else if (isString(obj)) {
// 为字符串类型时可认为就是url
url = getFileAccessHttpUrl(obj)
downloadNH5(url)
}
} else if (isApp) {
url = encodeURI(obj)
downloadNH5(url)
} else if (isH5) {
url = getFileAccessHttpUrl(obj)
window.open(url)
}
}
/**
* 非H5文件下载地址
* @param 文件路径 url
*/
function downloadNH5(url) {
const image_arr = ['png', 'jpg', 'jpeg']
const fileType = url.split('.').pop()
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode == 200) {
let filePath = res.tempFilePath
const system = uni.getSystemInfoSync().platform
if (system == 'ios') {
filePath = encodeURI(filePath)
}
if (isMp) {
const suffix = getSuffix(url).toLowerCase()
if (image_arr.indexOf(suffix) != -1) {
// 预览图片
uni.previewImage({
urls: [filePath],
})
} else {
uni.openDocument({
filePath,
fileType,
success: (res) => {
console.log('打开文档成功')
},
fail: (res) => {
console.log('打开文档失败')
tip.error('不支持打开此格式', true)
},
})
}
} else if (isApp) {
const suffix = getSuffix(url).toLowerCase()
if (image_arr.indexOf(suffix) != -1) {
// 预览图片
uni.previewImage({
urls: [filePath],
})
} else {
uni.saveFile({
tempFilePath: filePath,
success: (res) => {
// 保存成功并打开文件
tip.success('保存成功')
uni.openDocument({
filePath: res.savedFilePath,
success: function (res) {
console.log('openDocument', res)
},
fail() {
uni.showToast({
title: '暂不支持打开此类型',
duration: 2000,
})
},
})
},
fail: () => tip.alert('保存失败'),
})
}
}
} else {
tip.alert('文件异常')
}
},
fail: (err) => {
tip.alert('下载失败')
},
})
}
// 获取后缀
export function getSuffix(text) {
if (text) {
const arr = text.split('.')
const suffix = arr[arr.length - 1]
return suffix
}
return text
}
/**
* 获取后缀图标
* @param {Object} doctype
*/
export function getFileIcon(doctype) {
const file_doc_type_arr = ['doc', 'excel', 'file', 'folder', 'image', 'pdf', 'video']
if (file_doc_type_arr.indexOf(doctype) >= 0) {
return `/static/${doctype}.png`
} else {
return '/static/file.png'
}
}
/**
* 获取uuid
*/
export function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
export const queryString = (params) => {
return Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`)
.join('&')
}
/**
* 将任意日期转换为对应季度的首日
* @param {Date|string} date - 日期对象或日期字符串
* @returns {string} 格式为'YYYY-MM-DD'的季度首日
*/
export function dateToQuarterStart(date) {
if (!date){
return ''
}
// 如果传入的是字符串转换为Date对象
if (typeof date === 'string') {
date = new Date(date)
}
const year = date.getFullYear()
const month = date.getMonth() // 0-11
// 计算季度
const quarter = Math.floor(month / 3)
// 确定季度首月的月份 (0=1月, 3=4月, 6=7月, 9=10月)
const quarterStartMonth = quarter * 3
// 格式化为'YYYY-MM-DD'
return `${year}-${String(quarterStartMonth + 1).padStart(2, '0')}-01`
}
//时间数组
export function timeData(startTime = '08:00', endTime = '18:30', timeInterval = 0.5) {
const time = []
const date = timeStamp(Date.now()).allDate
const startDate = `${date} ${startTime}`
const endDate = `${date} ${endTime}`
const startTimeStamp = new Date(startDate).getTime()
const endTimeStamp = new Date(endDate).getTime()
const timeStr = 3600 * 1000 * timeInterval
console.log(startTimeStamp)
for (let i = startTimeStamp; i <= endTimeStamp; i = i + timeStr) {
const timeObj:any = {}
timeObj.time = timeStamp(i).hour
timeObj.disable = 0
time.push(timeObj)
}
return time
}
//字符串拼接
function strFormat(str) {
return str < 10 ? `0${str}` : str
}
//时间戳转字符串
export function timeStamp(time) {
const dates = new Date(time)
const year = dates.getFullYear()
const month = dates.getMonth() + 1
const date = dates.getDate()
const day = dates.getDay()
const hour = dates.getHours()
const min = dates.getMinutes()
const days = ['日', '一', '二', '三', '四', '五', '六']
return {
allDate: `${year}/${strFormat(month)}/${strFormat(date)}`,
date: `${strFormat(month)}-${strFormat(date)}`, //返回的日期 07-01
day: `星期${days[day]}`, //返回的礼拜天数 星期一
hour: strFormat(hour) + ':' + strFormat(min) //返回的时钟 08:00
}
}
/**
* 字符串转时间
* @param {Object} dateString
*/
export function stringToDate(dateString) {
var newDate = new Date();
var yearNum = Number(dateString.substr(0,4));
var monthNum = Number(dateString.substr(5,2))-1;
var dayNum = Number(dateString.substr(8,2));
var hourNum = Number(dateString.substr(11,2));
var minuteNum = Number(dateString.substr(14,2));
//var secondNum = Number(dateString.substr(12,2));
newDate.setFullYear(yearNum);
newDate.setMonth(monthNum);
newDate.setDate(dayNum);
newDate.setHours(hourNum);
newDate.setMinutes(minuteNum);
//newDate.setSeconds(secondNum);
return newDate;
}

View File

@@ -0,0 +1,135 @@
/**
* 基础服务(原常用服务)
* useful server
*/
const icon_prefix = '/static/index/128/'
/*
*/
export const us = {
data: [
{
title: 'online',
icon: icon_prefix + 'qingjia1.png',
description: 'online',
useCount: 10000,
routeIndex: 'online',
enabled: true,
},
{
title: '仪表盘',
icon: icon_prefix + 'chart.png',
description: '仪表盘',
useCount: 10000,
routeIndex: 'workHome',
enabled: true,
},
{
title: '组件示例',
icon: icon_prefix + 'chuchai.png',
description: '组件示例',
useCount: 10000,
routeIndex: 'demo',
enabled: true,
},
{
title: '流程待办',
icon: icon_prefix + 'gongwen.png',
description: '流程待办',
useCount: 10000,
routeIndex: 'flowIndex',
},
{
title: '知识库',
icon: icon_prefix + 'qingjia1.png',
description: '知识库',
useCount: 10000,
routeIndex: 'knowledge',
enabled: true,
},
{
title: '通知公告',
icon: icon_prefix + 'tongzhi.png',
description: '查看企业对员工下发的通知公告',
useCount: 10000,
routeIndex: 'annotationList',
enabled: true,
},
{
title: '请假申请',
icon: icon_prefix + 'richang.png',
description: '请假申请',
useCount: 10000,
routeIndex: 'leave',
enabled: true,
},
{
title: '出差申请',
icon: icon_prefix + 'zhoubao.png',
description: '出差申请',
useCount: 10000,
routeIndex: 'businesStrip',
enabled: true,
},
{
title: '公文发文',
icon: icon_prefix + 'zhoubao.png',
description: '公文发文',
useCount: 10000,
routeIndex: 'docSend',
enabled: true,
},
{
title: '日程',
icon: icon_prefix + 'richeng.png',
description: '建立和查看个人工作安排',
useCount: 10000,
routeIndex: 'schedule',
},
{
title: '考勤',
icon: icon_prefix + 'kaoqin.png',
description: '工作考勤',
routeIndex: 'ClockIn',
useCount: 10000,
enabled: true,
},
{
title: '内部邮件',
icon: icon_prefix + 'youjian.png',
description: '查看内部消息',
useCount: 10000,
dot: false,
routeIndex: 'mailHome',
enabled: true,
},
],
}
/**
* 生产服务(原其他服务)
* 首页「更多」按钮仍按 type=other 追加,勿删空数组
*/
export const os = {
data: [
//update-begin---author:xsl ---date:20260720 for【APP首页】生产服务菜单调整-----------
{
title: '原材料送检',
icon: icon_prefix + 'renwu.png',
description: '扫码送检与录入检验结果',
useCount: 10000,
routeIndex: 'rawMaterialInspect',
enabled: true,
},
{
title: '扫描识别',
icon: icon_prefix + 'wendang.png',
description: '手持机扫描头识别条码',
useCount: 10000,
routeIndex: 'scanIdentify',
enabled: true,
},
//update-end---author:xsl ---date:20260720 for【APP首页】生产服务菜单调整-----------
],
}

View File

@@ -0,0 +1,73 @@
<template>
<wd-popup v-model="show" position="bottom" @close="handleClose">
<view class="contetn">
<wd-text custom-class="title" v-if="title" :text="title"></wd-text>
<wd-cell-group border>
<wd-cell
v-for="(item, index) in options"
:icon="item.icon"
:label="item.label"
:custom-class="item.color"
clickable
@click="handleClick(item)"
></wd-cell>
</wd-cell-group>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
name: 'BottomOperate',
options: {
styleIsolation: 'shared',
},
})
const eimt = defineEmits(['change', 'close'])
const show = ref(true)
const props = defineProps(['title', 'data', 'options'])
const handleClose = () => {
show.value = false
setTimeout(() => {
eimt('close')
}, 300)
}
const handleClick = (item) => {
eimt('change', { option: item, data: props.data })
handleClose()
}
</script>
<style lang="scss" scoped>
.contetn {
padding-top: 10px;
:deep(.title) {
padding-left: 10px;
padding-right: 10px;
font-size: 14px;
color: #666;
margin-bottom: 10px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.wd-cell) {
padding-left: 10px;
--wot-cell-icon-size: 15px;
--wot-cell-label-color: #444;
--wot-cell-label-fs: 14px;
.wd-icon {
margin-right: 10px;
}
.wd-cell__label {
margin-top: 0;
}
&.red {
color: red;
--wot-cell-label-color: red;
}
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="breadcrumb">
<template v-for="(item, index) in items" :key="index">
<span
class="breadcrumb-item"
:class="{ 'is-link': index < items.length - 1 }"
@click="handleClick(item, index)"
>
<slot :name="'item-' + index" :item="item">
{{ item.title }}
</slot>
</span>
<span v-if="index < items.length - 1" class="breadcrumb-separator">{{ separator }}</span>
</template>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'Breadcrumb',
})
interface BreadcrumbItem {
title: string
[key: string]: any
}
const props = defineProps({
items: {
type: Array as PropType<BreadcrumbItem[]>,
default: () => [],
},
separator: {
type: String,
default: '/',
},
})
const emit = defineEmits(['click'])
const handleClick = (item: BreadcrumbItem, index: number) => {
if (index < props.items.length - 1) {
emit('click', item, index)
}
}
</script>
<style lang="scss" scoped>
.breadcrumb {
display: flex;
align-items: center;
font-size: 14px;
line-height: 1.5;
padding: 8px;
&-item {
&.is-link {
color: rgba(0, 0, 0, 0.45);
cursor: pointer;
transition: color 0.3s;
}
&:last-child {
color: rgba(0, 0, 0, 0.75);
}
}
&-separator {
margin: 0 8px;
color: rgba(0, 0, 0, 0.45);
}
}
</style>

View File

@@ -0,0 +1,286 @@
<template>
<view class="CategorySelect">
<view class="pickerArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view v-if="!!showText && !$attrs.disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup v-if="popupShow" position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<scroll-view class="flex-1" scroll-y>
<DaTree
:data="treeData"
labelField="title"
valueField="key"
loadMode
:showCheckbox="multiple"
:showRadioIcon="false"
:checkStrictly="true"
:defaultCheckedKeys="defaultCheckedKeys"
:loadApi="asyncLoadTreeData"
@change="handleTreeChange"
></DaTree>
</scroll-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray } from '@/utils/is'
defineOptions({
name: 'CategorySelect',
})
const props = defineProps({
modelValue: {
type: [Array, String],
},
condition: {
type: String,
default: '',
required: false,
},
// 是否支持多选
multiple: {
type: Boolean,
default: false,
},
pid: {
type: String,
default: '',
required: false,
},
pcode: {
type: String,
default: '',
required: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const api = {
loadDictItem: '/sys/category/loadDictItem/',
loadTreeData: '/sys/category/loadTreeData',
}
const showText = ref('')
const popupShow = ref(false)
const treeData = ref<any[]>([])
const treeValue = ref([])
const attrs = useAttrs()
const defaultCheckedKeys: any = ref([])
const handleClick = () => {
if (!attrs.disabled) {
popupShow.value = true
}
}
const cancel = () => {
popupShow.value = false
}
const confirm = () => {
const titles = treeValue.value.map((item) => item.title)
const keys = treeValue.value.map((item) => item.key).join(',')
showText.value = titles.join(',')
popupShow.value = false
emit('update:modelValue', keys)
emit('change', keys)
}
const handleTreeChange = (value, record) => {
const { originItem, checkedStatus } = record
const { key, title } = originItem
if (checkedStatus) {
// 选中
if (props.multiple) {
treeValue.value.push({ key, title })
} else {
treeValue.value = [{ key, title }]
}
} else {
// 取消
if (props.multiple) {
const findIndex = treeValue.value.findIndex((item) => item.key == key)
if (findIndex != -1) {
treeValue.value.splice(findIndex, 1)
}
} else {
treeValue.value = []
}
}
}
const transformField = (result) => {
for (let i of result) {
i.value = i.key
if (i.leaf == false) {
i.isLeaf = false
} else if (i.leaf == true) {
i.isLeaf = true
}
}
}
// 异步加载
const asyncLoadTreeData = ({ originItem }) => {
return new Promise<void>((resolve) => {
let param = {
pid: originItem.key,
condition: props.condition,
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
transformField(result)
resolve(result)
} else {
resolve(null)
}
})
.catch((err) => resolve(null))
})
}
// 加载根节点
function loadRoot() {
let param = {
pid: props.pid,
pcode: !props.pcode ? '0' : props.pcode,
condition: props.condition,
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
if (result && result.length > 0) {
transformField(result)
treeData.value = result
}
} else {
toast.warning('分类字典书组件根节点数据加载失败~')
}
})
.catch((err) => {
toast.warning('分类字典书组件根节点数据加载失败~')
})
}
// 翻译input内的值
function loadItemByCode() {
let value = props.modelValue
if(value){
if (isArray(props.modelValue)) {
// @ts-ignore
value = value.join()
}
if (value === treeData.value.map((item) => item.key).join(',')) {
// 说明是刚选完,内部已有翻译。不需要再请求
return
}
value = (value as string).trim()
if (value) {
http
.get(api.loadDictItem, { ids: value })
.then((res: any) => {
if (res.success) {
const { result = [] } = res
showText.value = result.join(',')
defaultCheckedKeys.value = value
} else {
}
})
.catch((err) => {})
}
}
}
// 清空
const handleClear = () => {
showText.value = ''
treeValue.value = []
confirm()
}
watch(
() => props.modelValue,
() => {
loadItemByCode()
},
{ deep: true, immediate: true },
)
watch(
() => props.pcode,
() => {
loadRoot()
},
{ deep: true, immediate: true },
)
</script>
<style lang="scss" scoped>
:deep(.wd-popup-wrapper) {
.wd-popup {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
}
.content {
height: 50vh;
width: 100vw;
display: flex;
flex-direction: column;
.operation {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 5px;
position: relative;
&::before {
content: ' ';
position: absolute;
bottom: 0;
left: 8px;
right: 8px;
height: 1px;
background-color: #e5e5e5;
}
.cancel,
.confrim {
font-size: 15px;
height: 40px;
min-width: 40px;
text-align: center;
}
.confrim {
color: var(--wot-color-theme);
}
}
:deep(.da-tree) {
.da-tree-item__checkbox {
// display: none;
}
}
}
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<view class="DateTime">
<view class="inputArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view v-if="!!showText && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup v-if="popupShow" position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<picker-view class="picker-view" :value="pickerValue" @change="handleChange">
<!-- -->
<picker-view-column v-if="showYear">
<view class="picker-item" v-for="(item, index) in years" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showMonth">
<view class="picker-item" v-for="(item, index) in months" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showDay">
<view class="picker-item" v-for="(item, index) in days" :key="index">{{ item }}</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showHour">
<view class="picker-item" v-for="(item, index) in hours" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showMinute">
<view class="picker-item" v-for="(item, index) in minutes" :key="index">
{{ item }}
</view>
</picker-view-column>
<!-- -->
<picker-view-column v-if="showSecond">
<view class="picker-item" v-for="(item, index) in seconds" :key="index">
{{ item }}
</view>
</picker-view-column>
</picker-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { useToast } from 'wot-design-uni'
import dayjs from 'dayjs'
import { getPlaceholder } from '@/common/uitls'
defineOptions({
name: 'DateTime',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: String,
default: '',
},
// 显示格式year/month/day/hour/minute/second
format: {
type: String,
default: 'YYYY-MM-DD HH:mm:ss',
},
// 开始时间
startTime: {
type: String,
default: '1970-01-01 00:00:00',
},
// 结束时间
endTime: {
type: String,
default: '2100-12-31 23:59:59',
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
// 显示控制
const showYear = computed(() => props.format.includes('YYYY'))
const showMonth = computed(() => props.format.includes('MM'))
const showDay = computed(() => props.format.includes('DD'))
const showHour = computed(() => props.format.includes('HH'))
const showMinute = computed(() => props.format.includes('mm'))
const showSecond = computed(() => props.format.includes('ss'))
// 数据源
const years = ref<number[]>([])
const months = ref<number[]>([])
const days = ref<number[]>([])
const hours = ref<number[]>([])
const minutes = ref<number[]>([])
const seconds = ref<number[]>([])
// 当前选中的值
const pickerValue = ref<number[]>([])
const showText = computed(() => props.modelValue)
const popupShow = ref(false)
// 初始化数据
const initData = () => {
const start = dayjs(props.startTime)
const end = dayjs(props.endTime)
// 初始化年份
if (showYear.value) {
years.value = Array.from({ length: end.year() - start.year() + 1 }, (_, i) => start.year() + i)
}
// 初始化月份
if (showMonth.value) {
months.value = Array.from({ length: 12 }, (_, i) => i + 1)
}
// 初始化日期
if (showDay.value) {
const currentYear = props.modelValue ? dayjs(props.modelValue).year() : start.year()
const currentMonth = props.modelValue ? dayjs(props.modelValue).month() + 1 : 1
const daysInMonth = dayjs(`${currentYear}-${currentMonth}`).daysInMonth()
days.value = Array.from({ length: daysInMonth }, (_, i) => i + 1)
}
// 初始化小时
if (showHour.value) {
hours.value = Array.from({ length: 24 }, (_, i) => i)
}
// 初始化分钟
if (showMinute.value) {
minutes.value = Array.from({ length: 60 }, (_, i) => i)
}
// 初始化秒
if (showSecond.value) {
seconds.value = Array.from({ length: 60 }, (_, i) => i)
}
}
// 更新日期
const updateDays = () => {
if (!showYear.value || !showMonth.value) return
const year = years.value[pickerValue.value[0] || 0]
const month = months.value[pickerValue.value[1] || 0]
const daysInMonth = dayjs(`${year}-${month}`).daysInMonth()
days.value = Array.from({ length: daysInMonth }, (_, i) => i + 1)
}
// 初始化pickerValue
const initializePickerValue = () => {
let newPickerValue = []
if (
[
'YYYY-MM-DD HH:mm:ss',
'YYYY-MM-DD HH:mm',
'YYYY-MM-DD HH',
'YYYY',
'YYYY-MM',
'YYYY-MM-DD',
].includes(props.format)
) {
const dateTime = props.modelValue ? dayjs(props.modelValue) : dayjs()
if (showYear.value) {
const yearIndex = years.value.indexOf(dateTime.year())
newPickerValue.push(yearIndex >= 0 ? yearIndex : 0)
}
if (showMonth.value) {
const monthIndex = months.value.indexOf(dateTime.month() + 1)
newPickerValue.push(monthIndex >= 0 ? monthIndex : 0)
}
if (showDay.value) {
const dayIndex = days.value.indexOf(dateTime.date())
newPickerValue.push(dayIndex >= 0 ? dayIndex : 0)
}
if (showHour.value) {
newPickerValue.push(dateTime.hour())
}
if (showMinute.value) {
newPickerValue.push(dateTime.minute())
}
if (showSecond.value) {
newPickerValue.push(dateTime.second())
}
} else if (['HH:mm:ss', 'HH:mm', 'mm:ss', 'HH', 'mm', 'ss'].includes(props.format)) {
const dateTime = props.modelValue
if (dateTime) {
newPickerValue = dateTime.split(':').map((item) => +item)
} else {
// 获取当前时间
const now = dayjs()
if (showHour.value) {
newPickerValue.push(now.hour())
}
if (showMinute.value) {
newPickerValue.push(now.minute())
}
if (showSecond.value) {
newPickerValue.push(now.second())
}
}
}
pickerValue.value = newPickerValue
}
// 处理选择变化
const handleChange = (e: any) => {
const values = e.detail.value
pickerValue.value = values
// 如果选择了年月,需要更新日期
if (showYear.value && showMonth.value && showDay.value) {
updateDays()
}
}
// 点击确认
const confirm = () => {
const values = pickerValue.value
let selectedDate = dayjs()
let index = 0
// 只设置用户选择的时间部分,未选择的保持当前时间
if (showYear.value && values[index] !== undefined) {
selectedDate = selectedDate.year(years.value[values[index]])
index++
}
if (showMonth.value && values[index] !== undefined) {
selectedDate = selectedDate.month(months.value[values[index]] - 1)
index++
}
if (showDay.value && values[index] !== undefined) {
selectedDate = selectedDate.date(days.value[values[index]])
index++
}
if (showHour.value && values[index] !== undefined) {
selectedDate = selectedDate.hour(hours.value[values[index]])
index++
}
if (showMinute.value && values[index] !== undefined) {
selectedDate = selectedDate.minute(minutes.value[values[index]])
index++
}
if (showSecond.value && values[index] !== undefined) {
selectedDate = selectedDate.second(seconds.value[values[index]])
index++
}
popupShow.value = false
const output = selectedDate.format(props.format)
emit('update:modelValue', output)
emit('change', output)
}
// 点击取消
const cancel = () => {
popupShow.value = false
}
// 清空选择
const handleClear = () => {
emit('update:modelValue', '')
emit('change', '')
}
// 点击输入框
const handleClick = () => {
if (!props.disabled) {
initData()
initializePickerValue()
popupShow.value = true
}
}
</script>
<style lang="scss" scoped>
.DateTime {
.picker-view {
width: 100%;
height: 400rpx;
}
.picker-item {
line-height: 88rpx;
text-align: center;
}
.operation {
display: flex;
justify-content: space-between;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
.cancel,
.confrim {
padding: 10rpx 30rpx;
}
.confrim {
color: #2979ff;
}
}
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<view
class="wrap"
:style="{
'--borderColor': borderColor,
'--imgWidth': imgWidth,
'--imgHeight': imgHeight,
}"
>
<wd-grid :clickable="clickable" :column="column">
<template v-for="(item, index) in modelValue" :key="item[itemKey]">
<wd-grid-item
:custom-class="getClass(index)"
use-icon-slot
:text="item.text"
@itemclick="handleClik(item, index)"
>
<template #icon>
<wd-img :width="imgWidth" :height="imgHeight" :src="item.img"></wd-img>
</template>
</wd-grid-item>
</template>
</wd-grid>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
name: 'Grid',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
column: {
type: Number,
default: 4,
},
itemKey: {
type: String,
default: 'id',
},
imgWidth: {
type: String,
default: '28px',
},
imgHeight: {
type: String,
default: '28px',
},
clickable: {
type: Boolean,
default: true,
},
borderColor: {
type: String,
default: 'rgba(165, 165, 165, 0.1)',
},
})
const emit = defineEmits(['itemClik'])
const getClass = (index) => {
let className = ''
if (index < props.column) {
className = 'first-row'
}
if ((index + 1) % props.column == 0) {
className += ` lastCol`
}
return className
}
const handleClik = (item, index) => {
emit('itemClik', item, index)
}
</script>
<style lang="scss" scoped>
:deep(.wd-grid-item) {
box-sizing: border-box;
border-right: 1px solid var(--borderColor, rgba(165, 165, 165, 0.1));
border-bottom: 1px solid var(--borderColor, rgba(165, 165, 165, 0.1));
&.first-row {
border-top: 1px solid var(--borderColor, rgba(165, 165, 165, 0.1));
}
&.lastCol {
border-right: none;
}
.wd-grid-item__text {
margin-top: 10px;
}
.wd-grid-item__wrapper {
width: var(--imgWidth, 28px) !important;
height: var(--imgHeight, 28px) !important;
}
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<view class="help-wrap">
<view class="help-icon" @click.stop="open">
<text class="q">?</text>
</view>
<view v-if="visible" class="mask" @click="close" @touchmove.stop.prevent="">
<view class="bubble" :style="bubbleStyle" @click.stop="">
<text class="bubble-text">{{ text }}</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
defineOptions({ name: 'HelpTipBubble' })
const props = defineProps({
text: {
type: String,
default: '',
},
top: {
type: String,
default: '180upx',
},
})
const visible = ref(false)
const bubbleStyle = computed(() => ({
top: props.top,
}))
function open() {
visible.value = true
}
function close() {
visible.value = false
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped>
.help-wrap {
display: inline-flex;
align-items: center;
}
.help-icon {
width: 36upx;
height: 36upx;
border-radius: 50%;
border: 2upx solid #888;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8upx;
}
.q {
font-size: 22upx;
color: #666;
font-weight: 600;
line-height: 1;
}
.mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1000;
background: transparent;
}
.bubble {
position: absolute;
left: 32upx;
right: 32upx;
padding: 24upx 28upx;
border-radius: 16upx;
background: rgb(180, 180, 180);
box-sizing: border-box;
}
.bubble-text {
font-size: 26upx;
color: #fff;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<view class="previewImage" @tap="close">
<view class="page" v-if="urls.length > 0">
<text class="text">{{ current + 1 }} / {{ urls.length }}</text>
</view>
<swiper
class="swiper"
:current="current"
@change="swiperChange"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
>
<swiper-item class="swiperItem" v-for="(item, index) in urls" :key="index">
<movable-area class="movable-area" scale-area>
<movable-view
class="movable-view"
direction="all"
:inertia="true"
damping="100"
scale="true"
scale-min="1"
scale-max="4"
:scale-value="scale"
>
<scroll-view scroll-y="true" class="uni-scroll-view">
<view class="scroll-view">
<image
:key="index"
class="image"
:src="item"
mode="widthFix"
@longpress="onLongpress(item)"
/>
</view>
</scroll-view>
</movable-view>
</movable-area>
</swiper-item>
</swiper>
</view>
</template>
<script>
export default {
props: {
urls: {
type: Array,
required: true,
default: () => {
return []
},
},
},
data() {
return {
show: false,
current: 0, //当前页
scale: 1,
isZooming: false, // 是否处于缩放状态
}
},
methods: {
open(current) {
this.current = this.urls.findIndex((item) => item === current)
},
//关闭
close() {
if (!this.isZooming) {
this.show = false
this.current = 0
this.$emit('close')
}
},
//图片改变
swiperChange(e) {
this.current = e.detail.current
},
//监听长按
onLongpress(e) {
this.$emit('onLongpress', e)
},
handleTouchStart() {
this.isZooming = true
},
handleTouchEnd() {
this.isZooming = false
},
},
}
</script>
<style lang="scss" scoped>
.previewImage {
z-index: 9999;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000000;
.swiper {
width: 100%;
height: 100vh;
.swiperItem {
.movable-area {
height: 100%;
width: 100%;
.movable-view {
width: 100%;
min-height: 100%;
.uni-scroll-view {
height: 100vh;
}
.scroll-view {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
.image {
width: 100%;
height: auto;
}
}
}
}
}
}
.page {
position: absolute;
z-index: 9999;
width: 100%;
top: 60rpx;
text-align: center;
.text {
color: #fff;
font-size: 32rpx;
background-color: rgba(0, 0, 0, 0.5);
padding: 3rpx 16rpx;
border-radius: 20rpx;
user-select: none;
}
}
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<view :style="[{backgroundColor:backgroundColor}]">
<map
v-if="latitude && longitude"
class="map-content"
:latitude="latitude"
:longitude="longitude"
:markers="marker"
:scale="scale"
:circles="circles"
>
</map>
</view>
</template>
<script setup>
import useGeo from '@/hooks/useGeoPosition'
// 定义 props
const props = defineProps({
compLatitude: {
type: Number,
default: 40.009390,
required: false
},
compLongitude: {
type: Number,
default: 116.374322,
required: false
}
});
const emit = defineEmits(['success','fail']);
const backgroundColor = 'transparent';
// 引入地理定位逻辑
let [{inCircle,getAddress,getMyAddress,latitude,longitude,marker,scale,circles},{refreshLocation}] = useGeo({ compLatitude: props.compLatitude, compLongitude: props.compLongitude }, emit);
defineExpose({
inCircle,
getAddress,
getMyAddress,
refreshLocation
});
</script>
<style lang="scss" scoped>
.map-content {
width: 100%;
height: 250px;
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<view class='t-toptips' :style="{top: top,background: cubgColor}" :class="[show?'t-top-show':'']">
<view v-if="loading" class="flex flex-sub">
<view class="cu-progress flex-sub round striped active sm">
<view :style="{ background: color,width: value + '%'}"></view>
</view>
<text class="margin-left">{{value}}%</text>
</view>
<block v-else>{{msg}}</block>
<!-- #ifdef H5 -->
<view ref="input" class="input"></view>
<!-- #endif -->
</view>
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue'
import {useToast} from "wot-design-uni";
import {http} from "@/utils/http";
const props = defineProps({
top: {
type: String,
default: 'auto'
},
bgColor: {
type: String,
default: 'rgba(49, 126, 243, 0.5)',
},
color: {
type: String,
default: '#e54d42',
},
type: {
type: String,
default: 'file'
}
})
const toast = useToast()
const emits = defineEmits(['up-success', 'up-error'])
const cubgColor = ref('')
const loading = ref(false)
const value = ref(5)
const show = ref(false)
const msg = ref('执行完毕~')
const input = ref(null)
const getRequest = (url) => {
let theRequest = {}
let index = url.indexOf("?")
if (index != -1) {
let str = url.substring(index+1)
let strs = str.split("&")
for(let i = 0; i < strs.length; i++) {
theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1])
}
}
return theRequest
}
/**
*
* @param currentWebview 当前窗口webview对象
* @param url 上传接口地址
* @param name 上传文件key值
* @param header 上传接口请求头
* @param formData body内其他参数
*/
const appChooseFile = ({currentWebview, url, name = 'file', header, ...formData} = {} as any) => {
// #ifdef APP-PLUS
let wv = plus.webview.create("","/hybrid/html/index.html",{
//'uni-app': 'none',
top: '0px',
height: '100%',
background: 'transparent'
},{
url,
header,
formData,
key: name,
})
wv.loadURL("/hybrid/html/index.html")
currentWebview.append(wv)
wv.overrideUrlLoading({mode:'reject'},(e)=>{
let {fileName, id, fileSize} = getRequest(e.url) as any
fileName = unescape(fileName)
id = unescape(id)
fileSize = unescape(fileSize)
console.log("fileSize", fileSize)
return onCommit(
emits('up-success', {fileName, fileSize, data: {id, statusCode: 200}})
)
})
// #endif
}
/**
*
* @param url 上传接口地址
* @param name 上传文件key值
* @param header 上传接口请求头
* @param formData body内其他参数
*/
const h5ChooseFile = ({url, name = 'file', header, ...formData} = {} as any) => {
// #ifdef H5
const inputEl = document.createElement('input');
inputEl.type = 'file'
inputEl.style.display = 'none'
inputEl.id = 'file'
if(props.type === 'image'){
inputEl.accept = 'image/*'
}
input.value.$el.appendChild(inputEl)
document.getElementById("file").click()
inputEl.onchange = (event) => {
uploadH5(event, url, header)
}
// #endif
}
/**
*
* @param url
* @param name
* @param header
* @param formData
*/
const wxChooseFile = ({url, name = 'file', header, ...formData} = {} as any) => {
wx.chooseMessageFile({
count: 1,
type: 'file',
success: ({tempFiles}) => {
let [{path: filePath, name: fileName}] = tempFiles
setDefUI()
console.log("tempFiles==>::", tempFiles)
console.log("filePath==>::", filePath)
console.log("url==>::", url)
return http.upload(url, {
filePath: filePath,
name: 'file'
}).then((res : any) => {
if (res.statusCode === 200) {
console.log("wxChooseFile返回值success::", res)
let data = res.data
return onCommit(emits('up-success', {fileName, data}))
}
}).catch(err => {
console.log("wxChooseFile返回值err", err)
return errorHandler('文件上传失败', upErr)
})
},
fail: () => errorHandler('文件选择失败', upErr)
})
}
/**
* 上传
* @param param
*/
const upload = (param = {} as any) => {
if (!param.url) {
toast.warning('上传地址不正确')
return
}
if (loading.value) {
toast.warning('还有个文件玩命处理中,请稍候..')
return
}
// #ifdef APP-PLUS
return appChooseFile(param)
// #endif
}
/**
* 打开文件
* @param filePath
*/
const open = (filePath) => {
let system = uni.getSystemInfoSync().platform
if(system == 'ios'){
filePath = encodeURI(filePath)
}
uni.openDocument({
filePath,
success: (res) => { console.log('打开文档成功') }
})
}
/**
* type: temporary=返回临时地址local=长期保存到本地
* @param url
* @param type
*/
const download = (url, type = 'temporary') => {
if (loading.value) {
toast.warning('还有个文件玩命处理中,请稍候..')
return
}
setDefUI()
return new Promise((resolve, reject) => {
let downloadTask = uni.downloadFile({
url,
success: ({statusCode, tempFilePath}) => {
if (statusCode === 200) {
if (type == 'local') {
uni.saveFile({
tempFilePath,
success: ({savedFilePath}) => onCommit(resolve(savedFilePath)),
fail: () => errorHandler('下载失败', reject)
})
}
else {
onCommit(resolve(tempFilePath))
}
}
},
fail: () => errorHandler('下载失败', reject)
})
downloadTask.onProgressUpdate(({progress = 0}) => {
if (progress <= 100) {
nextTick(() => {
value.value = progress
})
}
})
})
}
const onCommit = (resolve) => {
msg.value = '执行完毕~'
loading.value = false
cubgColor.value = 'rgba(57, 181, 74, 0.5)'
setTimeout(() => { show.value = false }, 1500)
return resolve
}
// #ifdef H5
const uploadH5 = (event, url, header) => {
let _file = event.target.files[0]
let formData = new FormData()
formData.append("file", _file)
let xhr = new XMLHttpRequest()
xhr.open("post", url, true)
for (let keys in header) {
xhr.setRequestHeader(keys, header[keys])
}
xhr.upload.onloadstart = () => {
toast.loading('上传中...')
}
xhr.upload.onprogress = (res) => {
if(res.loaded / res.total == 1){
setTimeout(() => {
toast.close()
}, 5000)
}
}
xhr.onload = (res:any) => {
console.log("上传完成", res)
console.log(res.target.response)
toast.close()
if (res.target.status === 200) {
let data = JSON.parse(res.target.response)
console.log("data", data)
let fileName = data.message
onCommit(emits('up-success', {fileSize: _file.size, fileName, data}))
} else {
toast.error('文件上传失败')
}
const inputEl = document.getElementById("file");
input.value.$el.removeChild(inputEl)
}
xhr.onerror = (data) => {
toast.close()
toast.error('上传失败')
}
xhr.ontimeout = function(event) {
toast.close()
toast.error('上传超时,请刷新重试')
}
xhr.send(formData)
}
// #endif
const setDefUI = () => {
cubgColor.value = props.bgColor
value.value = 0
loading.value = true
show.value = true
}
const upErr = (errText) => {
emits('up-error', errText)
}
const errorHandler = (errText, reject) => {
msg.value = errText
loading.value = false
cubgColor.value = 'rgba(229, 77, 66, 0.5)'
setTimeout(() => { show.value = false }, 1500)
return reject(errText)
}
defineExpose({
upload,
open,
download
})
</script>
<style scoped>
.t-toptips {
width: 100%;
padding: 18upx 30upx;
box-sizing: border-box;
position: fixed;
z-index: 90;
color: #fff;
font-size: 30upx;
left: 0;
display: flex;
align-items: center;
justify-content: center;
word-break: break-all;
display: none;
transform: translateZ(0) translateY(-100%);
transition: all 0.3s ease-in-out;
}
.t-top-show {
transform: translateZ(0) translateY(0);
display: block;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view class="pageLayout">
<view
v-if="navbarShow"
:class="{ pageNav: true, transparent: navBgTransparent, fixed: navFixed }"
:style="{ height: `${statusBarHeight + navHeight}px` }"
>
<view class="statusBar" :style="{ height: `${statusBarHeight}px` }"></view>
<wd-navbar
:bordered="false"
:title="navTitle"
:leftText="navLeftText"
:leftArrow="navLeftArrow"
:rightText="isMp ? '' : navRightText"
@clickLeft="handleClickLeft"
@clickRight="handleClickRight"
:custom-class="getClass()"
>
<template v-if="isMp === false && $slots.navRight" #right>
<slot name="navRight"></slot>
</template>
<template v-if="isMp" #left>
<!-- 为了在小程序上美观 -->
<template v-if="$slots.navRight && isShowNavRightTextMp">
<view class="btnGroup">
<view class="left" @click.stop="handleClickLeft">{{ navLeftText }}</view>
<view class="right" @click.stop="() => emit('navRightMp')">{{ navRightTextMp }}</view>
</view>
</template>
<template v-else-if="$slots.navLeft">
<slot name="navLeft"></slot>
</template>
<template v-else>
<view class="mpNavLeft" @click.stop="handleClickLeft">
<wd-icon v-if="navLeftArrow" name="thin-arrow-left" size="14px"></wd-icon>
<view>{{ navLeftText }}</view>
</view>
</template>
</template>
</wd-navbar>
</view>
<view class="pageContent">
<view class="content">
<slot></slot>
</view>
<!--微信中导航右侧的在这儿生成-->
<view class="navRight" v-if="isMp && !$slots.navRight && mpNavRightIsBottom && navRightText">
<wd-button @click="handleClickRight">{{ navRightText }}</wd-button>
</view>
</view>
<view class="tabbar"></view>
</view>
</template>
<script setup lang="ts">
import { useSlots } from 'vue'
import { useRouter } from '@/plugin/uni-mini-router'
import { useParamsStore } from '@/store/page-params'
import { isMp } from '@/utils/platform'
// const isMp = true
defineOptions({
name: 'pageLayout',
options: {
// apply-shared当前页面样式会影响到子组件样式.(小程序)
// shared当前页面样式影响到子组件子组件样式也会影响到当前页面.(小程序)
styleIsolation: 'shared',
},
})
const paramsStore = useParamsStore()
const router = useRouter()
const props = defineProps({
backRouteName: {
type: String,
default: '',
},
backRoutePath: {
type: String,
default: '',
},
routeParams: {
type: Object,
default: () => {},
},
routeQuery: {
type: Object,
default: () => {},
},
routeMethod: {
type: String,
default: 'replace',
},
navbarShow: {
type: Boolean,
default: true,
},
navBgTransparent: {
type: Boolean,
default: false,
},
navFixed: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'page', // 'page','popup'
},
navTitle: {
type: String,
default: '',
},
navLeftText: {
type: String,
default: '返回',
},
navLeftArrow: {
typeof: Boolean,
default: true,
},
navRightText: {
typeof: String,
default: '',
},
// 当有右侧有slot时在小程序上不显示slot显示文字 (默认显示)
isShowNavRightTextMp: {
typeof: Boolean,
default: true,
},
// 当有右侧有slot时在小程序上不显示slot显示文字
navRightTextMp: {
typeof: String,
default: '确定',
},
// 右侧的文本在小程序端是否生成在页面底部(否:就得业务中自己写)
mpNavRightIsBottom: {
typeof: Boolean,
default: true,
},
})
const slot = useSlots()
const globalData = getApp().globalData
const { systemInfo, navHeight } = globalData
const { statusBarHeight } = systemInfo
const emit = defineEmits(['navBack', 'navRight', 'navRightMp'])
const handleClickLeft = () => {
emit('navBack')
// 只有在页面中才默认返回,弹层中不返回
if (props.type === 'page') {
const pages = getCurrentPages()
if (props.backRouteName || props.backRoutePath) {
const prevPage = pages[pages.length - 2]
if (prevPage) {
const route = prevPage.route
const name = route.split('/').pop()
if (route === props.backRoutePath || props.backRouteName === name) {
router.back()
clearPageParamsCache()
return
}
}
if (props.backRouteName) {
router[props.routeMethod]({ name: props.backRouteName, params: props.routeParams })
clearPageParamsCache()
} else {
router[props.routeMethod]({ name: props.backRoutePath, query: props.routeQuery })
clearPageParamsCache()
}
} else {
router.back()
clearPageParamsCache()
}
}
}
const clearPageParamsCache = () => {
// 清除页面传参缓存
const pages = getCurrentPages()
const curPage = pages[pages.length - 1]
const curRoute = curPage.route
const name = curRoute.split('/').pop()
paramsStore.clearPageParams(name)
}
const handleClickRight = () => {
emit('navRight')
}
const getClass = () => {
const cls = `nav ${isMp ? 'mp' : ''} ${slot.navRight ? 'slot_navRight' : ''}`
return cls
}
console.log('props:', props)
</script>
<style lang="scss" scoped>
.pageLayout {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
.pageNav {
background-image: linear-gradient(45deg, #0081ff, #1cbbb4);
&.transparent {
background-image: none;
}
&.fixed {
position: fixed;
top: 0;
left: 0;
z-index: 1;
width: 100%;
}
.statusBar {
width: 100%;
height: 0;
}
:deep(.wd-navbar) {
background-color: transparent;
--wot-navbar-title-font-weight: 400;
--wot-navbar-arrow-size: 18px;
--wot-navbar-desc-font-size: 14px;
--wot-navbar-title-font-size: 16px;
&.mp {
&.slot_navRight {
padding-right: 90px;
.wd-navbar__text {
display: none;
}
.wd-navbar__content {
display: flex;
}
.wd-navbar__left,
.wd-navbar__title,
.wd-navbar__right {
position: static;
}
.wd-navbar__title {
max-width: none;
}
}
}
}
}
.pageContent {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
background-color: #f1f1f1;
.content {
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
}
.navRight {
padding: 15px;
padding-top: 10px;
display: flex;
margin-bottom: calc(constant(safe-area-inset-bottom));
margin-bottom: calc(env(safe-area-inset-bottom));
justify-content: flex-end;
:deep(.wd-button) {
--wot-button-medium-height: 38px;
width: 70%;
display: block;
margin: 0 auto;
}
}
}
.tabbar {
/* #ifdef H5 */
height: var(--window-bottom);
/* #endif */
}
.btnGroup {
display: flex;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.6);
height: 28px;
line-height: 28px;
color: #fff;
align-items: center;
.left {
border-right: 1px solid rgba(255, 255, 255, 0.6);
}
.left,
.right {
padding: 0 8px;
font-size: 14px;
}
}
.mpNavLeft {
display: flex;
color: #fff;
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<view :style="{ height: addUnit(height) }">
<view :class="`wd-navbar ${customClass} ${fixed ? 'is-fixed' : ''} ${bordered ? 'is-border' : ''}`" :style="rootStyle">
<view class="wd-navbar__content">
<view class="wd-navbar__capsule" v-if="$slots.capsule">
<slot name="capsule" />
</view>
<view :class="`wd-navbar__left ${leftDisabled ? 'is-disabled' : ''}`" @click="handleClickLeft" v-else-if="!$slots.left">
<wd-icon v-if="leftArrow" name="arrow-left" custom-class="wd-navbar__arrow" />
<view v-if="leftText" class="wd-navbar__text">{{ leftText }}</view>
</view>
<view v-else :class="`wd-navbar__left ${leftDisabled ? 'is-disabled' : ''}`" @click="handleClickLeft">
<slot name="left" />
</view>
<view class="wd-navbar__title">
<slot name="title" />
<block v-if="!$slots.title && title">{{ title }}</block>
</view>
<view :class="`wd-navbar__right ${rightDisabled ? 'is-disabled' : ''}`" @click="handleClickRight" v-if="$slots.right || rightText">
<slot name="right" />
<view v-if="!$slots.right && rightText" class="wd-navbar__text" hover-class="wd-navbar__text--hover" :hover-stay-time="70">
{{ rightText }}
</view>
</view>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-navbar',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { type CSSProperties, computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
import { getRect, addUnit, isDef, objToStyle } from 'wot-design-uni/components/common/util'
import { navbarProps } from './types'
const props = defineProps(navbarProps)
const emit = defineEmits(['click-left', 'click-right'])
const height = ref<number | ''>('') // 占位高度
const { statusBarHeight } = uni.getSystemInfoSync()
watch(
[() => props.fixed, () => props.placeholder],
() => {
setPlaceholderHeight()
},
{ deep: true, immediate: false }
)
const rootStyle = computed(() => {
const style: CSSProperties = {}
if (props.fixed && isDef(props.zIndex)) {
style['z-index'] = props.zIndex
}
if (props.safeAreaInsetTop) {
style['padding-top'] = addUnit(statusBarHeight || 0)
}
return `${objToStyle(style)};${props.customStyle}`
})
onMounted(() => {
if (props.fixed && props.placeholder) {
nextTick(() => {
setPlaceholderHeight()
})
}
})
function handleClickLeft() {
if (!props.leftDisabled) {
emit('click-left')
}
}
function handleClickRight() {
if (!props.rightDisabled) {
emit('click-right')
}
}
const { proxy } = getCurrentInstance() as any
function setPlaceholderHeight() {
if (!props.fixed || !props.placeholder) {
return
}
getRect('.wd-navbar', false, proxy).then((res) => {
height.value = res.height as number
})
}
</script>
<style lang="scss">
@import './index.scss';
</style>

View File

@@ -0,0 +1,93 @@
@import "wot-design-uni/components/common/abstracts/_mixin.scss";
@import "wot-design-uni/components/common/abstracts/variable.scss";
.wot-theme-dark {
@include b(navbar) {
background-color: $-dark-background;
@include e(title) {
color: $-dark-color;
}
@include e(text) {
color: $-dark-color;
}
:deep(.wd-navbar__arrow) {
color: $-dark-color;
}
}
}
@include b(navbar) {
position: relative;
text-align: center;
user-select: none;
height: $-navbar-height;
line-height: $-navbar-height;
background-color: $-navbar-background;
box-sizing: content-box;
@include e(content) {
position: relative;
height: 100%;
width: 100%;
}
@include e(title) {
max-width: 60%;
height: 100%;
margin: 0 auto;
color: $-navbar-color;
font-weight: $-navbar-title-font-weight;
font-size: $-navbar-title-font-size;
@include lineEllipsis();
}
@include e(text) {
display: inline-block;
vertical-align: middle;
color: $-navbar-desc-font-color;
}
@include e(left, right, capsule) {
position: absolute;
top: 0;
bottom: 0;
font-size: $-navbar-desc-font-size;
display: flex;
align-items: center;
padding: 0 12px;
@include when(disabled) {
opacity: $-navbar-disabled-opacity;
}
}
@include e(left, capsule) {
left: 0;
}
@include e(right) {
right: 0;
}
@include edeep(arrow) {
font-size: $-navbar-arrow-size;
color: $-navbar-color;
}
@include when(border) {
@include halfPixelBorder('bottom');
}
@include when(fixed) {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 500;
}
}

View File

@@ -0,0 +1,52 @@
import type { ExtractPropTypes } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp } from 'wot-design-uni/components/common/props'
export const navbarProps = {
...baseProps,
/**
* 标题文字
*/
title: String,
/**
* 左侧文案
*/
leftText: String,
/**
* 右侧文案
*/
rightText: String,
/**
* 是否显示左侧箭头
*/
leftArrow: makeBooleanProp(false),
/**
* 是否显示下边框
*/
bordered: makeBooleanProp(true),
/**
* 是否固定到顶部
*/
fixed: makeBooleanProp(false),
/**
* 固定在顶部时,是否在标签位置生成一个等高的占位元素
*/
placeholder: makeBooleanProp(false),
/**
* 导航栏 z-index
*/
zIndex: makeNumberProp(500),
/**
* 是否开启顶部安全区适配
*/
safeAreaInsetTop: makeBooleanProp(false),
/**
* 是否禁用左侧按钮,禁用时透明度降低,且无法点击
*/
leftDisabled: makeBooleanProp(false),
/**
* 是否禁用右侧按钮,禁用时透明度降低,且无法点击
*/
rightDisabled: makeBooleanProp(false)
}
export type NavbarProps = ExtractPropTypes<typeof navbarProps>

View File

@@ -0,0 +1,142 @@
<template>
<view class="Popup">
<view class="inputArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
type="text"
readonly
v-model="showText"
clearable
v-bind="$attrs"
/>
<view v-if="!!showText && !$attrs.disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<popupReportModal
v-if="reportModal.show"
:code="code"
:showFiled="reportModal.showFiled"
:multi="multi"
@close="handleClose"
@change="handleChange"
></popupReportModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import popupReportModal from './components/popupReportModal.vue'
defineOptions({
name: 'Popup',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
code: {
type: String,
required: true,
default: '',
},
fieldConfig: {
type: Array,
required: true,
default: () => [],
},
setFieldsValue: {
type: Function,
required: true,
default: () => {},
},
modelValue: {
type: String,
default: '',
},
multi: {
type: Boolean,
default: false,
},
spliter: {
type: String,
default: ',',
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const showText = ref('')
const attrs: any = useAttrs()
const reportModal = reactive({
show: false,
showFiled: props.fieldConfig.map((item) => item['source']).join(','),
})
if (!props.code || props.fieldConfig.length == 0) {
toast.error('popup参数未正确配置!')
}
/**
* 监听value数值
*/
watch(
() => props.modelValue,
(val) => {
showText.value = val && val.length > 0 ? val.split(props.spliter).join(',') : ''
},
{ immediate: true },
)
function callBack(rows) {
let fieldConfig: any = props.fieldConfig
//匹配popup设置的回调值
let values = {}
let labels = []
for (let item of fieldConfig) {
let val = rows.map((row) => row[item.source])
val = val.length == 1 ? val[0] : val.join(',')
item.target.split(',').forEach((target) => {
values[target] = val
})
}
showText.value = labels.join(',')
props.setFieldsValue(values)
emit('change', values)
// emit('update:modelValue', values)
}
// 清空
const handleClear = () => {
showText.value = ''
handleChange([])
}
const handleClick = () => {
if (!attrs.disabled) {
reportModal.show = true
}
}
const handleClose = () => {
reportModal.show = false
}
const handleChange = (data) => {
console.log('选中的值:', data)
callBack(data)
}
</script>
<style lang="scss" scoped>
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,303 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="navTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<view class="wrap">
<z-paging
ref="paging"
:fixed="false"
v-model="dataList"
@query="queryList"
:default-page-size="15"
>
<template #top>
<wd-search
hide-cancel
:placeholder="search.placeholder"
v-model="search.keyword"
@search="handleSearch"
@clear="handleClear"
/>
</template>
<template v-if="multi">
<wd-checkbox-group shape="square" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<view class="list" @click="hanldeCheck(index)">
<view class="left text-gray-5">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
<view class="right" @click.stop>
<wd-checkbox ref="checkboxRef" :modelValue="index"></wd-checkbox>
</view>
</view>
</template>
</wd-checkbox-group>
</template>
<template v-else>
<wd-radio-group shape="dot" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<wd-cell>
<view class="list" @click="hanldeCheck(index)">
<view class="left text-gray-5">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
<view class="right" @click.stop>
<wd-radio :value="index"></wd-radio>
</view>
</view>
</wd-cell>
</template>
</wd-radio-group>
</template>
</z-paging>
</view>
</PageLayout>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray } from '@/utils/is'
defineOptions({
name: 'popupReportModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
code: {
type: String,
default: '',
required: true,
},
showFiled: {
type: String,
default: '',
required: true,
},
multi: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
const api = {
getColumns: '/online/cgreport/api/getRpColumns',
getData: '/online/cgreport/api/getData',
getQueryInfo: '/online/cgreport/api/getQueryInfo',
}
console.log('props:::', props)
const navTitle = ref('')
const paging = ref(null)
const dataList = ref([])
// 报表id
let rpConfigId = null
let loadedColumns = false
const dictOptions = ref([])
const columns = ref([])
const checkedValue: any = ref(props.multi ? [] : '')
const checkboxRef = ref(null)
const search = reactive({
keyword: '',
placeholder: '',
field: '',
})
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const handleConfirm = () => {
if (checkedValue.value.length == 0) {
toast.warning('还没选择~')
return
}
const result = []
let value = checkedValue.value
if (!Array.isArray(checkedValue.value)) {
value = [checkedValue.value]
}
value.forEach((index) => {
result.push(dataList.value[index])
})
show.value = false
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
// 搜索
function handleSearch() {
paging.value.reload()
}
// 清除搜索条件
function handleClear() {
search.keyword = ''
handleSearch()
}
const hanldeCheck = (index) => {
if (props.multi) {
if (Array.isArray(checkboxRef.value)) {
checkboxRef.value[index].toggle()
}
} else {
checkedValue.value = index
}
}
const getQueryInfo = () => {
const analysis = (data = []) => {
if (data.length) {
search.placeholder = `请输入${data[0].label}`
search.field = data[0].field
} else {
const item = columns[0] ?? {}
search.placeholder = `请输入${item.title}`
search.field = item.dataIndex
}
}
http
.get(`${api.getQueryInfo}/${rpConfigId}`)
.then((res: any) => {
if (res.success) {
analysis(res.result)
} else {
analysis()
}
})
.catch((err) => {
analysis()
})
}
const getRpColumns = () => {
return new Promise<void>((resolve, reject) => {
if (loadedColumns) {
resolve()
} else {
http
.get(`${api.getColumns}/${props.code}`)
.then((res: any) => {
if (res.success) {
loadedColumns = true
const { result } = res
navTitle.value = result.cgRpConfigName
dictOptions.value = result.dictOptions
rpConfigId = result.cgRpConfigId
const fileds = props.showFiled.split(',')[0]
result.columns?.forEach((item) => {
if (fileds.includes(item.dataIndex)) {
columns.value.push(item)
}
})
// 最少展示三个字段(字段够多时)
const minNum = 3
if (columns.value.length < minNum) {
result.columns?.forEach((item) => {
const findItem = columns.value.find((o) => o.dataIndex == item.dataIndex);
if (!findItem) {
if (columns.value.length < minNum) {
columns.value.push(item)
}
}
})
}
getQueryInfo()
resolve()
} else {
reject()
}
})
.catch((err) => {
reject()
})
}
})
}
const queryList = (pageNo, pageSize) => {
const pararms = { pageNo, pageSize }
if (search.keyword) {
pararms[search.field] = `*${search.keyword}*`
}
getRpColumns()
.then(() => {
http
.get(`${api.getData}/${rpConfigId}`, pararms)
.then((res: any) => {
if (res.success && res.result.records) {
paging.value.complete(res.result.records ?? [])
} else {
paging.value.complete(false)
}
})
.catch((err) => {})
})
.catch((err) => {})
}
</script>
<style lang="scss" scoped>
:deep(.wd-cell) {
--wot-color-white: tranparent;
--wot-cell-padding: 0;
.wd-cell__wrapper {
--wot-cell-wrapper-padding: 0;
}
.wd-cell__left {
display: none;
}
}
:deep(.wd-checkbox-group) {
--wot-checkbox-bg: tranparent;
}
:deep(.wd-radio-group) {
--wot-radio-bg: tranparent;
}
.list {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
line-height: 1.8;
padding: 16px;
margin-top: 16px;
.left {
display: flex;
justify-content: center;
flex-direction: column;
.row {
display: flex;
}
}
.right {
:deep(.wd-checkbox) {
margin-bottom: 0;
}
}
}
.wrap {
height: 100%;
}
</style>

View File

@@ -0,0 +1,219 @@
<template>
<view class="PopupDict">
<view class="inputArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-select-picker
v-model="showText"
:columns="options"
readonly
:type="multi ? 'checkbox' : 'radio'"
@click="handleClick"
v-bind="$attrs"
></wd-select-picker>
<view v-if="!!showText && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<popupReportModal
v-if="reportModal.show"
:code="code"
:showFiled="labelFiled"
:multi="multi"
@close="handleClose"
@change="handleChange"
></popupReportModal>
</view>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import popupReportModal from '@/components/Popup/components/popupReportModal.vue'
defineOptions({
name: 'PopupDict',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
dictCode: {
type: String,
required: true,
default: '',
},
modelValue: {
type: String,
default: '',
},
multi: {
type: Boolean,
default: false,
},
spliter: {
type: String,
default: ',',
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const showText = ref<any>(props.multi ? [] : '')
const options = ref<any>([])
const cgRpConfigId = ref('')
const code = ref(props.dictCode.split(',')[0])
const labelFiled = ref(props.dictCode.split(',')[1])
const valueFiled = ref(props.dictCode.split(',')[2])
const reportModal = reactive({
show: false,
})
//定义请求url信息
const configUrl = reactive({
getColumns: '/online/cgreport/api/getRpColumns/',
getData: '/online/cgreport/api/getData/',
})
if (!code.value || !valueFiled.value || !labelFiled.value) {
toast.error('popupDict参数未正确配置!')
}
/**
* 监听value数值
*/
watch(
() => props.modelValue,
(val) => {
const callBack = () => {
if (props.multi) {
showText.value = val && val.length > 0 ? val.split(props.spliter) : []
} else {
showText.value = val ?? ''
}
}
if (props.modelValue) {
if (cgRpConfigId.value) {
loadData({ callBack })
} else {
loadColumnsInfo({ callBack })
}
} else {
callBack()
}
},
{ immediate: true },
)
watch(
() => showText.value,
(val) => {
let result
if (props.multi) {
result = val.join(',')
} else {
result = val
}
nextTick(() => {
emit('change', result)
emit('update:modelValue', result)
})
},
)
/**
* 加载列信息
*/
function loadColumnsInfo({ callBack }) {
let url = `${configUrl.getColumns}${code.value}`
http
.get(url)
.then((res: any) => {
if (res.success) {
cgRpConfigId.value = res.result.cgRpConfigId
loadData({ callBack })
}
})
.catch((err) => {
callBack?.()
})
}
function loadData({ callBack }) {
let url = `${configUrl.getData}${unref(cgRpConfigId)}`
http
.get(url, { ['force_' + valueFiled.value]: props.modelValue })
.then((res: any) => {
let data = res.result
if (data.records?.length) {
options.value = data.records.map((item) => {
return { value: item[valueFiled.value], label: item[labelFiled.value] }
})
}
})
.finally(() => {
callBack?.()
})
}
/**
* 传值回调
*/
function callBack(rows) {
const dataOptions: any = []
const dataValue: any = []
let result
rows.forEach((item) => {
dataOptions.push({ value: item[valueFiled.value], label: item[labelFiled.value] })
dataValue.push(item[valueFiled.value])
})
options.value = dataOptions
if (props.multi) {
showText.value = dataValue
result = dataValue.join(props.spliter)
} else {
showText.value = dataValue[0]
result = dataValue[0]
}
nextTick(() => {
emit('change', result)
emit('update:modelValue', result)
})
}
// 清空
const handleClear = () => {
if (!props.disabled) {
showText.value = ''
handleChange([])
}
}
const handleClick = () => {
if (!props.disabled) {
reportModal.show = true
}
}
const handleClose = () => {
reportModal.show = false
}
const handleChange = (data) => {
console.log('选中的值:', data)
callBack(data)
}
</script>
<style lang="scss" scoped>
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<view class="ProgressMap">
<view v-if="title.length" class="title">{{ title }}</view>
<view
:class="{
stepBox: true,
active: item.activeStep,
'u-iconfont': true,
'u-icon-clock': !item.activeStep,
'u-icon-success': item.activeStep,
}"
v-for="(item, index) in dataSource"
:key="index"
>
<view :class="{ stepContent: true, active: item.activeStep }">
<template v-for="(inItem, inIndex) in item.data" :key="inIndex">
<view v-if="inItem.label" class="item">
{{ inItem.label }}
</view>
</template>
</view>
<view v-if="item.file" class="file">
<template v-for="(file, inIndex) in item.file" :key="inIndex">
<view :data-url="file.filePath" class="item" @click="handClick(file)">
<view class="u-iconfont u-icon-link"></view>
<view class="text ellipsis">{{ file.fileName }}</view>
</view>
</template>
</view>
</view>
<ImgPreview
v-if="imgPreview.show"
:urls="imgPreview.urls"
@close="() => (imgPreview.show = false)"
></ImgPreview>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { downloadFile } from '@/common/uitls'
import { getFileAccessHttpUrl } from '@/common/uitls'
const props: any = defineProps({
title: {
type: String,
default: '',
},
dataSource: {
type: Array,
default: () => [{ activeStep: false, data: [] }],
},
})
const imgPreview = reactive({
show: false,
urls: [],
})
const handClick = (file) => {
const suffix = file.filePath.split('.').pop()
if (['gif', 'png', 'jpg', 'jpeg'].includes(suffix)) {
imgPreview.urls = [getFileAccessHttpUrl(file.filePath)]
imgPreview.show = true
} else {
downloadFile(file.filePath)
}
}
</script>
<style lang="scss" scoped>
.ProgressMap {
background-color: #fff;
}
.title {
padding: 20upx;
font-weight: bold;
}
.stepBox {
position: relative;
line-height: 1;
padding: 32upx 32upx 32upx 120upx;
color: #aaaaaa;
&.active {
color: #39b54a;
}
&::before {
position: absolute;
left: 38upx;
background-color: #fff;
z-index: 9;
top: 50upx;
border-radius: 50%;
margin: 8upx;
}
&::after {
content: '';
display: block;
position: absolute;
width: 0.5px;
background-color: #ddd;
left: 60upx;
height: 100%;
top: 0;
z-index: 8;
}
.stepContent {
padding: 10upx 50upx 10upx 30upx;
border-radius: 8upx;
background-color: #f0f0f0;
color: #333333;
font-size: 13px;
line-height: 1.6;
&.active {
font-weight: 500;
background-color: #39b54a;
color: #fff;
box-shadow: 0 0 5px #39b54a;
}
}
.file {
padding-top: 10px;
.item {
margin-bottom: 5px;
display: flex;
font-size: 14px;
align-items: center;
color: var(--color-blue);
line-height: 1.2;
.u-iconfont {
font-size: 14px;
margin-right: 5px;
color: #666;
}
}
}
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<wd-popup v-model="show" position="right" @close="handleClose">
<view class="content">
<wd-text v-if="title" :text="title"></wd-text>
<wd-cell-group border>
<wd-radio-group v-model="checked">
<template v-for="(item, index) in options">
<wd-cell :title="item.title" clickable @click="handleSelected(item)">
<wd-radio :value="item.key"></wd-radio>
</wd-cell>
</template>
</wd-radio-group>
</wd-cell-group>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { hasRoute, cache } from '@/common/uitls'
import { ref } from 'vue'
const eimt = defineEmits(['change', 'close'])
const show = ref(true)
const props = defineProps(['title', 'data', 'options', 'checked'])
const checked = ref(props.checked)
const handleClose = () => {
show.value = false
setTimeout(() => {
eimt('close')
}, 300)
}
const handleSelected = (item) => {
checked.value = item.key
eimt('change', { option: item, data: props.data })
handleClose()
}
</script>
<style lang="scss" scoped>
.content {
max-width: 200px;
padding: 10px;
.wd-text.is-default {
font-size: 14px;
color: #666;
}
.wd-cell {
padding-left: 0;
--wot-cell-label-color: #444;
--wot-cell-label-fs: 14px;
&.red {
color: red;
--wot-cell-label-color: red;
}
}
.wd-cell-group {
:deep(.wd-cell__wrapper) {
align-items: center;
.wd-cell__right {
flex: none;
width: 24px;
}
.wd-radio {
margin-top: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,285 @@
<template>
<view class="CategorySelect">
<view class="pickerArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
v-model="showText"
clearable
readonly
></wd-input>
<view v-if="!!showText && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup v-if="popupShow" position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<scroll-view class="flex-1" scroll-y>
<DaTree
:data="treeData"
:labelField="labelKey"
:valueField="rowKey"
loadMode
:showCheckbox="multiple"
:showRadioIcon="false"
:checkStrictly="true"
:defaultCheckedKeys="defaultCheckedKeys"
:loadApi="asyncLoadTreeData"
@change="handleTreeChange"
></DaTree>
</scroll-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray } from '@/utils/is'
import { getPlaceholder } from '@/common/uitls'
defineOptions({
name: 'SelectDept',
})
const props = defineProps({
modelValue: {
type: [Array, String],
},
// 是否支持多选
multiple: {
type: Boolean,
default: true,
},
rowKey: {
type: String,
default: 'key',
},
labelKey: {
type: String,
default: 'title',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const api = {
loadDictItem: '/sys/category/loadDictItem/',
queryDepartTreeSync: '/sys/sysDepart/queryDepartTreeSync',
}
const showText = ref('')
const popupShow = ref(false)
const treeData = ref<any[]>([])
const treeValue = ref([])
const defaultCheckedKeys: any = ref(props.multiple ? [] : '')
const handleClick = () => {
if (!props.disabled) {
popupShow.value = true
}
}
const cancel = () => {
popupShow.value = false
}
const confirm = () => {
const titles = treeValue.value.map((item) => item.title)
const keys = treeValue.value.map((item) => item.key).join(',')
showText.value = titles.join(',')
popupShow.value = false
emit('update:modelValue', keys)
emit('change', keys)
}
const handleTreeChange = (value, record) => {
const { originItem, checkedStatus } = record
if (checkedStatus) {
// 选中
if (props.multiple) {
treeValue.value.push({ key: originItem[props.rowKey], title: originItem[props.labelKey] })
} else {
treeValue.value = [{ key: originItem[props.rowKey], title: originItem[props.labelKey] }]
}
} else {
// 取消
if (props.multiple) {
const findIndex = treeValue.value.findIndex(
(item) => item[props.rowKey] == originItem[props.rowKey],
)
if (findIndex != -1) {
treeValue.value.splice(findIndex, 1)
}
} else {
treeValue.value = []
}
}
}
const transformField = (result) => {
for (let i of result) {
i.value = i.key
if (i.isLeaf == false) {
i.leaf = false
} else if (i.isLeaf == true) {
i.leaf = true
}
}
}
// 异步加载
const asyncLoadTreeData = ({ originItem }) => {
return new Promise<void>((resolve) => {
let param = {
pid: originItem.key,
primaryKey: props.rowKey,
}
http
.get(api.queryDepartTreeSync, param)
.then((res: any) => {
if (res.success) {
const { result } = res
transformField(result)
resolve(result)
} else {
resolve(null)
}
})
.catch((err) => resolve(null))
})
}
// 加载根节点
function loadRoot() {
let param = {
primaryKey: props.rowKey,
}
http
.get(api.queryDepartTreeSync, param)
.then((res: any) => {
if (res.success) {
const { result } = res
if (result && result.length > 0) {
transformField(result)
treeData.value = result
}
} else {
console.error('部门组件加载根节点数据失败~')
}
})
.catch((err) => {
console.error('部门组件加载根节点数据失败~')
})
}
// 翻译input内的值
function loadItemByCode() {
let value = props.modelValue
if(value){
console.log('部门组件翻译props.modelValue', props.modelValue)
if (isArray(props.modelValue)) {
// @ts-ignore
value = value.join(',')
}
if (value === treeData.value.map((item) => item.key).join(',')) {
// 说明是刚选完,内部已有翻译。不需要再请求
return
}
value = (value as string).trim()
if (value) {
http
.get(api.queryDepartTreeSync, { ids: value })
.then((res: any) => {
if (res.success) {
const { result = [] } = res
showText.value = result.map((item) => item[props.labelKey]).join(',')
if (props.multiple) {
defaultCheckedKeys.value = typeof value === 'string' ? value.split(',') : [value]
} else {
defaultCheckedKeys.value = value
}
} else {
}
})
.catch((err) => {})
}
}
}
// 清空
const handleClear = () => {
showText.value = ''
treeValue.value = []
confirm()
}
watch(
() => props.modelValue,
() => {
loadItemByCode()
},
{ deep: true, immediate: true },
)
loadRoot()
</script>
<style lang="scss" scoped>
:deep(.wd-popup-wrapper) {
.wd-popup {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
}
.content {
height: 50vh;
width: 100vw;
display: flex;
flex-direction: column;
.operation {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 5px;
position: relative;
&::before {
content: ' ';
position: absolute;
bottom: 0;
left: 8px;
right: 8px;
height: 1px;
background-color: #e5e5e5;
}
.cancel,
.confrim {
font-size: 15px;
height: 40px;
min-width: 40px;
text-align: center;
}
.confrim {
color: var(--wot-color-theme);
}
}
:deep(.da-tree) {
.da-tree-item__checkbox {
// display: none;
}
}
}
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<view class="SelectUser">
<template v-if="showType === 'form'">
<view class="inputArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view
v-if="!!showText && !disabled"
class="u-iconfont u-icon-close"
@click.stop="handleClear"
></view>
</view>
</template>
<template v-else>
<view class="list">
<template v-for="(item, index) in selectedUser" :key="item.id">
<view
v-if="maxShowUser === -1 || index < maxShowUser"
class="user"
@click="handleRemove(index)"
>
<view v-if="isDelUser" class="u-iconfont u-icon-close"></view>
<wd-img
v-if="getAvatar(item.avatar)"
radius="50%"
width="40px"
height="40px"
:src="getAvatar(item.avatar)"
></wd-img>
<view class="name">{{ item.realname }}</view>
</view>
</template>
<view v-if="isAddUser" class="u-iconfont u-icon-newAdd" @click="handleClick"></view>
</view>
</template>
<SelectUserModal
v-if="modalShow"
:selected="modelValue"
:defaultSelectedValue="defaultValue"
:selectedUser="selectedUser"
:modalTitle="modalTitle"
:maxSelectCount="maxSelectCount"
:multi="!isRadioSelection"
:rowKey="rowKey"
:readonlyUser="readonlyUser"
@change="handleChange"
@close="() => (modalShow = false)"
></SelectUserModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray, isString, isNumber } from '@/utils/is'
import SelectUserModal from './components/SelectUserModal.vue'
import { getPlaceholder, getFileAccessHttpUrl } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
defineOptions({
name: 'SelectUser',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: [Array, String],
default: '',
},
defaultValue: {
type: [Array, String],
default: '',
},
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
isRadioSelection: {
type: Boolean,
default: false,
},
modalTitle: {
type: String,
default: '选择用户',
},
maxSelectCount: {
type: Number,
},
showType: {
type: String,
default: 'form', // form、card
},
// showType为card时有效
isDelUser: {
type: Boolean,
default: true,
},
// showType为card时有效
isAddUser: {
type: Boolean,
default: true,
},
// showType为card时有效
maxShowUser: {
type: Number,
default: -1,
},
// 只读用户(添加的用户不可取消)
readonlyUser: {
type: Array,
default: [],
},
disabled: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change', 'getSelectdAllData'])
const api = {
list: '/sys/user/list',
}
const showText = ref('')
const modalShow = ref(false)
const selectedUser = ref([])
const getAvatar = (url) => {
let result = getFileAccessHttpUrl(url)
if (result.length) {
return result
} else {
return defaultAvatar
}
}
const handleRemove = (index?) => {
if (props.isDelUser) {
if (isNumber(index)) {
selectedUser.value.splice(index, 1)
} else {
selectedUser.value.pop()
}
handleChange(selectedUser.value)
}
}
// 翻译
const transform = () => {
let value = props.modelValue
let len
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length
value = value.join(',')
} else {
len = value.split(',').length
}
value = value.trim()
if (value) {
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value }
http.get(api.list, params).then((res: any) => {
if (res.success) {
const records = res.result?.records ?? []
showText.value = records.map((item) => item[props.labelKey]).join(',')
selectedUser.value = records
} else {
console.log('翻译失败~')
}
})
}
} else {
showText.value = ''
}
}
// 打开popup
const handleClick = () => {
console.log('handleClick', !props.disabled)
console.log('handleClick', props)
if (!props.disabled) {
modalShow.value = true
}
}
// 清空
const handleClear = () => {
showText.value = ''
handleChange([])
}
const handleChange = (data) => {
const rowkey = data.map((item) => item[props.rowKey]).join(',')
const labelKey = data.map((item) => item[props.labelKey]).join(',')
showText.value = labelKey
selectedUser.value = data
emit('update:modelValue', rowkey)
emit('change', rowkey)
emit('getSelectdAllData', data)
}
watch(
() => props.modelValue,
() => {
transform()
},
{ immediate: true },
)
</script>
<style lang="scss" scoped>
.list {
display: flex;
align-items: center;
flex-wrap: wrap;
color: #666;
.user {
padding: 7px;
text-align: center;
position: relative;
.u-iconfont {
font-size: 14px;
position: absolute;
right: 4px;
top: 7px;
z-index: 1;
/* background: white; */
width: 14px;
height: 14px;
border-radius: 50%;
line-height: 1;
}
.name {
font-size: 12px;
}
}
.u-iconfont {
font-size: 22px;
}
.u-icon-newAdd {
margin-left: 8px;
}
.u-icon-remove {
color: var(--color-red);
}
}
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,338 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="modalTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<view class="wrap">
<z-paging
ref="paging"
:fixed="false"
v-model="dataList"
@query="queryList"
:default-page-size="15"
>
<template #top>
<wd-search
hide-cancel
:placeholder="search.placeholder"
v-model="search.keyword"
@search="handleSearch"
@clear="handleClear"
/>
</template>
<template v-if="multi">
<wd-checkbox-group shape="square" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<view class="list" @click="hanldeCheck(index, item[rowKey])">
<view class="left text-gray-5">
<wd-img
custom-class="avatar"
radius="50%"
height="40"
width="40"
:src="getAvatar(item.avatar)"
></wd-img>
<view class="subContent">
<text>账号{{ item.username }}</text>
<text>姓名{{ item.realname }}</text>
</view>
</view>
<view class="right" @click.stop>
<wd-checkbox
ref="checkboxRef"
:disabled="readonlyUser.includes(item[rowKey])"
:modelValue="item[rowKey]"
></wd-checkbox>
</view>
</view>
</template>
</wd-checkbox-group>
</template>
<template v-else>
<wd-radio-group shape="dot" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<wd-cell>
<view class="list" @click="hanldeCheck(index, item[rowKey])">
<view class="left text-gray-5">
<wd-img
custom-class="avatar"
radius="50%"
height="40"
width="40"
:src="getAvatar(item.avatar)"
></wd-img>
<view class="subContent">
<text>账号{{ item.username }}</text>
<text>姓名{{ item.realname }}</text>
</view>
</view>
<view class="right" @click.stop>
<wd-radio :value="item[rowKey]"></wd-radio>
</view>
</view>
</wd-cell>
</template>
</wd-radio-group>
</template>
</z-paging>
</view>
</PageLayout>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray, isString } from '@/utils/is'
import { cache, getFileAccessHttpUrl } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
defineOptions({
name: 'SelectUserModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
multi: {
type: Boolean,
default: true,
},
modalTitle: {
type: String,
default: '选择用户',
},
maxSelectCount: {
type: Number,
},
// 这是用户id
selected: {
type: [Array, String],
default: '',
},
// 这是用户id只是默认勾选在弹窗中勾选
defaultSelectedValue: {
type: [Array, String],
default: '',
},
// 这是用户全数据项包含idusername、realname
selectedUser: {
type: Array,
default: () => [],
},
rowKey: {
type: String,
default: 'username',
},
// 只读用户
readonlyUser: {
type: Array,
default: [],
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
const api = {
selectUserList: '/sys/user/selectUserList',
userlist: '/sys/user/list',
}
const paging = ref(null)
const dataList = ref([])
const checkedValue: any = ref(props.multi ? [] : '')
const checkboxRef = ref(null)
const search = reactive({
keyword: '',
placeholder: '输入姓名可搜索',
field: 'realname',
})
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const handleConfirm = () => {
if (checkedValue.value.length == 0) {
toast.warning('还没选择用户~')
return
}
const result = []
let value = checkedValue.value
if (!Array.isArray(checkedValue.value)) {
value = [checkedValue.value]
}
value.forEach((rowKey, index) => {
const findIndex = dataList.value.findIndex((item) => item[props.rowKey] === rowKey)
if (findIndex == -1) {
// 传进来选中的用户可能在第二页(还没加载进来)
const index = props.selectedUser.findIndex((item) => item[props.rowKey] === rowKey)
if (index != -1) {
result.push(props.selectedUser[index])
} else {
// 传进来defaultSelectedValue的用户可能在第二页还没加载进来
if (isArray(props.defaultSelectedValue)) {
const index = props.defaultSelectedValue.findIndex((item) => item === rowKey)
index != -1 && result.push({ [props.rowKey]: props.defaultSelectedValue[index] })
} else {
props.defaultSelectedValue == rowKey && result.push({ [props.rowKey]: rowKey })
}
}
} else {
result.push(dataList.value[findIndex])
}
})
show.value = false
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
// 搜索
function handleSearch() {
paging.value.reload()
}
// 清除搜索条件
function handleClear() {
search.keyword = ''
handleSearch()
}
const hanldeCheck = (index, username) => {
if (props.multi) {
if (Array.isArray(checkboxRef.value)) {
checkboxRef.value[index].toggle()
nextTick(() => {
if (props.maxSelectCount) {
if (checkedValue.value.length > props.maxSelectCount) {
toast.warning(`最多可选择${props.maxSelectCount}个用户`)
// 超过个数取消
checkboxRef.value[index].toggle()
}
}
})
}
} else {
checkedValue.value = username
}
}
const getAvatar = (url) => {
let result = getFileAccessHttpUrl(url)
if (result.length) {
return result
} else {
return defaultAvatar
}
}
const queryList = (pageNo, pageSize) => {
const pararms = { pageNo, pageSize, column: 'createTime', order: 'desc' }
if (search.keyword) {
pararms[search.field] = `*${search.keyword}*`
}
http
.get(`${api.userlist}`, pararms)
.then((res: any) => {
if (res.success && res.result.records) {
paging.value.complete(res.result.records ?? [])
} else {
paging.value.complete(false)
}
})
.catch((err) => {})
}
const init = () => {
if (props.selected.length) {
if (props.multi) {
if (isArray(props.selected)) {
checkedValue.value = props.selected
} else if (isString(props.selected)) {
checkedValue.value = props.selected.split(',')
}
} else {
if (isString(props.selected)) {
checkedValue.value = props.selected
} else if (isArray(props.selected)) {
checkedValue.value = props.selected.join(',')
}
}
} else {
if (props.defaultSelectedValue.length) {
if (props.multi) {
if (isArray(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue
} else if (isString(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue.split(',')
}
} else {
if (isString(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue
} else if (isArray(props.defaultSelectedValue)) {
checkedValue.value = props.defaultSelectedValue.join(',')
}
}
}
}
}
init()
</script>
<style lang="scss" scoped>
:deep(.wd-cell) {
--wot-color-white: tranparent;
--wot-cell-padding: 0;
.wd-cell__wrapper {
--wot-cell-wrapper-padding: 0;
}
.wd-cell__left {
display: none;
}
}
:deep(.wd-checkbox-group) {
--wot-checkbox-bg: tranparent;
}
:deep(.wd-radio-group) {
--wot-radio-bg: tranparent;
}
.list {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
padding: 16px;
margin-top: 16px;
.left {
display: flex;
align-items: center;
text-align: left;
:deep(.avatar) {
margin-right: 8px;
background-color: #e9e9e9;
}
.subContent {
display: flex;
flex-direction: column;
}
}
.right {
:deep(.wd-checkbox) {
margin-bottom: 0;
}
}
}
.wrap {
height: 100%;
}
:deep(.wd-popup-wrapper) {
.wd-popup {
top: 100px;
}
}
</style>

View File

@@ -0,0 +1,159 @@
<template>
<view class="SelectUser">
<view class="inputArea" :class="{ clear: !!showText }" @click.stop="handleClick">
<wd-input
:placeholder="getPlaceholder($attrs)"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view
v-if="!!showText && !disabled"
class="u-iconfont u-icon-close"
@click.stop="handleClear"
></view>
</view>
<SelectUserModal
v-if="modalShow"
:selected="modelValue"
:selectedUser="selectedUser"
:modalTitle="modalTitle"
:rowKey="rowKey"
@change="handleChange"
@close="() => (modalShow = false)"
></SelectUserModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray, isString, isNumber } from '@/utils/is'
import SelectUserModal from './components/SelectUserModal.vue'
import { getPlaceholder, getFileAccessHttpUrl } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
defineOptions({
name: 'SelectUserByDepart',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: [Array, String],
default: '',
},
labelKey: {
type: String,
default: 'realname',
},
rowKey: {
type: String,
default: 'username',
},
modalTitle: {
type: String,
default: '选择用户',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change'])
const api = {
list: '/sys/user/list',
}
const showText = ref('')
const modalShow = ref(false)
const selectedUser = ref([])
// 翻译
const transform = () => {
let value = props.modelValue
let len
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length
value = value.join(',')
} else {
len = value.split(',').length
}
value = value.trim()
if (value) {
// 如果value的值在selectedUser中存在则不请求翻译
let isNotRequestTransform = false
isNotRequestTransform = value
.split(',')
.every((value) => !!selectedUser.value.find((item) => item[props.rowKey] === value))
if (isNotRequestTransform) {
return
}
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value }
http.get(api.list, params).then((res: any) => {
if (res.success) {
const records = res.result?.records ?? []
showText.value = records.map((item) => item[props.labelKey]).join(',')
selectedUser.value = records
} else {
console.log('翻译失败~')
}
})
}
} else {
showText.value = ''
}
}
// 打开popup
const handleClick = () => {
console.log('handleClick', !props.disabled)
console.log('handleClick', props)
if (!props.disabled) {
modalShow.value = true
}
}
// 清空
const handleClear = () => {
showText.value = ''
handleChange([])
}
const handleChange = (data) => {
const rowkey = data.map((item) => item[props.rowKey]).join(',')
const labelKey = data.map((item) => item[props.labelKey]).join(',')
showText.value = labelKey
selectedUser.value = data
emit('update:modelValue', rowkey)
emit('change', rowkey)
}
watch(
() => props.modelValue,
() => {
transform()
},
{ immediate: true },
)
</script>
<style lang="scss" scoped>
.inputArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,841 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout :navTitle="modalTitle" type="popup" @navBack="handleCancel">
<view class="wrap">
<view class="wrap-content">
<wd-search
hide-cancel
placeholder="请输入姓名/部门"
v-model="searchText"
@search="handleSearch"
@clear="handleSearch"
/>
<view class="main-content">
<scroll-view scroll-y>
<template v-if="searchResult.depart.length || searchResult.user.length">
<view class="search-result">
<template v-if="searchResult.user.length">
<view class="search-user solid-top">
<text class="search-user-title">人员</text>
<template v-for="item in searchResult.user" :key="item.id">
<view
class="search-user-item solid-top"
@click="handleSearchUserCheck(item)"
>
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleSearchUserCheck(item, $event)"
/>
</view>
<view class="right">
<view class="search-user-item-circle">
<wd-img
width="36"
height="36px"
v-if="item.avatar"
:src="getFileAccessHttpUrl(item.avatar)"
/>
</view>
<view class="search-user-item-info">
<view class="search-user-item-name">{{ item.realname }}</view>
<view class="search-user-item-org">{{ item.orgCodeTxt }}</view>
</view>
</view>
</view>
</template>
</view>
</template>
<template v-if="searchResult.depart.length">
<view class="search-depart solid-top">
<text class="search-depart-title">部门</text>
<template v-for="item in searchResult.depart" :key="item.id">
<view
class="search-depart-item solid-top"
@click="handleSearchDepartClick(item)"
>
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleSearchDepartCheck($event, item)"
/>
</view>
<view class="search-depart-item-name">{{ item.departName }}</view>
<wd-icon name="arrow-right" size="16px"></wd-icon>
</view>
</template>
</view>
</template>
</view>
</template>
<template v-else>
<view v-if="breadcrumb.length" class="breadcrumb-wrap solid-top">
<Breadcrumb
separator="/"
:items="[
{ title: '首页', icon: 'home' },
...breadcrumb.map((item) => ({ title: item.departName, ...item })),
]"
@click="(item, index) => handleBreadcrumbClick(index === 0 ? undefined : item)"
>
<template #item-0="{ item }">
<wd-icon name="home" size="18px"></wd-icon>
</template>
</Breadcrumb>
</view>
<div v-if="currentDepartUsers.length">
<!-- 当前部门用户树 -->
<div class="depart-users-tree solid-top">
<div v-if="!currentDepartTree.length" class="allChecked">
<wd-checkbox
shape="square"
v-model="currentDepartAllUsers"
@change="handleAllUsers"
>
全选
</wd-checkbox>
</div>
<template v-for="item in currentDepartUsers" :key="item.id">
<div
class="depart-users-tree-item solid-top"
@click="handleDepartUsersTreeCheck(item)"
>
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleDepartUsersTreeCheck(item, $event)"
/>
</view>
<div class="right">
<div class="depart-users-tree-item-circle">
<wd-img
width="36"
height="36px"
v-if="item.avatar"
:src="getFileAccessHttpUrl(item.avatar)"
/>
</div>
<div class="depart-users-tree-item-name">{{ item.realname }}</div>
</div>
</div>
</template>
</div>
</div>
<!-- 部门树 -->
<div v-if="currentDepartTree.length" class="depart-tree">
<template v-for="item in currentDepartTree" :key="item.id">
<div class="depart-tree-item solid-top" @click="handleDepartTreeClick(item)">
<view @click.stop>
<wd-checkbox
shape="square"
v-model="item.checked"
@change="($event) => handleDepartTreeCheck($event, item)"
/>
</view>
<div class="depart-tree-item-name">{{ item.departName }}</div>
<wd-icon name="arrow-right" size="16px"></wd-icon>
</div>
</template>
</div>
<div
v-if="currentDepartTree.length === 0 && currentDepartUsers.length === 0"
class="no-data"
>
<wd-status-tip image="content" tip="暂无内容" />
</div>
</template>
</scroll-view>
<view v-if="showSelectedUser" class="selected-user">
<SelectedUser :selectedUsers="selectedUsers" @del="handleDelUser"></SelectedUser>
</view>
</view>
</view>
<view class="wrap-footer">
<view class="text" @click="() => (showSelectedUser = !showSelectedUser)">
<view>已选</view>
<view class="num">{{ selectedUsers.length }}</view>
<view></view>
<text class="tip">(查看选中用户)</text>
</view>
<wd-button type="primary" @click="handleConfirm">确定</wd-button>
</view>
</view>
</PageLayout>
<wd-toast :selector="selector"></wd-toast>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray, isString } from '@/utils/is'
import { cache, getFileAccessHttpUrl, uuid } from '@/common/uitls'
import defaultAvatar from '@/static/default-avatar.png'
import SelectedUser from './SelectedUser.vue'
defineOptions({
name: 'SelectUserModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
multi: {
type: Boolean,
default: true,
},
modalTitle: {
type: String,
default: '选择用户',
},
maxSelectCount: {
type: Number,
},
// 这是用户全数据项包含idusername、realname
selectedUser: {
type: Array,
default: () => [],
},
rowKey: {
type: String,
default: 'username',
},
})
const api = {
selectUserList: '/sys/user/selectUserList',
userlist: '/sys/user/list',
queryTreeList: '/sys/sysDepart/queryTreeList',
getTableList: '/sys/user/queryUserComponentData',
}
const emit = defineEmits(['change', 'close'])
const selector = uuid()
const toast = useToast(selector)
const show = ref(true)
// 搜索文本
const searchText = ref('')
const breadcrumb = ref<any[]>([])
// 部门树(整颗树)
const departTree = ref([])
// 当前部门树
const currentDepartTree = ref<any[]>([])
// 选中的部门节点
const checkedDepartIds = ref<string[]>([])
// 当前部门用户
const currentDepartUsers = ref([])
// 已选用户
const selectedUsers = ref<any[]>([])
// 全选
const currentDepartAllUsers = ref(false)
// 搜索结构
const searchResult: any = reactive({
depart: [],
user: [],
})
// 映射部门和人员的关系
const cacheDepartUser = {}
// 是否显示已选用户
const showSelectedUser = ref(false)
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const handleConfirm = () => {
if (selectedUsers.value.length == 0) {
toast.warning('还没选择用户~')
return
}
show.value = false
let result = []
let value = selectedUsers.value
if (!Array.isArray(selectedUsers.value)) {
value = [selectedUsers.value]
}
result = value
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
const getAvatar = (url) => {
let result = getFileAccessHttpUrl(url)
if (result.length) {
return result
} else {
return defaultAvatar
}
}
// 搜索人员/部门
const handleSearch = () => {
if (searchText.value) {
http
.get(`/sys/user/listAll`, {
column: 'createTime',
order: 'desc',
pageNo: 1,
pageSize: 100,
realname: `*${searchText.value}*`,
})
.then((res: any) => {
if (res.success) {
res.result.records?.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id)
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true
} else {
item.checked = false
}
})
searchResult.user = res.result.records ?? []
} else {
toast.warning(res.message)
}
})
searchResult.depart = getDepartByName(searchText.value) ?? []
showSelectedUser.value = false
} else {
searchResult.user = []
searchResult.depart = []
}
}
// 面包屑
const handleBreadcrumbClick = (item?) => {
// 先清空
currentDepartUsers.value = []
if (item) {
const findIndex = breadcrumb.value.findIndex((o) => o.id === item.id)
if (findIndex != -1) {
breadcrumb.value = breadcrumb.value.filter((item, index) => {
console.log(item)
return index <= findIndex
})
}
const data = getDepartTreeNodeById(item.id, departTree.value)
currentDepartTree.value = data.children
} else {
// 根节点
currentDepartTree.value = departTree.value
breadcrumb.value = []
}
}
// 点击部门树复选框触发
const handleDepartTreeCheck = ({ value }, item) => {
const target = { checked: value }
if (target.checked) {
// 选中
getUsersByDeptId(item['id']).then((users) => {
addUsers(users)
})
checkedDepartIds.value.push((item as any).id)
// 检查父节点下所有子节点是否选中
const parentItem = getDepartTreeParentById(item.id)
if (parentItem?.children) {
const isChildAllChecked = parentItem.children.every((item) => item.checked)
if (isChildAllChecked) {
parentItem.checked = true
} else {
parentItem.checked = false
}
}
} else {
// 取消选中
const findIndex = checkedDepartIds.value.findIndex((o: any) => o.id === item.id)
if (findIndex != -1) {
checkedDepartIds.value.splice(findIndex, 1)
}
// 如果父节点是选中,则需要取消
const parentItem = getDepartTreeParentById(item.id)
if (parentItem) {
parentItem.checked = false
}
getUsersByDeptId(item['id']).then((users) => {
users.forEach((item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id)
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1)
}
})
})
}
}
// 点击部门树节点触发
const handleDepartTreeClick = (item) => {
breadcrumb.value = [...breadcrumb.value, item]
if (item.children) {
// 有子节点,则显示部门
if (item.checked) {
// 父节点勾选,则子节点全部勾选
item.children.forEach((item) => {
item.checked = true
})
}
currentDepartTree.value = item.children
http
.get('/sys/sysDepart/getUsersByDepartId', {
id: item['id'],
})
.then((res: any) => {
const result = res.result ?? []
if (item.checked) {
// 父节点勾选,则默认勾选
result.forEach((item) => {
item.checked = true
})
}
currentDepartUsers.value = result
})
} else {
// 没有子节点,则显示用户
currentDepartTree.value = []
getTableList({
departId: item['id'],
}).then((res: any) => {
if (res.success) {
if (res?.result.records) {
let checked = true
res.result.records.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id)
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true
} else {
item.checked = false
checked = false
}
})
currentDepartAllUsers.value = checked
currentDepartUsers.value = res.result.records
} else {
toast.warning(res.message)
}
}
})
}
}
// 点击部门用户树复选框触发
const handleDepartUsersTreeCheck = (item, e?) => {
if (e) {
// 点击复选框时
item.checked = e.value
} else {
// 点击整条数据时
item.checked = !item.checked
}
if (item.checked) {
addUsers(item)
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id)
}
if (item.checked == false) {
// 有一个是false则全选false
currentDepartAllUsers.value = false
}
}
// 全选
const handleAllUsers = ({ target }) => {
const { checked } = target
if (checked) {
currentDepartUsers.value.forEach((item: any) => (item.checked = true))
addUsers(currentDepartUsers.value)
} else {
currentDepartUsers.value.forEach((item: any) => (item.checked = false))
selectedUsers.value = selectedUsers.value.filter((user) => {
const userId = user.id
const findItem = currentDepartUsers.value.find((item: any) => item.id === userId)
if (findItem) {
return false
} else {
return true
}
})
}
}
// 删除人员
const handleDelUser = (item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id)
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1)
}
const findItem: any = currentDepartUsers.value.find((user: any) => user.id === item.id)
if (findItem) {
findItem.checked = false
currentDepartAllUsers.value = false
}
}
// 点击搜索用户复选框
const handleSearchUserCheck = (item, e?) => {
if (!e) {
item.checked = !item.checked
}
if (item.checked) {
addUsers(item)
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id)
}
}
// 点击搜索部门复选框
const handleSearchDepartCheck = (e, item) => {
handleDepartTreeCheck(e, item)
}
// 点击搜索部门
const handleSearchDepartClick = (item) => {
searchResult.depart = []
searchResult.user = []
breadcrumb.value = getPathToNodeById(item.id)
handleDepartTreeClick(item)
}
// 添加人员到右侧
const addUsers = (users) => {
let newUsers: any = []
if (isArray(users)) {
// selectedUsers里面没有才添加防止重复
newUsers = users.filter((user: any) => !selectedUsers.value.find((item) => item.id === user.id))
} else {
if (!selectedUsers.value.find((user) => user.id === users.id)) {
// selectedUsers里面没有才添加防止重复
newUsers = [users]
}
}
selectedUsers.value = [...selectedUsers.value, ...newUsers]
const result = currentDepartUsers.value.every((item: any) => !!item.checked)
currentDepartAllUsers.value = result
}
// 解析参数
const parseParams = (params) => {
return params
}
const getQueryTreeList = (params?) => {
params = parseParams(params)
queryTreeList({ ...params }).then((res: any) => {
if (res.success) {
departTree.value = res.result
currentDepartTree.value = res.result
} else {
toast.warning(res.message)
}
})
}
// 根据部门id获取用户
const getTableList = (params) => {
params = parseParams(params)
return getTableListOrigin({ ...params })
}
const getUsersByDeptId = (id) => {
return new Promise<any[]>((resolve) => {
if (cacheDepartUser[id]) {
resolve(cacheDepartUser[id])
} else {
getTableList({
departId: id,
}).then((res: any) => {
if (res.success) {
cacheDepartUser[id] = res?.result?.records ?? []
if (res?.result?.records?.length) {
resolve(res.result.records ?? [])
}
} else {
toast.warning(res.message)
}
})
}
})
}
// 根据id获取根节点到当前节点路径
const getPathToNodeById = (id: string, tree = departTree.value, path = []): any[] => {
for (const node of tree) {
if ((node as any).id === id) {
return [...path]
}
if ((node as any).children) {
const foundPath = getPathToNodeById(id, (node as any).children, [...path, node])
if (foundPath.length) {
return foundPath
}
}
}
return []
}
// 根据id获取部门树父节点数据
const getDepartTreeParentById = (id: string, tree = departTree.value, parent = null): any => {
for (const node of tree) {
if ((node as any).id === id) {
return parent
}
if ((node as any).children) {
const found = getDepartTreeParentById(id, (node as any).children, node)
if (found) {
return found
}
}
}
return null
}
// 通过名称搜索部门支持模糊
const getDepartByName = (name: string, tree = departTree.value): any[] => {
const result: any[] = []
const search = (nodes: any[]) => {
for (const node of nodes) {
if (node.departName?.toLowerCase().includes(name.toLowerCase())) {
result.push(node)
}
if (node.children?.length) {
search(node.children)
}
}
}
search(tree)
return result
}
// 根据id获取部门树当前节点数据
const getDepartTreeNodeById = (id: string, tree = departTree.value): any => {
for (const node of tree) {
if ((node as any).id === id) {
return node
}
if ((node as any).children) {
const found = getDepartTreeNodeById(id, (node as any).children)
if (found) {
return found
}
}
}
return null
}
// 获取部门树列表
const queryTreeList = (params = {}) => {
return http.get(api.queryTreeList, { ...params })
}
// 根据部门id获取用户列表包含子孙部门
const getTableListOrigin = (params = {}) => {
return http.get(api.getTableList, { ...params })
}
// 初始化
const init = () => {
if (props.selectedUser.length) {
// 编辑时,传进来已选中的数据
selectedUsers.value = props.selectedUser
}
getQueryTreeList()
}
init()
</script>
<style lang="scss" scoped>
.wrap {
height: 100vh;
display: flex;
flex-direction: column;
}
.wrap-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-content {
flex: 1;
position: relative;
overflow: hidden;
color: var(--color-grey);
.breadcrumb-wrap {
background-color: #fff;
}
.selected-user {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #fff;
}
.no-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.depart-tree {
.depart-tree-item {
background-color: #fff;
padding: 0 16px;
line-height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
}
.depart-tree-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 8px;
}
}
.depart-users-tree {
.allChecked {
padding: 0 16px;
padding-bottom: 16px;
padding-top: 12px;
background-color: #fff;
:deep(.ant-checkbox-wrapper) {
font-size: 12px;
}
}
.depart-users-tree-item {
line-height: 50px;
padding: 0 16px;
display: flex;
align-items: center;
cursor: pointer;
background-color: #fff;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.depart-users-tree-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #aaa;
overflow: hidden;
:deep(image) {
display: block;
width: 100%;
height: 100%;
}
}
.depart-users-tree-item-name {
margin-left: 8px;
}
}
}
.search-depart {
margin-bottom: 8px;
.search-depart-title {
padding-left: 16px;
line-height: 50px;
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: var(--UI-FG-1);
}
.search-depart-item {
line-height: 50px;
display: flex;
align-items: center;
padding: 0 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.search-depart-item-name {
margin-left: 8px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.search-result {
background-color: #fff;
min-height: 100%;
}
.search-user {
margin-bottom: 8px;
.search-user-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
padding-left: 16px;
line-height: 50px;
color: var(--UI-FG-1);
}
.search-user-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.search-user-item-info {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 8px;
}
.search-user-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
background-color: #aaa;
}
.search-user-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-user-item-org {
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.wrap-footer {
padding: 10px;
padding-bottom: calc(constant(safe-area-inset-bottom) + 10px);
padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e5e5e5;
background-color: #fff;
.text {
display: flex;
align-items: center;
.num {
font-size: 18px;
margin: 0 5px;
color: var(--wot-color-theme);
}
}
.tip {
font-size: 11px;
color: #999;
}
}
.home-icon {
margin-right: 4px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,115 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
}
</route>
<template>
<scroll-view scroll-y class="selected-user-container solid-top">
<view class="user-container">
<template v-if="selectedUsers.length">
<view class="user-item" v-for="item in selectedUsers" :key="item.id">
<view class="user-item-avatar" @click="handledelUser(item)">
<wd-img
width="36px"
height="36px"
radius="50%"
:src="getFileAccessHttpUrl(item.avatar)"
></wd-img>
<view class="u-iconfont u-icon-close"></view>
</view>
<view class="user-item-name ellipsis">{{ item.realname }}</view>
</view>
</template>
<template v-else>
<view class="empty-data">
<wd-status-tip image="content" tip="无选中用户" />
</view>
</template>
</view>
</scroll-view>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
import { useMessage, useToast } from 'wot-design-uni'
import { useRouter } from '@/plugin/uni-mini-router'
import { useUserStore } from '@/store/user'
import { http } from '@/utils/http'
import { useParamsStore } from '@/store/page-params'
import { cache, getFileAccessHttpUrl } from '@/common/uitls'
defineOptions({
name: 'User',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps(['selectedUsers'])
const emit = defineEmits(['del'])
const router = useRouter()
const paramsStore = useParamsStore()
const userStore = useUserStore()
const toast = useToast()
const message = useMessage()
// 删除用户
const handledelUser = (item: any) => {
emit('del', item)
}
// 获取用户名的最后一个字符
const getLastCharacterOfName = (name: string): string => {
if (!name) return ''
return name.charAt(name.length - 1)
}
</script>
<style lang="scss" scoped>
//
.selected-user-container {
height: 100%;
}
.user-container {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
padding: 10px;
min-height: 100%;
.user-item {
position: relative;
display: flex;
width: 20%;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 10px;
.user-item-avatar {
position: relative;
background-color: #f5f5f5;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 5px;
}
.u-icon-close {
position: absolute;
top: -3px;
right: -5px;
border-radius: 50%;
}
}
.empty-data {
width: 100%;
min-height: 100%;
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,386 @@
<template>
<view class="TreeSelect">
<view class="pickerArea" :class="{ clear: !!showText }" @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
v-bind="$attrs"
readonly
v-model="showText"
></wd-input>
<view v-if="!!showText && !$attrs.disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
<wd-popup position="bottom" v-model="popupShow">
<view class="content">
<view class="operation">
<view class="cancel text-gray-5" @click.stop="cancel">取消</view>
<view class="confrim" @click.stop="confirm">确定</view>
</view>
<scroll-view class="flex-1" scroll-y>
<DaTree
v-if="popupShow"
:data="treeData"
labelField="title"
valueField="key"
loadMode
:showCheckbox="multiple"
:showRadioIcon="false"
:checkStrictly="true"
:loadApi="asyncLoadTreeData"
:defaultCheckedKeys="defaultCheckedKeys"
@change="handleTreeChange"
></DaTree>
</scroll-view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray } from '@/utils/is'
defineOptions({
name: 'TreeSelect',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
modelValue: {
type: [Array, String],
},
dict: {
type: String,
default: 'id',
},
pidValue: {
type: String,
default: '',
},
pidField: {
type: String,
default: 'pid',
},
hasChildField: {
type: String,
default: '',
},
condition: {
type: String,
default: '',
required: false,
},
converIsLeafVal: {
type: Number,
default: 1,
},
// 是否支持多选
multiple: {
type: Boolean,
default: true,
},
hiddenNodeKey: {
type: String,
default: '',
},
// url: {
// type: String,
// default: '',
// },
// params: {
// type: Object,
// default: () => ({}),
// },
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const api = {
loadTreeData: '/sys/dict/loadTreeData',
view: '/sys/dict/loadDictItem/',
}
const showText = ref('')
const popupShow = ref(false)
const treeData = ref<any[]>([])
const treeValue = ref([])
const tableName = ref<any>('')
const text = ref<any>('')
const code = ref<any>('')
const attrs = useAttrs()
const defaultCheckedKeys: any = ref(props.multiple ? [] : '')
const handleClick = () => {
if (!attrs.disabled) {
popupShow.value = true
}
}
const cancel = () => {
popupShow.value = false
}
const confirm = () => {
const titles = treeValue.value.map((item) => item.title)
const keys = treeValue.value.map((item) => item.key).join(',')
showText.value = titles.join(',')
popupShow.value = false
emit('update:modelValue', keys)
emit('change', keys)
}
const handleTreeChange = (value, record) => {
const { originItem, checkedStatus } = record
const { key, title } = originItem
if (checkedStatus) {
// 选中
if (props.multiple) {
treeValue.value.push({ key, title })
} else {
treeValue.value = [{ key, title }]
}
} else {
// 取消
if (props.multiple) {
const findIndex = treeValue.value.findIndex((item) => item.key == key)
if (findIndex != -1) {
treeValue.value.splice(findIndex, 1)
}
} else {
treeValue.value = []
}
}
}
const transformField = (result) => {
for (let i of result) {
i.value = i.key
if (i.leaf == false) {
i.isLeaf = false
} else if (i.leaf == true) {
i.isLeaf = true
}
}
}
// 异步加载
const asyncLoadTreeData = ({ originItem }) => {
return new Promise<void>((resolve) => {
let param = {
pid: originItem.key,
pidField: props.pidField,
hasChildField: props.hasChildField,
converIsLeafVal: props.converIsLeafVal,
condition: props.condition,
tableName: unref(tableName),
text: unref(text),
code: unref(code),
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
transformField(result)
resolve(result)
} else {
resolve(null)
}
})
.catch((err) => resolve(null))
})
}
// 加载根节点
function loadRoot() {
let param = {
pid: props.pidValue,
pidField: props.pidField,
hasChildField: props.hasChildField,
condition: props.condition,
converIsLeafVal: props.converIsLeafVal,
tableName: unref(tableName),
text: unref(text),
code: unref(code),
}
http
.get(api.loadTreeData, param)
.then((res: any) => {
if (res.success) {
const { result } = res
if (result && result.length > 0) {
transformField(result)
handleHiddenNode(result)
treeData.value = result
}
} else {
toast.warning('自定义树组件根节点数据加载失败~')
}
})
.catch((err) => {
toast.warning('自定义树组件根节点数据加载失败~')
})
}
// 翻译input内的值
function loadItemByCode() {
let value = props.modelValue
if(value){
if (isArray(props.modelValue)) {
// @ts-ignore
value = value.join()
}
if (value === treeData.value.map((item) => item.key).join(',')) {
// 说明是刚选完,内部已有翻译。不需要再请求
return
}
value = (value as string).trim()
if (value) {
http
.get(`${api.view}${props.dict}`, { key: value })
.then((res: any) => {
if (res.success) {
const { result = [] } = res
showText.value = result.join(',')
if (props.multiple) {
defaultCheckedKeys.value = typeof value === 'string' ? value.split(',') : [value]
} else {
defaultCheckedKeys.value = value
}
} else {
}
})
.catch((err) => {})
}
}
}
const initDictInfo = () => {
let arr = props.dict?.split(',')
tableName.value = arr[0]
text.value = arr[1]
code.value = arr[2]
}
const handleHiddenNode = (data) => {
if (props.hiddenNodeKey && data?.length) {
for (let i = 0, len = data.length; i < len; i++) {
const item = data[i]
if (item.key == props.hiddenNodeKey) {
data.splice(i, 1)
i--
len--
return
}
}
}
}
const validateProp = () => {
let mycondition = props.condition
return new Promise<void>((resolve, reject) => {
if (!mycondition) {
resolve()
} else {
try {
let test = JSON.parse(mycondition)
if (typeof test == 'object' && test) {
resolve()
} else {
toast.error('组件TreeSelect-condition传值有误需要一个json字符串!')
reject()
}
} catch (e) {
toast.error('组件TreeSelect-condition传值有误需要一个json字符串!')
reject()
}
}
})
}
// 清空
const handleClear = () => {
showText.value = ''
treeValue.value = []
confirm()
}
watch(
() => props.modelValue,
() => loadItemByCode(),
{ deep: true, immediate: true },
)
watch(
() => props.dict,
() => {
initDictInfo()
loadRoot()
},
)
watch(
() => props.hiddenNodeKey,
() => {
if (treeData.value?.length && props.hiddenNodeKey) {
handleHiddenNode(treeData.value)
treeData.value = [...treeData.value]
}
},
)
// 初始化
validateProp().then(() => {
initDictInfo()
loadRoot()
loadItemByCode()
})
</script>
<style lang="scss" scoped>
:deep(.wd-popup-wrapper) {
.wd-popup {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
}
.content {
height: 50vh;
width: 100vw;
display: flex;
flex-direction: column;
.operation {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 5px;
position: relative;
&::before {
content: ' ';
position: absolute;
bottom: 0;
left: 8px;
right: 8px;
height: 1px;
background-color: #e5e5e5;
}
.cancel,
.confrim {
font-size: 15px;
height: 40px;
min-width: 40px;
text-align: center;
}
.confrim {
color: var(--wot-color-theme);
}
}
:deep(.da-tree) {
.da-tree-item__checkbox {
// display: none;
}
}
}
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(14px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-input__body) {
padding-right: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,23 @@
declare module 'vue' {
export interface GlobalComponents {
BottomOperate: typeof import('./BottomOperate/BottomOperate.vue')['default']
Breadcrumb: typeof import('./Breadcrumb/Breadcrumb.vue')['default']
CategorySelect: typeof import('./CategorySelect/CategorySelect.vue')['default']
DateTime: typeof import('./DateTime/DateTime.vue')['default']
Grid: typeof import('./Grid/Grid.vue')['default']
HelpTipBubble: typeof import('./HelpTipBubble/HelpTipBubble.vue')['default']
ImgPreview: typeof import('./ImgPreview/ImgPreview.vue')['default']
LFile: typeof import('./LFile/LFile.vue')['default']
PageLayout: typeof import('./PageLayout/PageLayout.vue')['default']
Popup: typeof import('./Popup/Popup.vue')['default']
PopupDict: typeof import('./PopupDict/PopupDict.vue')['default']
ProgressMap: typeof import('./ProgressMap/ProgressMap.vue')['default']
RightConditionFilter: typeof import('./RightConditionFilter/RightConditionFilter.vue')['default']
SelectDept: typeof import('./SelectDept/SelectDept.vue')['default']
SelectUser: typeof import('./SelectUser/SelectUser.vue')['default']
SelectUserByDepart: typeof import('./SelectUserByDepart/SelectUserByDepart.vue')['default']
TreeSelect: typeof import('./TreeSelect/TreeSelect.vue')['default']
}
}
export {}

View File

@@ -0,0 +1,169 @@
// 定义 FormProperty 函数
const FormProperty = (propertyId, formSchema, required = []) => {
// 初始化私有属性
const _propertyId = propertyId;
const _formSchem = formSchema;
const _required = required;
// 定义 formSchema 的 getter 方法
const getFormSchema = () => {
return _formSchem || {};
};
// 定义 key 的 getter 方法
const getKey = () => {
return _propertyId;
};
// 定义 type 的 getter 方法
const getType = () => {
return getFormSchema().view;
};
// 定义 disabled 的 getter 方法
const getDisabled = () => {
if (_formSchem && _formSchem.ui && _formSchem.ui.widgetattrs && _formSchem.ui.widgetattrs.disabled === true) {
return true;
}
return false;
};
// 定义 label 的 getter 方法
const getLabel = () => {
const schema = getFormSchema();
return schema.title || getKey();
};
// 定义 placeholder 的 getter 方法
const getPlaceholder = () => {
const viewType = getType();
const label = getLabel();
if (viewType.indexOf('date') >= 0 || viewType.indexOf('select') >= 0 || viewType.indexOf('list') >= 0) {
return "请选择" + label;
} else if (viewType.indexOf('upload') >= 0 || viewType.indexOf('file') >= 0 || viewType.indexOf('image') >= 0) {
return "请上传" + label;
} else {
return "请输入" + label;
}
};
// 定义 dictStr 的 getter 方法
const getDictStr = () => {
const viewType = getType();
if (viewType === 'sel_search') {
const schema = getFormSchema();
return schema.dictTable + ',' + schema.dictText + ',' + schema.dictCode;
}
return '';
};
// 定义 listSource 的 getter 方法
const getListSource = () => {
const schema = getFormSchema();
if (!schema.enum) {
return [];
}
const arr = [...schema.enum];
for (let a = 0; a < arr.length; a++) {
if (!arr[a].label) {
arr[a].label = arr[a].text;
}
if (schema.type === 'number') {
arr[a].value = parseInt(arr[a].value);
}
}
return arr;
};
// 定义 popupCode 的 getter 方法
const getPopupCode = () => {
return getFormSchema().code;
};
// 定义 dest 的 getter 方法
const getDest = () => {
return getFormSchema().destFields;
};
// 定义 ogn 的 getter 方法
const getOgn = () => {
return getFormSchema().orgFields;
};
// 定义 rules 的 getter 方法
const getRules = () => {
const rules = [];
const isRequired = _required?.includes(getKey()) ?? false;
if (isRequired) {
let msg = getLabel() + '为必填项';
rules.push({ required: true, message: msg });
}
let viewType = getType();
if ('list' === viewType || 'markdown' === viewType || 'pca' === viewType) {
return rules;
}
if (viewType.indexOf('upload') >= 0 || viewType.indexOf('file') >= 0 || viewType.indexOf('image') >= 0) {
return rules;
}
const schema = getFormSchema();
if (schema.pattern) {
if (schema.pattern === 'only') {
// 这里 checkOnlyMethod 未定义,需要根据实际情况补充
rules.push({ validator: () => {} });
} else if (schema.pattern === 'z') {
if (schema.type === 'number' || schema.type === 'integer') {
// 这里 onlyInteger 未定义,需要根据实际情况处理
} else {
rules.push({ pattern: '^-?[1-9]\\d*$', message: '请输入整数' });
}
} else {
let msg = getLabel() + '校验未通过';
rules.push({ pattern: schema.pattern, message: msg });
}
}
return rules;
};
// 返回包含所有 getter 方法的对象
return {
get formSchema() {
return getFormSchema();
},
get key() {
return getKey();
},
get type() {
return getType();
},
get disabled() {
return getDisabled();
},
get label() {
return getLabel();
},
get placeholder() {
return getPlaceholder();
},
get dictStr() {
return getDictStr();
},
get listSource() {
return getListSource();
},
get popupCode() {
return getPopupCode();
},
get dest() {
return getDest();
},
get ogn() {
return getOgn();
},
get rules() {
return getRules();
}
};
};
export default FormProperty;

View File

@@ -0,0 +1,157 @@
<template>
<view class="onlineTableCell">
<!--图片-->
<template v-if="column?.type === 'image'">
<template v-if="record[column.key]">
<wd-img
width="30"
height="30"
:src="getFirstImg(record[column.key])"
@click="handleClickImg"
></wd-img>
<ImgPreview
v-if="imgPreview.show"
:urls="imgPreview.urls"
@close="() => (imgPreview.show = false)"
></ImgPreview>
</template>
<template v-else>
<text>无图片</text>
</template>
</template>
<!--下载-->
<template v-else-if="column?.type === 'file'">
<template v-if="record[column.key]">
<wd-button @click="handleDownload(record[column.key])">下载</wd-button>
</template>
<template v-else>
<text>无文件</text>
</template>
</template>
<template v-else-if="['markdown', 'umeditor'].includes(column?.type)">
<rich-text :nodes="record[column.key]"></rich-text>
</template>
<template v-else-if="column?.type === 'pca'">
<text class="ellipsis-2">{{ getPcaText(record[column.key])}}</text>
</template>
<template v-else-if="['datetime', 'date'].includes(column?.type)">
<text class="ellipsis-2">{{ getFormatDate(record[column.key], column) }}</text>
</template>
<template v-else>
<text class="ellipsis-2">{{ renderVal(record, column)}}</text>
</template>
</view>
</template>
<script setup lang="ts">
import { getFormatDate, filterMultiDictText } from '@/common/uitls'
import { isString } from '@/utils/is'
import { getFileAccessHttpUrl } from '@/common/uitls'
import { getAreaTextByCode } from '@/common/areaData/Area'
defineOptions({
name: 'OnlineSubTableCell',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
columnsInfo: {
type: Object,
default: () => {},
},
column: {
type: Object,
default: () => {},
},
record: {
type: Object,
default: () => {},
},
})
console.log("根据配置动态加载表单record ",props.record );
const imgPreview = ref({
show: false,
urls: [],
})
// 下载
const handleDownload = (text) => {
uni.downloadFile({
url: text,
success: (res) => {
if (res.statusCode === 200) {
console.log('下载成功')
console.log(res);
}
},
})
}
// 省市区
const getPcaText = (code) => {
if (!code) {
return ''
}
return getAreaTextByCode(code)
}
// 列表只显示第一张图
const getFirstImg = (text) => {
if (isString(text)) {
var imgs = text.split(',')
return getFileAccessHttpUrl(imgs[0])
} else {
return ''
}
}
// 点击图时
const handleClickImg = () => {
imgPreview.value.show = true
}
// 渲染值
const renderVal = (record, column) => {
const { type , key } = column
let text = record[key]
if (['date', 'Date'].includes(type)) {
if (!text) {
return ''
}
if (text.length > 10) {
return text.substring(0, 10)
}
return text
} else if (['popup_dict'].includes(type)) {
const dict = record[key + '_dictText']
if (dict != undefined) {
return record[key + '_dictText']
}
return text
}
//字典值翻譯
if(props.columnsInfo?.dictOptions && props.columnsInfo?.dictOptions[key]){
return filterMultiDictText(props.columnsInfo.dictOptions[key], text + '')
}
return text
}
// 初始化
const init = () => {
const field = props.column.dataIndex
if (props.column?.customRender === 'imgSlot') {
const text = props.record[field]
if (isString(text)) {
imgPreview.value.urls = text.split(',').map((item) => getFileAccessHttpUrl(item))
} else {
return ''
}
}
}
init()
</script>
<style lang="scss" scoped>
:deep(.wd-button) {
--wot-button-medium-height: 30px;
--wot-button-medium-fs: 12px;
--wot-button-medium-padding: 8px;
&.is-medium.is-round {
min-width: 80px;
}
}
</style>

View File

@@ -0,0 +1,867 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="navTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<scroll-view scroll-y class="wrap">
<wd-form ref="form" :model="formData">
<wd-cell-group class="mb-14px" border>
<view
class="onlineLoader-form"
v-for="(item, index) in rootProperties"
:key="index"
:class="{ 'mt-14px': index % 2 == 0 }"
>
<!-- 图片 -->
<wd-cell
v-if="item.type == 'image'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<online-image
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
></online-image>
</wd-cell>
<!-- 文件 -->
<wd-cell
v-else-if="item.type == 'file'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<view style="text-align: left">
<!-- #ifndef APP-PLUS -->
<online-file
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<online-file-custom
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file-custom>
<!-- #endif -->
</view>
</wd-cell>
<!-- 开关 -->
<wd-cell
v-else-if="['switch', 'checkbox'].includes(item.type)"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
center
:required="fieldRequired(item)"
>
<view style="text-align: left">
<wd-switch
:label="get4Label(item.label)"
:name="item.key"
size="18px"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:active-value="switchOpt(item.formSchema?.extendOption, 0)"
:inactive-value="switchOpt(item.formSchema?.extendOption, 1)"
/>
</view>
</wd-cell>
<!-- 日期时间 -->
<DateTime
v-else-if="item.type === 'datetime'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
startTime="1949-01-01 00:00:00"
endTime="2050-01-01 00:00:00"
:type="item.type"
:name="item.key"
format="YYYY-MM-DD HH:mm:ss"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 时间 -->
<DateTime
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
format="HH:mm:ss"
:type="item.type"
:name="item.key"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 日期 -->
<online-date
v-else-if="item.type === 'date'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="getDateExtendType(item.formSchema)"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-date>
<!-- 时间 -->
<online-time
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-time>
<!-- 下拉选择 -->
<online-select
v-else-if="item.type === 'list' || item.type === 'sel_search'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="item.type"
:dict="item.options"
:dictStr="item.dictStr"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></online-select>
<!-- checkbox -->
<online-checkbox
v-else-if="item.type === 'checkbox'"
:name="item.key"
:type="item.type"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:dict="item.options"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-checkbox>
<!-- radio -->
<online-radio
v-else-if="item.type === 'radio'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:type="item.type"
:dict="item.options"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-radio>
<!-- 下拉多选 -->
<online-multi
v-else-if="item.type === 'list_multi'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:dict="item.options"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-multi>
<!-- 省市区 -->
<online-pca
v-else-if="item.type === 'pca'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
></online-pca>
<!-- 数字框 小数 -->
<wd-input
v-else-if="
item.type === 'number' && (!item.onlyInteger || item.onlyInteger == false)
"
:label-width="labelWidth"
v-model="formData[item.key]"
:label="get4Label(item.label)"
:name="item.key"
inputMode="decimal"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.validateRules"
/>
<!-- 数字框 整数 -->
<wd-input
v-else-if="item.type === 'number' && item.onlyInteger === true"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
inputMode="numeric"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.validateRules"
/>
<!-- 多行文本 -->
<wd-textarea
v-else-if="['textarea', 'markdown', 'umeditor'].includes(item.type)"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
clearable
:maxlength="300"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.validateRules"
/>
<!-- 密码输入框 -->
<wd-input
v-else-if="item.type === 'password'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.validateRules"
show-password
/>
<!-- popup字典 -->
<PopupDict
v-else-if="item.type === 'popup_dict'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="item.formSchema.popupMulti"
:dictCode="`${item.formSchema.code},${item.formSchema['destFields']},${item.formSchema['orgFields']}`"
></PopupDict>
<!-- popup -->
<Popup
v-else-if="item.type === 'popup'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="!!item.formSchema?.popupMulti"
:code="`${item.formSchema.popupCode}`"
:setFieldsValue="setFieldsValue"
:fieldConfig="getPopupFieldConfig(item)"
></Popup>
<!-- 关联记录 -->
<online-popup-link-record
v-else-if="item.type === 'link_table'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model:formSchema="item.formSchema"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
@selected="linkRecordChange"
></online-popup-link-record>
<!-- 他表字段 -->
<wd-input
v-else-if="item.type === 'link_table_field'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="true"
:label="get4Label(item.label)"
:name="item.key"
/>
<!-- 用户选择 -->
<select-user
v-else-if="item.type === 'sel_user'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-user>
<!-- 部门选择 -->
<select-dept
v-else-if="item.type === 'sel_depart'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
labelKey="departName"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-dept>
<!-- 分类字典树 -->
<CategorySelect
v-else-if="item.type === 'cat_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:pid="`${item.formSchema.pidValue}`"
></CategorySelect>
<!-- 自定义树 -->
<TreeSelect
v-else-if="item.type === 'sel_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:dict="`${item.formSchema.dict}`"
:pidField="`${item.formSchema.pidField}`"
:pidValue="`${item.formSchema.pidValue}`"
:hasChildField="`${item.formSchema.hasChildField}`"
></TreeSelect>
<!-- 普通输入框 -->
<wd-input
v-else-if="item.type !== 'hidden'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.validateRules"
clearable
/>
</view>
</wd-cell-group>
</wd-form>
</scroll-view>
</PageLayout>
</wd-popup>
<wd-toast></wd-toast>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useToast } from 'wot-design-uni'
import OnlineCheckbox from '@/components/online/view/online-checkbox.vue'
import OnlineMulti from '@/components/online/view/online-multi.vue'
import SelectUser from '@/components/SelectUser/SelectUser.vue'
import OnlinePopupLinkRecord from '@/components/online/view/online-popup-link-record.vue'
import OnlineSelect from '@/components/online/view/online-select.vue'
import OnlineDate from '@/components/online/view/online-date.vue'
import OnlineTime from '@/components/online/view/online-time.vue'
import OnlineRadio from '@/components/online/view/online-radio.vue'
import SelectDept from '@/components/SelectDept/SelectDept.vue'
import OnlineImage from '@/components/online/view/online-image.vue'
import OnlineFile from '@/components/online/view/online-file.vue'
import OnlineFileCustom from '@/components/online/view/online-file-custom.vue'
import OnlinePca from '../view/online-pca.vue'
import { loadOneFieldDefVal } from '../defaultVal'
import { isArray, isNumber, isString } from '@/utils/is'
import { formatDate } from '@/common/uitls'
import { deepClone } from 'wot-design-uni/components/common/util'
import { duplicateCheck } from '@/service/api'
import { isObject } from '@/common/is'
defineOptions({
name: 'OnlineSubformPopup',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
schema: {
type: Object,
default: () => ({}) as any,
required: true,
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
//表数据Data
const formData = ref<any>({})
//是否填值规则字段
const hasFillRuleFields = ref([])
//是否必填字段
const hasRequiredFields = ref([])
//字段属性
const rootProperties = ref<any>([])
//表描述
const tableTxt = ref('')
//表名
const tableName = ref('')
//数据Id
const dataId = ref('')
//是否已加载
const loaded = ref(false)
//是否编辑
const isUpdate = ref(false)
//临时数据
const tempData = ref({})
// 标题宽度
const labelWidth = computed(() => {
return '100px'
})
// 导航标题
const get4Label = computed(() => {
return (lable) => {
return `${lable && lable.length > 4 ? lable.substring(0, 4) : lable}`
}
})
// 导航标题
const navTitle = computed(() => {
if (!props.title || props.title.length === 0) {
return tableTxt.value
} else {
return props.title
}
})
/**
* 获取日期控件的扩展类型
* @param formSchema
* @returns {string}
*/
const getDateExtendType = (formSchema: any) => {
if (formSchema.fieldExtendJson) {
let fieldExtendJson = JSON.parse(formSchema.fieldExtendJson)
let mapField = {
month: 'year-month',
year: 'year',
quarter: 'quarter',
week: 'week',
day: 'date',
}
return fieldExtendJson?.picker && mapField[fieldExtendJson?.picker]
? mapField[fieldExtendJson?.picker]
: 'date'
}
return 'date'
}
/**
* 判断是否选中
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const isChecked = (opts: any, value: any) => {
return opts && opts.length > 0 ? value === opts[0] : false
}
/**
* 开关选项
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const switchOpt = (opts: any, index: any) => {
const options = Array.isArray(opts) && opts.length > 0 ? opts : ['Y', 'N']
return options[index] + ''
}
/**
*
* @param item
* @returns {*|boolean}
*/
const componentDisabled = (item: any) => {
if (props.disabled === true) {
return true
}
return item.disabled
}
/**
* 获取扩展字段
* @param item
* @param field
*/
const getFieldExtendJson = (item, field) => {
let json = item.formSchema.fieldExtendJson ?? '{}'
if (isString(json) && json.trim().length === 0) {
json = '{}'
}
json = JSON.parse(json)
const result = json[field]
return result
}
/**
* 判断字段是否必填
* @param item
* @returns {boolean}
*/
const fieldRequired = (item: any) => {
return item?.key && hasRequiredFields.value.includes(item.key)
}
/**
* 关联记录同步修改他表字段
* @param linkRecord
* @param key
*/
const linkRecordChange = (linkRecord, key) => {
let linkFieldArr = rootProperties.value.filter(
(item) => item.type === 'link_table_field' && item?.formSchema?.dictTable == key,
)
console.log('linkRecordChange****》》》》', linkRecordChange)
linkFieldArr.forEach((field) => {
let value = linkRecord.map((record) => record[field.formSchema.dictText]).join(',')
nextTick(() => {
formData.value[field.key] = value
})
})
}
/**
* 校验字段
* @param values
* @returns {boolean}
*/
const fieldCheck = async (values: any) => {
// 校验字段
let flag = false
console.log('一对多子表rootProperties.value', rootProperties.value)
console.log('一对多子表values', values)
for (const item of rootProperties.value) {
// 校验提示
const tip = (msg) => {
// 提示校验未通过
toast.warning(`${msg}`)
flag = true
}
// 校验必填
if (fieldRequired(item) && !values[item.key]) {
tip(`${item.label}不能为空!`)
break
}
// 校验正则
let pattern = item?.formSchema?.pattern || item?.pattern
if (pattern) {
//可能存在是一个对象的情况,把正则表达式赋值一下
if (isObject(pattern)) {
pattern = pattern?.pattern
}
if (pattern == 'only') {
const res: any = await duplicateCheck({
tableName: tableName.value,
fieldName: item.key,
fieldVal: values[item.key],
dataId: dataId.value,
})
if (!res.success) {
tip(`${item.label} ${res.message}`)
break
}
} else {
const regex = compilePattern(pattern)
if (values[item.key] && !regex.test(values[item.key])) {
let errorInfo = item?.formSchema?.errorInfo || '格式不正确!'
tip(`${item.label}${errorInfo}`)
break
}
}
}
}
return flag
}
/**
* 省市区获取最后一位
* @param value
*/
function pcaValue(value) {
if (value.includes(',')) {
const parts = value.split(',')
if (parts.length >= 3) {
// 如果包含至少两个逗号,截取第三部分并去掉最后一位
return parts[2]
}
}
// 如果不包含逗号或不符合条件,返回原值
return value
}
/**
* 处理多选字段
* @param value
*/
function handleMultiOrDateField() {
let finalData = deepClone(formData.value)
//日期字段
let dateFieldArr = rootProperties.value.filter(
(item) => item.type === 'date' || item.type === 'datetime',
)
//省市区字段
let pcaArr = rootProperties.value.filter((item) => item.type === 'pca')
finalData = Object.keys(finalData).reduce((acc, key) => {
let value = finalData[key]
//省市区获取最后一位
if (value && pcaArr.length > 0 && pcaArr.map((item) => item.key).includes(key)) {
value = isArray(value) ? value[2] : pcaValue(value)
}
//是数组的就转换成字符串
if (value && isArray(value)) {
value = value.join(',')
}
//时间戳类型的日期转具体格式字符串
if (dateFieldArr.length > 0) {
const dateField = dateFieldArr.find((obj) => obj.key === key)
if (dateField) {
value =
value && isNumber(value)
? formatDate(
value,
dateField.type === 'datetime' ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd',
)
: value
}
}
acc[key] = value
return acc
}, {})
return finalData
}
//打开弹窗前
function beforeOpen(row, edit) {
isUpdate.value = edit
tempData.value = row
formData.value = { ...row }
}
/**
* 关闭
*/
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
/**
* 提交
*/
const handleConfirm = async () => {
// 判断字段必填和正则
if (await fieldCheck(formData.value)) {
return
}
// 处理特殊字段
let finalData = await handleMultiOrDateField()
emit('change', finalData, isUpdate.value)
//关闭弹窗
handleClose()
}
/**
* 取消
*/
const handleCancel = () => {
show.value = false
loaded.value = false
handleClose()
console.log('取消了~')
}
/**
* 设置pop组件数值
* @param data
*/
const setFieldsValue = (data) => {
formData.value = { ...formData.value, ...data }
}
/**
* popup组件配置
* @param item
*/
const getPopupFieldConfig = (item) => {
const { formSchema } = item
const { destFields = '', orgFields = '' } = formSchema
const result = orgFields.split(',').map((oField, index) => {
return {
source: oField,
target: destFields.split(',')[index],
}
})
return result
}
//监听配置变化
watchEffect(() => {
props.schema && !loaded.value && loadTableInfo(props.schema)
})
/**
* 初始化表单数据
*/
function initFormData(row) {
dataId.value = row?.id
formData.value = deepClone(row)
}
/**
* 根据配置动态加载表单
* @param dataID
*/
function loadTableInfo(formSchema: any) {
console.log('===子表many加载表单数据 schema===', formSchema)
createRootProperties(formSchema)
tableTxt.value = formSchema?.describe
if (isUpdate.value) {
initFormData(tempData.value)
} else {
// 新增页面处理表单默认值
handleDefaultValue()
}
loaded.value = true
}
/**
* 创建根属性
* @param formSchema
*/
function createRootProperties(formSchema: any) {
console.log('===子表配置项 formSchema===', formSchema)
formData.value = {}
hasRequiredFields.value = []
hasFillRuleFields.value = formSchema?.hasFillRuleFields ?? []
tableName.value = formSchema.key
const properties = formSchema.columns
let rootProps = []
properties.forEach((item) => {
if (item?.key && item?.key != 'action') {
formData.value[item?.key] = ''
let patternInfo = null
if (item?.validateRules && item?.validateRules.length > 0) {
//字段是否必填
let isRequired = item?.validateRules.some((valid) => valid?.required)
isRequired && hasRequiredFields.value.push(item.key)
//正则校验其他配置信息
patternInfo = item?.validateRules.find((valid) => valid.pattern)
if (patternInfo && patternInfo?.pattern && patternInfo.pattern == '*') {
hasRequiredFields.value.push(item.key)
}
}
rootProps.push({
...item,
type: item?.view || item.type,
label: item.title,
pattern: patternInfo,
formSchema: { ...item, pattern: patternInfo?.pattern, errorInfo: patternInfo?.message },
})
}
})
rootProperties.value = [...rootProps]
console.log('--子表 rootProperties--', rootProps)
}
/**
* 获取默认值
*/
function handleDefaultValue() {
rootProperties.value.forEach((item) => {
let field = item.key
let { defaultValue, type, fieldDefaultValue } = item.formSchema
let defVal = defaultValue || fieldDefaultValue
loadOneFieldDefVal(defVal, type, (value) => {
formData.value[field] = value
})
})
}
/**
* 根据规则表将 value 转换为对应的正则表达式
* @param {string} value 输入的值(如 "n6-16", "s6-18", "*6-16", "url", "e" 等)
* @returns {RegExp} 返回对应的正则表达式
*/
function compilePattern(value) {
let builtIn = patternArr.some((item) => item.value == value)
if (!builtIn) {
return new RegExp(value)
}
// 数字类型n
if (value.startsWith('n')) {
const rangeMatch = value.match(/^n(\d+)-(\d+)$/)
if (rangeMatch) {
const min = rangeMatch[1]
const max = rangeMatch[2]
return new RegExp(`^\\d{${min},${max}}$`)
}
return /^\d+$/ // 默认:纯数字
}
// 字母类型s
if (value.startsWith('s')) {
const rangeMatch = value.match(/^s(\d+)-(\d+)$/)
if (rangeMatch) {
const min = rangeMatch[1]
const max = rangeMatch[2]
return new RegExp(`^[a-zA-Z]{${min},${max}}$`)
}
return /^[a-zA-Z]+$/ // 默认:纯字母
}
// 任意字符(*
if (value.startsWith('*')) {
if (value === '*') return /^.+$/ // 非空
const rangeMatch = value.match(/^\*(\d+)-(\d+)$/)
if (rangeMatch) {
const min = rangeMatch[1]
const max = rangeMatch[2]
return new RegExp(`^.{${min},${max}}$`)
}
}
// 其他预定义规则
const predefinedPatterns = {
url: /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i,
e: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
m: /^1[3-9]\d{9}$/,
p: /^[1-9]\d{5}$/,
z: /^-?\d+$/,
money: /^-?\d+(\.\d{1,2})?$/,
}
return predefinedPatterns[value] || new RegExp(`^${escapeRegExp(value)}$`)
}
// 辅助函数:转义正则特殊字符
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const patternArr = [
{ label: '6到16位数字', value: 'n6-16' },
{ label: '6到18位字母', value: 's6-18' },
{ label: '6到16位任意字符', value: '*6-16' },
{ label: '网址', value: 'url' },
{ label: '电子邮件', value: 'e' },
{ label: '手机号码', value: 'm' },
{ label: '邮政编码', value: 'p' },
{ label: '字母', value: 's' },
{ label: '数字', value: 'n' },
{ label: '整数', value: 'z' },
{ label: '非空', value: '*' },
{ label: '金额', value: 'money' },
]
defineExpose({
beforeOpen,
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,30 @@
/*
*
* 这里填写用户自定义的表达式
* 可用在Online表单的默认值表达式中使用
* 需要外部使用的变量或方法一定要 export否则无法识别
* 示例:
* export const name = '张三'; // const 是常量
* export let age = 17; // 看情况 export const 还是 let ,两者都可正常使用
* export function content(arg) { // export 方法可传参数使用时要加括号值一定要return回去可以返回Promise
* return 'content' + arg;
* }
* export const address = (arg) => content(arg) + ' | 北京市'; // export 箭头函数也可以
*
*/
/** 字段默认值官方示例:获取地址 */
export function demoFieldDefVal_getAddress(arg) {
if (!arg) {
arg = '朝阳区';
}
return `北京市 ${arg}`;
}
/** 自定义JS函数示例 */
export function sayHi(name) {
if (!name) {
name = '张三';
}
return `您好,我叫: ${name}`;
}

View File

@@ -0,0 +1,302 @@
// 移动端不支持自定义表达式设置的默认值
import {http} from "@/utils/http";
import {useUserStore} from "@/store";
import * as CustomExpression from './customExpression';
import dayjs from "dayjs";
// 获取所有用户自定义表达式的Key
const ceKeys = Object.keys(CustomExpression)
// 将key用逗号拼接可以拼接成方法参数a,b,c --> function(a,b,c){}
const ceJoin = ceKeys.join(',')
// 将用户自定义的表达式按key的顺序放到数组中可以使用 apply 传递给方法直接调用
const $CE$ = ceKeys.map(key => CustomExpression[key])
/** 普通规则表达式 #{...} */
const normalRegExp = /#{([^}]+)?}/g
/** 用户自定义规则表达式 {{...}} */
const customRegExp = /{{([^}]+)?}}/g
/** 填值规则表达式 ${...} */
const fillRuleRegExp = /\${([^}]+)?}/g
/** action 类型 */
export const ACTION_TYPES = { ADD: 'add', EDIT: 'edit', DETAIL: 'detail', RELOAD: 'reload' }
/**
* 获取单个字段的默认值-通过回调函数返回值
* @param {Object} defVal
* @param {Object} type
* @param {Object} callback
*/
export async function loadOneFieldDefVal(defVal, type, callback) {
if(hasEffectiveValue(defVal)){
let value = await handleDefaultValue(defVal, ACTION_TYPES.ADD, {});
if ('number' === type && value) {
value = Number.parseFloat(value)
}
callback(value)
}
}
/**
* 判断给定的值是不是有效的
*/
function hasEffectiveValue(val) {
if(val || val === 0){
return true;
}
return false;
}
/**
* 获取默认值
* @param {Object} defVal
* @param {Object} action
* @param {Object} getFormData
*/
async function handleDefaultValue(defVal, action, getFormData) {
if (defVal != null) {
// 检查类型,如果类型错误则不继续运行
if (checkExpressionType(defVal)) {
let value = await getDefaultValue(defVal, action, getFormData)
if (value != null) {
return value
}
}
}
return defVal;
}
/**
* 加载form组件默认值
* @param form Form对象
* @param properties 字段配置
* @param action 操作类型ACTION_TYPES除填值规则外其他表达式只在add下才执行
* @param getFormData 获取数据的方法,用于填值规则向后台传值
*/
export function loadFieldDefVal({ form, properties, action, getFormData }) {
if (Array.isArray(properties) && properties.length > 0) {
properties.forEach(async prop => {
let { defVal, type } = prop._formSchem
// key取值错误导致 树形表 表单默认值未生效 【online】树列表不支持控件默认值表达式配置 (博威)
let key = prop.key
// 2021年5月21日 Tree类型表单系统编码不生效。【issues/I3NR39】
if (!key) {
key = prop._propertyId
}
eachHandler(key, defVal, action, (value) => {
// 处理数字类型如果type=number并且value有值
if ('number' === type && value) {
// parseFloat() 可以直接处理字符串、整数、小数、null和undefined
// 非数字类型直接返回NaN不必担心报错
value = Number.parseFloat(value)
}
form.setFieldsValue({ [key]: value })
}, getFormData)
})
}
}
/** 加载JEditableTable组件默认值 */
export function loadFieldDefValForSubTable({ subForms, subTable, row, action, getFormData }) {
if (subTable && Array.isArray(subTable.columns) && subTable.columns.length > 0) {
subTable.columns.forEach(async column => {
let { key, fieldDefaultValue: defVal } = column
eachHandler(key, defVal, action, (value) => {
if (subForms.form) {
subForms.form.setFieldsValue({ [key]: value })
} else {
// update-begin---author:sunjianlei Date:20200725 foronline功能测试行操作切换成新的行编辑-----------
let v = [{rowKey: row.id, values: {[key]: value}}];
(subForms.jvt || subForms.jet).setValues(v)
// update-end---author:sunjianlei Date:20200725 foronline功能测试行操作切换成新的行编辑------------
}
}, getFormData)
})
}
}
async function eachHandler(key, defVal, action, callback, getFormData) {
if (defVal != null) {
// 检查类型,如果类型错误则不继续运行
if (checkExpressionType(defVal)) {
let value = await getDefaultValue(defVal, action, getFormData)
if (value != null) {
callback(value)
}
} else {
// 不合法的表达式直接返回不解析
callback(defVal)
}
}
}
/**
* 检查表达式类型是否合法,规则:
* 1、填值规则表达式不能和其他表达式混用
* 2、每次只能填写一个填值规则表达式
* 3、普通表达式和用户自定义表达式可以混用
*/
export function checkExpressionType(defVal) {
// 获取各个表达式的数量
let normalCount = 0, customCount = 0, fillRuleCount = 0
defVal.replace(fillRuleRegExp, () => fillRuleCount++)
if (fillRuleCount > 1) {
logWarn(`表达式[${defVal}]不合法:只能同时填写一个填值规则表达式!`)
return false
}
defVal.replace(normalRegExp, () => normalCount++)
defVal.replace(customRegExp, () => customCount++)
// 除填值规则外其他规则的数量
let fillRuleOtherCount = normalCount + customCount
if (fillRuleCount > 0 && fillRuleOtherCount > 0) {
logWarn(`表达式[${defVal}]不合法:填值规则表达式不能和其他表达式混用!`)
return false
}
return true
}
/** 获取所有匹配的表达式 */
function getRegExpMap(text, exp) {
let map = new Map()
if(text && text.length>0){
text.replace(exp, function (match, param, offset, string) {
map.set(match, param.trim())
return match
})
}
return map
}
/** 获取默认值,可以执行表达式,可以执行用户自定义方法,可以异步获取用户信息等 */
async function getDefaultValue(defVal, action, getFormData) {
// 只有在 add 和 reload 模式下才执行填值规则
if (action === ACTION_TYPES.ADD || action === ACTION_TYPES.RELOAD) {
// 判断是否是填值规则表达式,如果是就执行填值规则
if (fillRuleRegExp.test(defVal)) {
return await executeRegExp(defVal, fillRuleRegExp, executeFillRuleExpression, [getFormData])
}
}
// 只有在 add 模式下才执行其他表达式
if (action === ACTION_TYPES.ADD) {
// 获取并替换所有常规表达式
defVal = await executeRegExp(defVal, normalRegExp, executeNormalExpression)
// 获取并替换所有用户自定义表达式
defVal = await executeRegExp(defVal, customRegExp, executeCustomExpression)
return defVal
}
return null
}
async function executeRegExp(defVal, regExp, execFun, otherParams = []) {
let map = getRegExpMap(defVal, regExp)
// @ts-ignore
for (let origin of map.keys()) {
let exp = map.get(origin)
let result = await execFun.apply(null, [exp, origin, ...otherParams])
// 如果只有一个表达式那么就不替换因为一旦替换类型就会被转成String直接返回执行结果保证返回的类型不变
if (origin === defVal) {
return result
}
defVal = replaceAll(defVal, origin, result)
}
return defVal
}
/** 执行【普通表达式】#{xxx} */
async function executeNormalExpression(expression, origin) {
switch (expression) {
case 'date':
return dayjs().format('YYYY-MM-DD');
case 'time':
return dayjs().format('HH:mm:ss');
case 'datetime':
return dayjs().format('YYYY-MM-DD HH:mm:ss');
default:
// 获取当前登录用户的信息
let result = getUserInfoByExpression(expression)
if (result != null) {
return result
}
// 没有符合条件的表达式,返回原始值
return origin
}
}
/** 根据表达式获取相应的用户信息 */
function getUserInfoByExpression(expression) {
let userInfo:any = useUserStore().userInfo;
if (userInfo) {
switch (expression) {
case 'sysUserId':
return userInfo.id
// 当前登录用户登录账号
case 'sysUserCode':
return userInfo.username
// 当前登录用户真实名称
case 'sysUserName':
return userInfo.realname
// 当前登录用户部门编号
case 'sysOrgCode':
return userInfo.orgCode
}
}
return null
}
/**
* 2023-09-04
* liaozhiyang
* 用new Function替换eval
*/
function _eval(str: string) {
return new Function(`return ${str}`)();
}
/** 执行【用户自定义表达式】 {{xxx}} 移动端不支持 */
async function executeCustomExpression(expression, origin) {
// 利用 eval 生成一个方法,这个方法的参数就是用户自定义的所有的表达式
let fn = _eval(`(function (${ceJoin}){ return ${expression} })`);
try {
// 然后调用这个方法,并把表达式传递进去,从而完成表达式的执行
return fn.apply(null, $CE$)
} catch (e) {
// 执行失败,输出错误并返回原始值
logWarn(e)
return origin
}
}
/** 执行【填值规则表达式】 ${xxx} */
async function executeFillRuleExpression(expression, origin, getFormData) {
let url = `/sys/fillRule/executeRuleByCode/${expression}`
let formData = {}
if (typeof getFormData === 'function') {
formData = getFormData()
}
let res:any = await http.put(url, formData)
let { success, message, result } = res;
console.log(success, message, result)
if (success) {
return result
} else {
logError(`填值规则(${expression})执行失败:${message}`)
return origin
}
}
function logWarn(message) {
console.warn('[loadFieldDefVal]:', message)
}
function logError(message) {
console.error('[loadFieldDefVal]:', message)
}
function replaceAll(text, checker, replacer) {
let lastText = text
text = text.replace(checker, replacer)
if (lastText !== text) {
return replaceAll(text, checker, replacer)
}
return text
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,245 @@
<template>
<view class="online-sub-many-container">
<wd-button :disabled="componentDisabled" customClass="add" icon="add" size="small" @click="handleAdd">新增</wd-button>
<wd-table :data="dataList" height="400px">
<template v-for="(col,index) in columns" :key="index">
<wd-table-col :prop="col.key" :label="col.title" align="center" :fixed="col.key == 'action'">
<template #value="value" v-if="col.key == 'action'">
<view class="custom-action">
<wd-button size="small" type="icon" icon="edit" @click="handleEdit(value)"></wd-button>
<wd-button size="small" type="icon" icon="delete" style="color:rgba(255,0,0,0.5)" @click="handleDel(value)"></wd-button>
</view>
</template>
<template #value="{ row, index }" :key="index" v-else>
<online-sub-table-cell
:columnsInfo="columnsInfo"
:record="row"
:column="col"
></online-sub-table-cell>
</template>
</wd-table-col>
</template>
</wd-table>
</view>
<online-subform-popup
v-if="popupShow"
ref="subFormPop"
:title="tableTxt"
:schema="formSchema"
:disabled="disabled"
@close="handleClose"
@change="handleChange"
></online-subform-popup>
</template>
<script lang="ts" setup>
import {useMessage, useToast} from 'wot-design-uni';
import OnlineSubformPopup from './components/online-subform-popup.vue'
import OnlineSubTableCell from './components/online-sub-table-cell.vue'
import { http } from '@/utils/http'
import {deepClone} from "wot-design-uni/components/common/util";
defineOptions({
name: 'online-sub-many',
options: {
styleIsolation: 'shared',
},
})
// 接收 props
const props = defineProps({
tableInfo: {
type: Object,
required: true,
default: () => ({} as any)
},
dataInfo: {
type: Array,
required: false,
default: () => ([])
},
disabled: {
type: Boolean,
default: false,
required: false
},
edit: {
type: Boolean,
default: false,
required: false
},
showFooter: {
type: Boolean,
required: false,
default: true,
}
})
// 定义 emits
const emits = defineEmits(['back', 'success'])
//提示
const toast = useToast()
//pop弹窗
const subFormPop = ref(null)
//表ID
const tableId = ref('')
//表描述
const tableTxt = ref('')
//表名称
const tableName = ref('')
//列信息
const columns = ref([])
//表单配置信息
const formSchema = ref([])
//弹窗显示
const popupShow = ref(false)
//数据列表
const dataList = ref([])
//数据列表
const editIndex = ref(null)
//列信息
const columnsInfo = ref({})
/**
*
* @param item
* @returns {*|boolean}
*/
const componentDisabled = computed(()=>{
console.log("一对多many组件",props.disabled)
console.log("一对多many组件",props.showFooter)
if (props.disabled === true || !props.showFooter) {
return true
}
return false
})
//监听配置修改
watchEffect(()=>{
props.tableInfo && loadTableInfo(props.tableInfo)
})
/**
* 根据配置动态加载表单
* @param dataID
*/
function loadTableInfo(tableInfo: any){
console.log('===子表many加载表单数据 schema===', tableInfo)
formSchema.value = deepClone(tableInfo)
tableId.value = tableInfo.id;
tableName.value = tableInfo.key;
tableTxt.value = tableInfo.describe;
columns.value = tableInfo?.columns || [];
http
.get(`/online/cgform/api/getColumns/${tableId.value}`)
.then((res: any) => {
if (res.success) {
if (res.result?.columns?.length) {
columnsInfo.value = res.result;
}
}
})
let hasAction = columns.value.some(item=>item.key == 'action');
!hasAction && columns.value.unshift({title:'操作', key:'action'});
console.log("根据配置动态加载表单columns.value ",columns.value );
if (props.edit === true) {
initFormData(props.dataInfo)
}
}
/**
* 删除
* @param row
* @param index
*/
function handleDel({index}){
if(componentDisabled.value){
return;
}
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
dataList.value.splice(index,1);
toast.success('删除成功')
}
},
fail: (err) => {
console.log(err)
},
})
}
/**
* 新增
*/
function handleAdd(){
popupShow.value = true;
nextTick(()=>{
subFormPop.value.beforeOpen(null,false)
})
}
/**
* 编辑
* @param row
* @param index
*/
function handleEdit({row,index}){
if(!componentDisabled.value){
editIndex.value = index;
popupShow.value = true;
nextTick(()=>{
subFormPop.value.beforeOpen(row,true)
})
}
}
/**
* 关闭弹窗
* @param row
* @param index
*/
function handleClose(){
popupShow.value = false;
}
/**
* 表单内容修改
* @param row
* @param index
*/
function handleChange(record,isUpdate){
if(isUpdate){
dataList.value[unref(editIndex)] = record;
}else{
dataList.value.push(record);
}
}
/**
* 提交前处理
*/
function beforeSubmit(){
console.log("一对多beforeSubmit",dataList.value);
return {status:true,data:dataList.value}
}
/**
* 初始化表单数据
*/
function initFormData(data) {
if (data && data.length>0)
dataList.value = deepClone(data)
}
defineExpose({
beforeSubmit,
initFormData
})
</script>
<style lang="scss" scoped>
.online-sub-many-container {
min-height: 300px;
.custom-action{
display: flex;
}
:deep(.add) {
margin: 10px;
}
}
</style>

View File

@@ -0,0 +1,808 @@
<template>
<view class="online-sub-one-container">
<view class="form-container">
<wd-form ref="form" :model="formData">
<wd-cell-group border>
<view
class="onlineLoader-form"
v-for="(item, index) in rootProperties"
:key="index"
:class="{ 'mt-14px': index % 2 == 0, 'mt-2px': index == 0 }"
>
<!-- 图片 -->
<wd-cell
v-if="item.type == 'image'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<online-image
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
></online-image>
</wd-cell>
<!-- 文件 -->
<wd-cell
v-else-if="item.type == 'file'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
:required="fieldRequired(item)"
>
<view style="text-align: left">
<!-- #ifndef APP-PLUS -->
<online-file
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<online-file-custom
v-model:value="formData[item.key]"
:name="item.key"
:disabled="componentDisabled(item)"
:key="index"
:maxNum="getFieldExtendJson(item, 'uploadnum')"
></online-file-custom>
<!-- #endif -->
</view>
</wd-cell>
<!-- 日期时间 -->
<DateTime
v-else-if="item.type === 'datetime'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
startTime="1949-01-01 00:00:00"
endTime="2050-01-01 00:00:00"
:type="item.type"
:name="item.key"
format="YYYY-MM-DD HH:mm:ss"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 时间 -->
<DateTime
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
format="HH:mm:ss"
:type="item.type"
:name="item.key"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></DateTime>
<!-- 日期 -->
<online-date
v-else-if="item.type === 'date'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="getDateExtendType(item.formSchema)"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-date>
<!-- 时间 -->
<online-time
v-else-if="item.type === 'time'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:disabled="componentDisabled(item)"
v-model:value="formData[item.key]"
:required="fieldRequired(item)"
></online-time>
<!-- 下拉选择 -->
<online-select
v-else-if="item.type === 'list' || item.type === 'sel_search'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:type="item.type"
:dict="item.listSource"
:dictStr="item.dictStr"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:required="fieldRequired(item)"
></online-select>
<!-- checkbox -->
<online-checkbox
v-else-if="item.type === 'checkbox'"
:name="item.key"
:type="item.type"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:dict="item.listSource"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-checkbox>
<!-- radio -->
<online-radio
v-else-if="item.type === 'radio'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:type="item.type"
:dict="item.listSource"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-radio>
<!-- 下拉多选 -->
<online-multi
v-else-if="item.type === 'list_multi'"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:name="item.key"
:dict="item.listSource"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></online-multi>
<!-- 省市区 -->
<online-pca
v-else-if="item.type === 'pca'"
:name="item.key"
:label="get4Label(item.label)"
:labelWidth="labelWidth"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
></online-pca>
<!-- 数字框 小数 -->
<wd-input
v-else-if="item.type === 'number' && (!item.onlyInteger || item.onlyInteger == false)"
:label-width="labelWidth"
v-model="formData[item.key]"
:label="get4Label(item.label)"
:name="item.key"
inputMode="decimal"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.rules"
/>
<!-- 数字框 整数 -->
<wd-input
v-else-if="item.type === 'number' && item.onlyInteger === true"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
inputMode="numeric"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.rules"
/>
<!-- 开关 -->
<wd-cell
v-else-if="item.type == 'switch'"
:name="item.key"
:title="get4Label(item.label)"
:title-width="labelWidth"
center
:required="fieldRequired(item)"
>
<view style="text-align: left">
<wd-switch
:label="get4Label(item.label)"
:name="item.key"
size="18px"
:disabled="componentDisabled(item)"
v-model="formData[item.key]"
:active-value="switchOpt(item.formSchema?.extendOption, 0)"
:inactive-value="switchOpt(item.formSchema?.extendOption, 1)"
/>
</view>
</wd-cell>
<!-- 多行文本 -->
<wd-textarea
v-else-if="['textarea', 'markdown', 'umeditor'].includes(item.type)"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model="formData[item.key]"
clearable
:maxlength="300"
:disabled="componentDisabled(item)"
:placeholder="item.placeholder"
:rules="item.rules"
/>
<!-- 密码输入框 -->
<wd-input
v-else-if="item.type === 'password'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.rules"
show-password
/>
<!-- popup字典 -->
<PopupDict
v-else-if="item.type === 'popup_dict'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="item.formSchema.popupMulti"
:dictCode="`${item.formSchema.code},${item.formSchema['destFields']},${item.formSchema['orgFields']}`"
></PopupDict>
<!-- popup -->
<Popup
v-else-if="item.type === 'popup'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:multi="item.formSchema.popupMulti"
:code="`${item.formSchema.code}`"
:setFieldsValue="setFieldsValue"
:fieldConfig="getPopupFieldConfig(item)"
></Popup>
<!-- 关联记录 -->
<online-popup-link-record
v-else-if="item.type === 'link_table'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:name="item.key"
v-model:formSchema="item.formSchema"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model:value="formData[item.key]"
@selected="linkRecordChange"
></online-popup-link-record>
<!-- 他表字段 -->
<wd-input
v-else-if="item.type === 'link_table_field'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="true"
:label="get4Label(item.label)"
:name="item.key"
/>
<!-- 用户选择 -->
<select-user
v-else-if="item.type === 'sel_user'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-user>
<!-- 部门选择 -->
<select-dept
v-else-if="item.type === 'sel_depart'"
:label-width="labelWidth"
:name="item.key"
:label="get4Label(item.label)"
labelKey="departName"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
></select-dept>
<!-- 分类字典树 -->
<CategorySelect
v-else-if="item.type === 'cat_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:pid="`${item.formSchema.pidValue}`"
></CategorySelect>
<!-- 自定义树 -->
<TreeSelect
v-else-if="item.type === 'sel_tree'"
:label-width="labelWidth"
:label="get4Label(item.label)"
:disabled="componentDisabled(item)"
:required="fieldRequired(item)"
v-model="formData[item.key]"
:dict="`${item.formSchema.dict}`"
:pidField="`${item.formSchema.pidField}`"
:pidValue="`${item.formSchema.pidValue}`"
:hasChildField="`${item.formSchema.hasChildField}`"
></TreeSelect>
<!-- 普通输入框 -->
<wd-input
v-else-if="item.type !== 'hidden'"
:label-width="labelWidth"
v-model="formData[item.key]"
:disabled="componentDisabled(item)"
:label="get4Label(item.label)"
:name="item.key"
:placeholder="item.placeholder"
:rules="item.rules"
clearable
/>
</view>
</wd-cell-group>
</wd-form>
</view>
</view>
<wd-toast></wd-toast>
</template>
<script lang="ts" setup>
import FormProperty from './FormProperty'
import OnlineImage from '@/components/online/view/online-image.vue'
import OnlineFile from '@/components/online/view/online-file.vue'
import OnlineFileCustom from '@/components/online/view/online-file-custom.vue'
import OnlineSelect from '@/components/online/view/online-select.vue'
import OnlineTime from '@/components/online/view/online-time.vue'
import OnlineDate from '@/components/online/view/online-date.vue'
import OnlineRadio from '@/components/online/view/online-radio.vue'
import OnlineCheckbox from '@/components/online/view/online-checkbox.vue'
import OnlineMulti from '@/components/online/view/online-multi.vue'
import OnlinePca from './view/online-pca.vue'
import OnlinePopupLinkRecord from '@/components/online/view/online-popup-link-record.vue'
import SelectDept from '@/components/SelectDept/SelectDept.vue'
import SelectUser from '@/components/SelectUser/SelectUser.vue'
import { loadOneFieldDefVal } from './defaultVal'
import { useToast, useMessage, useNotify } from 'wot-design-uni'
import { deepClone } from 'wot-design-uni/components/common/util'
import { isArray, isNumber, isString } from '@/utils/is'
import { formatDate } from '@/common/uitls'
import { duplicateCheck } from '@/service/api'
defineOptions({
name: 'online-sub-one',
options: {
// apply-shared当前页面样式会影响到子组件样式.(小程序)
// shared当前页面样式影响到子组件子组件样式也会影响到当前页面.(小程序)
styleIsolation: 'shared',
},
})
// 接收 props
const props = defineProps({
tableInfo: {
type: Object,
required: true,
default: () => ({}) as any,
},
dataInfo: {
type: Array,
required: false,
default: () => [],
},
edit: {
type: Boolean,
default: false,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
showFooter: {
type: Boolean,
required: false,
default: true,
},
})
// 定义 emits
const emits = defineEmits(['back', 'success'])
//定义提示
const toast = useToast()
//表描述
const tableTxt = ref('')
//表名称
const tableName = ref('')
//表数据Data
const formData = ref<any>({})
//是否填值规则字段
const hasFillRuleFields = ref('')
//是否必填字段
const hasRequiredFields = ref([])
//字段属性
const rootProperties = ref<any>([])
//表单数据ID
const formDataId = ref('')
//表单数据ID
const loaded = ref(false)
// 标题宽度
const labelWidth = computed(() => {
return '100px'
})
// 导航标题
const get4Label = computed(() => {
return (lable) => {
return `${lable && lable.length > 4 ? lable.substring(0, 4) : lable}`
}
})
/**
* 获取日期控件的扩展类型
* @param formSchema
* @returns {string}
*/
const getDateExtendType = (formSchema: any) => {
if (formSchema?.fieldExtendJson) {
let fieldExtendJson = JSON.parse(formSchema.fieldExtendJson)
let mapField = {
month: 'year-month',
year: 'year',
quarter: 'quarter',
week: 'week',
day: 'date',
}
return fieldExtendJson?.picker && mapField[fieldExtendJson?.picker]
? mapField[fieldExtendJson?.picker]
: 'date'
}
return 'date'
}
/**
* 判断是否选中
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const isChecked = (opts: any, value: any) => {
return opts && opts.length > 0 ? value === opts[0] : false
}
/**
* 开关选项
* @param opts
* @param value
* @returns {boolean|boolean}
*/
const switchOpt = (opts: any, index: any) => {
const options = Array.isArray(opts) && opts.length > 0 ? opts : ['Y', 'N']
return options[index] + ''
}
/**
*
* @param item
* @returns {*|boolean}
*/
const componentDisabled = (item: any) => {
if (props.disabled === true || !props.showFooter) {
return true
}
return item.disabled
}
/**
* 判断字段是否必填
* @param item
* @returns {boolean}
*/
const fieldRequired = (item: any) => {
return item?.key && hasRequiredFields.value.includes(item.key)
}
/**
* 获取扩展字段
* @param item
* @param field
*/
const getFieldExtendJson = (item, field) => {
let json = item.formSchema.fieldExtendJson ?? '{}'
if (isString(json) && json.trim().length === 0) {
json = '{}'
}
json = JSON.parse(json)
const result = json[field]
return result
}
/**
* 关联记录同步修改他表字段
* @param linkRecord
* @param key
*/
const linkRecordChange = (linkRecord, key) => {
let linkFieldArr = rootProperties.value.filter(
(item) => item.type === 'link_table_field' && item?.formSchema?.dictTable == key,
)
linkFieldArr.forEach((field) => {
let value = linkRecord.map((record) => record[field.formSchema.dictText]).join(',')
nextTick(() => {
formData.value[field.key] = value
})
})
}
//监听配置修改
watchEffect(() => {
props.tableInfo && !loaded.value && loadTableInfo(props.tableInfo)
})
/**
* 省市区获取最后一位
* @param value
*/
function pcaValue(value) {
if (value.includes(',')) {
const parts = value.split(',')
if (parts.length >= 3) {
// 如果包含至少两个逗号,截取第三部分并去掉最后一位
return parts[2]
}
}
// 如果不包含逗号或不符合条件,返回原值
return value
}
/**
* 处理多选字段
* @param value
*/
function handleMultiOrDateField() {
let finalData = deepClone(formData.value)
//日期字段
let dateFieldArr = rootProperties.value.filter(
(item) => item.type === 'date' || item.type === 'datetime',
)
//省市区字段
let pcaArr = rootProperties.value.filter((item) => item.type === 'pca')
finalData = Object.keys(finalData).reduce((acc, key) => {
let value = finalData[key]
//省市区获取最后一位
if (value && pcaArr.length > 0 && pcaArr.map((item) => item.key).includes(key)) {
value = isArray(value) ? value[2] : pcaValue(value)
}
//是数组的就转换成字符串
if (value && isArray(value)) {
value = value.join(',')
}
//时间戳类型的日期转具体格式字符串
if (dateFieldArr.length > 0) {
const dateField = dateFieldArr.find((obj) => obj.key === key)
if (dateField) {
value =
value && isNumber(value)
? formatDate(
value,
dateField.type === 'datetime' ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd',
)
: value
}
}
acc[key] = value
return acc
}, {})
return finalData
}
/**
* 表单提交事件
* @param e
*/
const beforeSubmit = async () => {
// 判断字段必填和正则
if (await fieldCheck(formData.value)) {
return { status: false, data: [] }
}
// 处理多选字段
let finalData = await handleMultiOrDateField()
console.log('一对一beforeSubmit', finalData)
return { status: true, data: [finalData] }
}
/**
* 校验字段
* @param values
* @returns {boolean}
*/
const fieldCheck = async (values: any) => {
// 校验字段
let flag = false
for (const item of rootProperties.value) {
// 校验提示
const tip = (msg) => {
// 提示校验未通过
toast.warning(`${tableTxt.value}:${msg}`)
flag = true
}
// 校验必填
if (fieldRequired(item) && !values[item.key]) {
tip(`${item.label}不能为空!`)
break
}
// 校验正则
let pattern = item?.formSchema?.pattern
if (pattern) {
if (pattern == 'only') {
const res: any = await duplicateCheck({
tableName: tableName.value,
fieldName: item.key,
fieldVal: values[item.key],
dataId: formDataId.value,
})
if (!res.success) {
tip(`${item.label} ${res.message}`)
break
}
} else {
const regex = new RegExp(pattern)
if (values[item.key] && pattern && !regex.test(values[item.key])) {
let errorInfo = item?.formSchema?.errorInfo || '格式不正确!'
tip(`${item.label}${errorInfo}`)
break
}
}
}
}
return flag
}
/**
* 初始化表单数据
*/
function initFormData(data) {
console.log('一对一子表initFormData', data)
if (data && data.length > 0) {
formData.value = deepClone(data[0])
formDataId.value = formData.value?.id
}
}
/**
* 创建根属性
* @param formSchema
*/
function createRootProperties(formSchema: any) {
formData.value = {}
hasFillRuleFields.value = formSchema?.hasFillRuleFields ?? []
hasRequiredFields.value = formSchema?.required ?? []
const properties = formSchema.properties
let rootProps = []
console.log('===子表one配置项 properties===', properties)
Object.keys(properties).map((key) => {
if (key) {
const item = properties[key]
formData.value[key] = ''
let fp = FormProperty(key, item, formSchema.required)
rootProps.push(fp)
}
})
rootProps.sort((one, next) => {
return one.formSchema.order - next.formSchema.order
})
rootProperties.value = [...rootProps]
console.log('--子表one rootProperties--', rootProps)
}
/**
* 获取字段类型
* @param item
* @returns {string}
*/
const getFieldNumberType = (item: any) => {
return item.onlyInteger === true ? 'digit' : 'number'
}
/**
* 获取默认值
*/
function handleDefaultValue() {
rootProperties.value.forEach((item) => {
let field = item.key
let { defVal, type } = item.formSchema
loadOneFieldDefVal(defVal, type, (value) => {
formData.value[field] = value
})
})
}
/**
* 设置字段数值
* @param data
*/
const setFieldsValue = (data) => {
console.log('一对一子表popup设置值', data)
formData.value = { ...formData.value, ...data }
}
/**
* 获取popup字段配置项
* @param item
*/
const getPopupFieldConfig = (item) => {
const { formSchema } = item
const { destFields = '', orgFields = '' } = formSchema
const result = orgFields.split(',').map((oField, index) => {
return {
source: oField,
target: destFields.split(',')[index],
}
})
return result
}
/**
* 根据配置动态加载表单
* @param dataID
*/
function loadTableInfo(formSchema: any) {
console.log('===子表one加载表单数据 schema===', formSchema)
createRootProperties(formSchema)
tableTxt.value = formSchema?.describe
if (props.edit === true) {
initFormData(props.dataInfo)
} else {
// 新增页面处理表单默认值
handleDefaultValue()
}
loaded.value = true
}
defineExpose({
beforeSubmit,
initFormData,
loadTableInfo,
})
</script>
<style lang="scss" scoped>
.online-sub-one-container {
.form-container {
:deep(.wd-cell-group__body) {
background-color: #f1f1f1;
}
.onlineLoader-form {
:deep(.wd-input__label-inner) {
font-size: 16px;
}
:deep(.wd-picker__label) {
font-size: 16px;
}
:deep(.wd-select-picker__label) {
font-size: 16px;
}
:deep(.wd-cell__title) {
font-size: 16px;
}
:deep(.wd-textarea__label-inner) {
font-size: 16px;
}
:deep(.wd-input__label.is-required) {
padding-left: 0px;
}
:deep(.wd-input__label.is-required::after) {
left: -10px;
}
:deep(.wd-textarea__clear) {
color: #bfbfbf;
}
:deep(.wd-select-picker__clear) {
color: #bfbfbf;
}
:deep(.wd-input__clear) {
color: #bfbfbf;
}
:deep(.wd-upload__close) {
color: #bfbfbf;
}
}
}
.footer {
padding: 12px;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<wd-input
v-bind="$attrs"
:label-width="labelWidth"
v-model="currentText"
:disabled="disabled"
:label="label"
:inputMode="inputMode"
:placeholder="placeholder"
@input="debouncedInput"
/>
</template>
<script lang="ts" setup>
import { debounce } from 'lodash-es'
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
inputMode: {
type: String,
default: 'text',
required: false,
},
placeholder: {
type: String,
default: '',
required: false,
},
value: {
type: [String, Number],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
}
})
// 定义 emits
const emit = defineEmits(['input', 'update:value'])
// 定义响应式数据;
const currentText = ref(null)
// 监听 value 的变化
watch(
() => props.value,
(val) => {
currentText.value = val;
},
{
immediate:true
}
)
// 更新 value 的方法
const updateValue = () => {
emit('update:value', currentText.value)
}
// 创建防抖函数
const debouncedInput = debounce(updateValue, 1000)
// 记得在组件卸载时取消防抖
onBeforeUnmount(() => {
debouncedInput.cancel()
})
</script>
<style></style>

View File

@@ -0,0 +1,345 @@
<template>
<wd-popup position="bottom" v-model="show">
<PageLayout
:navTitle="navTitle"
type="popup"
navRightText="确定"
@navRight="handleConfirm"
@navBack="handleCancel"
>
<view class="wrap">
<z-paging
ref="paging"
:fixed="false"
v-model="dataList"
@query="queryList"
:default-page-size="15"
>
<!-- <template #top>-->
<!-- <wd-search-->
<!-- hide-cancel-->
<!-- :placeholder="search.placeholder"-->
<!-- v-model="search.keyword"-->
<!-- @search="handleSearch"-->
<!-- @clear="handleClear"-->
<!-- />-->
<!-- </template>-->
<template v-if="multi">
<wd-checkbox-group shape="square" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<view class="list" @click="hanldeCheck(index, item)">
<view class="left text-gray-5">
<view class="cu-avatar lg mr-4" v-if="imageField" :style="[{ backgroundImage: 'url(' + getImage(item[imageField] )+ ')' }]"></view>
<view class="field-content">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
</view>
<view class="right" @click.stop>
<wd-checkbox ref="checkboxRef" :modelValue="item.id"></wd-checkbox>
</view>
</view>
</template>
</wd-checkbox-group>
</template>
<template v-else>
<wd-radio-group shape="dot" v-model="checkedValue">
<template v-for="(item, index) in dataList" :key="index">
<wd-cell>
<view class="list" @click="hanldeCheck(index, item)">
<view class="left text-gray-5">
<view
class="cu-avatar lg mr-4"
v-if="imageField && item[imageField]"
:style="[{ backgroundImage: 'url(' + item[imageField] + ')' }]"
></view>
<view class="field-content">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view class="row">
<text class="label">{{ cItem.title }}</text>
<text class="value">{{ item[cItem.dataIndex] }}</text>
</view>
</template>
</view>
</view>
<view class="right" @click.stop>
<wd-radio :value="item.id"></wd-radio>
</view>
</view>
</wd-cell>
</template>
</wd-radio-group>
</template>
</z-paging>
</view>
</PageLayout>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import { isArray, isString } from '@/utils/is'
import { getFileAccessHttpUrl } from '@/common/uitls'
defineOptions({
name: 'popupReportModal',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
selected: {
type: String,
default: '',
},
dictTable: {
type: String,
required: true,
},
dictCode: {
type: String,
required: true,
},
dictText: {
type: String,
required: true,
},
multi: {
type: Boolean,
required: false,
},
imageField: {
type: String,
required: false,
},
})
const emit = defineEmits(['change', 'close'])
const toast = useToast()
const show = ref(true)
const api = {
getColumns: '/online/cgform/api/getColumns',
getData: '/online/cgform/api/getData',
}
console.log('props:::', props)
const navTitle = ref('')
const paging = ref(null)
const dataList = ref([])
// 报表id
let rpConfigId = null
let loadedColumns = false
const columns = ref([])
const selectArr = ref([])
const checkedValue: any = ref(props.multi ? [] : '')
const checkboxRef = ref(null)
const search = reactive({
keyword: '',
placeholder: '',
field: '',
})
const handleClose = () => {
setTimeout(() => {
emit('close')
}, 400)
}
const beforeOpen = (arr) => {
selectArr.value = arr || []
}
const handleConfirm = () => {
if (checkedValue.value.length == 0) {
toast.warning('还没选择~')
return
}
const result = []
let value = checkedValue.value
if (!Array.isArray(checkedValue.value)) {
value = [checkedValue.value]
}
value.forEach((id, index) => {
const findIndex = dataList.value.findIndex((item) => item.id === id)
result.push(dataList.value[findIndex])
})
show.value = false
emit('change', result)
handleClose()
}
const handleCancel = () => {
show.value = false
handleClose()
console.log('取消了~')
}
// 搜索
function handleSearch() {
paging.value.reload()
}
// 图片
function getImage(src) {
return src?src:'';
}
// 清除搜索条件
function handleClear() {
search.keyword = ''
handleSearch()
}
const hanldeCheck = (index, item) => {
if (props.multi) {
if (Array.isArray(checkboxRef.value)) {
checkboxRef.value[index].toggle()
}
} else {
checkedValue.value = item.id
}
}
const getRpColumns = () => {
return new Promise<void>((resolve, reject) => {
if (loadedColumns) {
resolve()
} else {
let linkTableSelectFields = props.dictCode + ',' + props.dictText
http
.get(`${api.getColumns}/${props.dictTable}?linkTableSelectFields=${linkTableSelectFields}`)
.then((res: any) => {
if (res.success) {
loadedColumns = true
const { result } = res
navTitle.value = result.description
rpConfigId = result.code
result.columns?.forEach((item) => {
if (linkTableSelectFields.includes(item.dataIndex)) {
columns.value.push(item)
}
})
resolve()
} else {
reject()
}
})
.catch((err) => {
reject()
})
}
})
}
const queryList = (pageNo, pageSize) => {
const pararms = { pageNo, pageSize, linkTableSelectFields: '' }
if (search.keyword) {
pararms[search.field] = `*${search.keyword}*`
}
getRpColumns()
.then(() => {
let linkTableSelectFields = props.dictCode + ',' + props.dictText
if (props.imageField) {
linkTableSelectFields = linkTableSelectFields + ',' + props.imageField
}
pararms.linkTableSelectFields = linkTableSelectFields
http
.get(`${api.getData}/${props.dictTable}`, pararms)
.then((res: any) => {
if (res.success && res.result.records) {
let dataRecords = res.result.records
if (dataRecords && dataRecords.length > 0) {
let id = dataRecords[0]['id']
for (let item of dataRecords) {
if (!id) {
item.id = new Date().getTime()
}
if (props.imageField && item[props.imageField]) {
let imgUrlArr = item[props.imageField].split(',')
item[props.imageField] =
imgUrlArr.length > 0 ? getFileAccessHttpUrl(imgUrlArr[0]) : ''
}
}
}
//TODO
if (selectArr.value && isArray(selectArr) && selectArr.length > 0) {
//checkedValue.value = [...selectArr]
}
paging.value.complete(dataRecords ?? [])
} else {
paging.value.complete(false)
}
})
.catch((err) => {})
})
.catch((err) => {})
}
const init = () => {
if (props.selected.length) {
if (props.multi) {
if (isArray(props.selected)) {
checkedValue.value = props.selected
} else if (isString(props.selected)) {
checkedValue.value = props.selected.split(',')
}
} else {
if (isString(props.selected)) {
checkedValue.value = props.selected
} else if (isArray(props.selected)) {
// @ts-ignore
checkedValue.value = props.selected.join(',')
}
}
}
}
init()
defineExpose({
beforeOpen,
})
</script>
<style lang="scss" scoped>
.wrap {
height: 100%;
.mr-4 {
margin-right: 10px;
}
}
:deep(.wd-cell) {
--wot-color-white: tranparent;
--wot-cell-padding: 0;
.wd-cell__wrapper {
--wot-cell-wrapper-padding: 0;
}
.wd-cell__left {
display: none;
}
}
:deep(.wd-checkbox-group) {
--wot-checkbox-bg: tranparent;
}
:deep(.wd-radio-group) {
--wot-radio-bg: tranparent;
}
.list {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
padding: 16px;
margin-top: 16px;
.left {
display: flex;
justify-content: center;
align-items: center;
.field-content {
.row {
display: flex;
}
}
}
.right {
:deep(.wd-checkbox) {
margin-bottom: 0;
}
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<wd-select-picker
:label-width="labelWidth"
:label="label"
v-model="selected"
filterable
clearable
:columns="options"
:disabled="disabled"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue';
import { isArray, isString } from 'lodash';
import {http} from "@/utils/http"; // 假设使用 lodash 来判断类型
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [Array, String],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['change', 'update:modelValue'])
// 定义响应式数据
const selected = ref([]);
const options = ref([]);
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
}
else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
let valueStr='';
if (value && isArray(value)) {
valueStr = value.join(',')
}
emit('update:modelValue', valueStr);
emit('change', valueStr);
}
// 监听 dict 和 value 的变化
watch(() => props.dict, () => {
initSelections();
});
// 监听value 的变化
watchEffect(()=>{
props.modelValue && initVal()
})
//初始化数值
function initVal(){
selected.value = isString(props.modelValue)? props.modelValue.split(',') :[];
}
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style lang="scss" scoped>
:deep(.wd-checkbox__shape){
border-radius:0 !important;
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<view class="pickerArea" :class="{ clear: !!currentTime }">
<!-- 周视图 -->
<wd-calendar
v-if="type == 'week'"
v-model="currentTime"
type="week"
:disabled="disabled"
:default-value="defaultValue"
:labelWidth="labelWidth"
:label="label"
@confirm="handleConfirm" />
<!-- 季度视图 -->
<wd-picker
v-else-if="type == 'quarter'"
v-model="currentQuarter"
:disabled="disabled"
:labelWidth="labelWidth"
:label="label"
:columns="quarterColumns"
@open="quarterOpen"
@confirm="quarterConfirm"/>
<!-- 其他视图 -->
<wd-datetime-picker
v-else
:disabled="disabled"
:minDate="-662716800000"
:maxDate="4102416000000"
:default-value="defaultValue"
:type="type"
:labelWidth="labelWidth"
v-model="currentTime"
:label="label"
@confirm="handleConfirm"
/>
<view v-if="!!currentTime && !disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
</view>
</template>
<script setup>
// 定义 props
import { isString } from '@/utils/is'
import dayjs from 'dayjs'
import {dateToQuarterStart} from "@/common/uitls";
defineOptions({
name: 'online-date',
options: {
styleIsolation: 'shared',
},
})
//props定义
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: 'date',
required: false,
},
value: {
type: [String, Number, Date],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:value'])
// 定义响应式数据;
const currentQuarter = ref('');
// 定义响应式数据;
const currentTime = ref(props.type == 'week' ? null : '');
// 默認
const defaultValue = ref(Date.now())
// 季度选择器列
const quarterColumns = ref([])
// 监听 value 的变化
watch(
() => props.value,
(val) => {
if (val) {
if (props.type == 'quarter' && quarterColumns.value.length == 0) {
generateQuarterOptions()
}
initVal(val)
}
},
{ immediate: true },
)
// 初始化值
function initVal(val){
if (props.type == 'quarter') {
currentQuarter.value = dateToQuarterStart(val);
}else{
if (typeof val == 'object') {
currentTime.value = dayjs(val).valueOf();
} else {
currentTime.value = isString(val) ? new Date(val).getTime() : val;
}
}
}
// 选择器改变事件处理函数
const handleConfirm = (e) => {
emit('update:value', currentTime.value)
emit('change', currentTime.value)
}
// 选择器改变事件处理函数
const quarterOpen = () => {
!quarterColumns.value.length && generateQuarterOptions();
if(!currentQuarter.value) {
currentQuarter.value = dateToQuarterStart(new Date());
}
}
// 选择器改变事件处理函数
const quarterConfirm = (e) => {
emit('update:value', currentQuarter.value)
emit('change', currentQuarter.value)
}
// 清空
const handleClear = () => {
currentTime.value = null
handleConfirm(null)
}
// 生成季度选择器列
function generateQuarterOptions(){
const options = [];
const currentYear = new Date().getFullYear();
const startYear = currentYear - 40;
const endYear = currentYear + 40;
const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => endYear - i).reverse();
years.forEach(year => {
options.push({label: `${year}年 Q1`, value: `${year}-01-01`})
options.push({label: `${year}年 Q2`, value: `${year}-04-01`})
options.push({label: `${year}年 Q3`, value: `${year}-07-01`})
options.push({label: `${year}年 Q4`, value: `${year}-10-01`})
})
quarterColumns.value = [...options];
}
</script>
<style lang="scss" scoped>
.pickerArea {
position: relative;
.u-icon-close {
position: absolute;
right: 15px;
top: calc(10px + 4px);
color: #585858;
font-size: 15px;
}
&.clear {
:deep(.wd-picker__arrow) {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,239 @@
<template>
<view>
<template v-if="fileList && fileList.length > 0">
<view class="cu-list menu-avatar ol-file-list">
<view
class="cu-item"
:class="modalName == 'move-box-' + index ? 'move-cur' : ''"
v-for="(item, index) in fileList"
:key="index"
@touchstart="ListTouchStart"
@touchmove="ListTouchMove"
@touchend="ListTouchEnd"
:data-target="'move-box-' + index"
>
<view class="flex align-center">
<text
class="mr-4px text-blue"
style="font-size: 18px"
:class="fileType(item.name) ? 'cuIcon-text' : 'cuIcon-pic'"
></text>
{{ item.name }}
</view>
<view class="move">
<view @tap="DelFile" class="bg-red" :data-index="index">删除</view>
</view>
</view>
</view>
</template>
<view class="cu-bar bg-white">
<wd-button @tap="ChooseFile" size="small" :disabled="disabled" plain type="primary">
<text class="cuIcon-upload"></text>
上传
</wd-button>
</view>
<LFile ref="customFile" @up-success="onSuccess"></LFile>
</view>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import LFile from '@/components/LFile/LFile.vue';
import { useUserStore } from '@/store';
import { getEnvBaseUrl } from '@/utils';
import { useToast } from 'wot-design-uni';
import { getFileAccessHttpUrl } from '@/common/uitls';
//图片类型
const imgTypeArr = ['png', 'jpg', 'jpeg', 'bmp']
const userStore = useUserStore()
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
value: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
name: {
type: String,
default: '',
required: false,
},
maxNum: {
type: Number,
default: null,
},
})
// 事件
const emits = defineEmits(['change', 'input', 'update:value'])
// 提示
const toast = useToast()
// 组件
const customFile = ref(null)
// 文件数据
const fileList = ref([])
// token
const token = ref(userStore.userInfo.token)
const modalName = ref(null)
const listTouchStart = ref(0)
const listTouchDirection = ref(null)
// ListTouch触摸开始
const ListTouchStart = (e) => {
listTouchStart.value = e.touches[0].pageX
}
// ListTouch计算方向
const ListTouchMove = (e) => {
listTouchDirection.value = e.touches[0].pageX - listTouchStart.value > 0 ? 'right' : 'left'
}
// ListTouch计算滚动
const ListTouchEnd = (e) => {
if (listTouchDirection.value == 'left') {
modalName.value = e.currentTarget.dataset.target
} else {
modalName.value = null
}
listTouchDirection.value = null
}
/**
* 选择文件
* @constructor
*/
const ChooseFile = () => {
if (props.maxNum && fileList.value.length >= props.maxNum) {
toast.warning('最多只能上传' + props.maxNum + '个文件')
return
}
customFile.value.upload({
// #ifdef APP-PLUS
currentWebview: getCurrentPages()[getCurrentPages().length - 1].$getAppWebview(),
// #endif
header: {
'X-Access-Token': token.value,
},
url: `${getEnvBaseUrl()}/sys/common/upload`,
})
}
const onSuccess = (res) => {
console.log('这是上传成功返回数值res:', res)
let fileObj = res.data
// #ifdef APP-PLUS
fileObj = JSON.parse(res.data.id)
// #endif
if (fileObj.success) {
let file = {
name: res.fileName,
path: fileObj.message,
url: getFileAccessHttpUrl(fileObj.message),
}
fileList.value.unshift(file)
changeOnlineFormValue()
}
}
// 获取文件路径
const pathArr = (arg) => {
return fileList.value.map((item) => item[arg])
}
// 改变表单值
const changeOnlineFormValue = () => {
let arr = pathArr('path')
let str = arr.join(',')
emits('change', str)
emits('input', str)
emits('update:value', str)
}
const DelFile = (e) => {
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
fileList.value.splice(e.currentTarget.dataset.index, 1)
changeOnlineFormValue()
}
},
})
}
const loadFile = () => {
if (!props.value || props.value.length == 0) {
return
}
let pathArr = props.value.split(',')
let fileArray = []
pathArr.map((path) => {
let seg = path.lastIndexOf('/')
if (seg < 0) {
seg = 0
}
fileArray.push({
name: formatPath(seg == 0 ? path : path.substr(seg + 1)),
path: path,
url: getFileAccessHttpUrl(path),
})
})
fileList.value = [...fileArray]
}
const formatPath = (path) => {
let seg = path.lastIndexOf('.')
if (seg < 0) {
seg = 0
}
let pathSuffix = path.substr(seg)
if (path.length > 20) {
return path.substring(0, 15) + '(..)' + pathSuffix
} else {
return path
}
}
const fileType = (name) => {
let seg = name.lastIndexOf('.')
if (seg < 0) {
seg = 0
}
let pathSuffix = name.substr(seg + 1)
return imgTypeArr.indexOf(pathSuffix) == -1
}
watch(
() => props.value,
() => {
loadFile()
},
{ immediate: true },
)
</script>
<style scoped>
.ol-file-block {
display: block;
}
.ol-file-block .action {
-webkit-justify-content: flex-start;
justify-content: flex-start;
height: 54px;
line-height: 54px;
color: #333333;
}
.ol-file-list > .cu-item {
-webkit-justify-content: flex-start;
justify-content: flex-start;
height: 45px;
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<wd-upload
v-model:file-list="fileList"
accept="all"
:upload-method="customUpload"
:disabled="disabled"
:limit="maxNum"
:before-remove="delFile"
:multiple="maxNum && maxNum > 1"
></wd-upload>
</template>
<script lang="ts" setup>
import type { UploadMethod } from 'wot-design-uni/components/wd-upload/types'
import { getEnvBaseUploadUrl,getEnvBaseUrl } from '@/utils'
import { useUserStore } from '@/store'
import { getFileAccessHttpUrl } from '@/common/uitls'
import { isString } from '@/utils/is'
import { useToast } from 'wot-design-uni'
const toast = useToast()
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/sys/common/upload`
// 接收 props
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
value: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
name: {
type: String,
default: '',
required: false,
},
maxNum: {
type: Number,
default: null
},
uploadFileType: {
type: String,
default: 'image',
required: false,
},
})
// 定义 emits
const emit = defineEmits(['change', 'update:value'])
// 定义响应式数据
const fileList = ref([])
/**
* 自定义上传方法
* @param file
* @param formData
* @param options
*/
const customUpload: UploadMethod = (file, formData, options) => {
const userStore = useUserStore()
const uploadTask = uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
header: {
'X-Access-Token': userStore.userInfo.token,
'X-Tenant-Id': userStore.userInfo.tenantId,
...options.header,
},
name: options.name,
fileName: options.name,
fileType: options.fileType,
formData,
filePath: file.url,
success(res: any) {
if (res.statusCode === options.statusCode) {
let data = res.data
if (data && isString(data)) {
data = JSON.parse(data)
}
// 设置上传成功
if (data && data.success) {
const file = {
id: new Date().getTime(),
name: options.name,
path: data.message,
url: getFileAccessHttpUrl(data.message),
}
fileList.value.unshift(file)
changeOnlineFormValue()
}
} else {
// 设置上传失败
options.onError({ ...res, errMsg: res.errMsg || '' }, file, formData)
}
},
fail(err) {
console.info('upload fail', err)
// 设置上传失败
options.onError(err, file, formData)
},
})
// 设置当前文件加载的百分比
uploadTask.onProgressUpdate((res) => {
options.onProgress(res, file)
})
}
const changeOnlineFormValue = () => {
console.log('changeOnlineFormValue fileList.value', fileList)
const arr = fileList.value.map((item) => item['path'])
const str = arr.join(',')
emit('change', str)
emit('update:value', str)
}
const delFile = ({ file, fileList, resolve }) => {
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
console.log('当前删除文件', file)
changeOnlineFormValue()
toast.success('删除成功')
resolve(true)
}
},
fail: (err) => {
console.log(err)
resolve(false)
},
})
}
const loadFile = () => {
if (!props.value || props.value.length === 0) {
return
}
const pathArr = props.value.split(',')
const fileArray = []
pathArr.forEach((path) => {
const seg = path.lastIndexOf('/')
fileArray.push({
name: path.substr(seg < 0 ? 0 : seg),
path: path,
url: getFileAccessHttpUrl(path),
})
})
console.log('当前图片回显数据', fileArray)
fileList.value = [...fileArray]
}
// 监听 value 的变化
watch(
() => props.value,
() => {
loadFile()
},
{ immediate: true },
)
// 组件挂载时加载文件
onMounted(() => {
loadFile()
})
</script>

View File

@@ -0,0 +1,171 @@
<template>
<wd-upload
v-model:file-list="fileList"
accept="image"
:upload-method="customUpload"
:disabled="disabled"
:limit="maxNum"
:before-remove="delFile"
:multiple="maxNum && maxNum > 1"
></wd-upload>
</template>
<script lang="ts" setup>
import type { UploadMethod } from 'wot-design-uni/components/wd-upload/types'
import { getEnvBaseUploadUrl, getEnvBaseUrl } from '@/utils'
import { useUserStore } from '@/store'
import { getFileAccessHttpUrl } from '@/common/uitls'
import { isString } from '@/utils/is'
import { useToast } from 'wot-design-uni'
const toast = useToast()
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/sys/common/upload`
// 接收 props
const props = defineProps({
title: {
type: String,
default: '',
required: false,
},
value: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
name: {
type: String,
default: '',
required: false,
},
maxNum: {
type: Number,
default: null,
},
uploadFileType: {
type: String,
default: 'image',
required: false,
},
})
// 定义 emits
const emit = defineEmits(['change', 'update:value'])
// 定义响应式数据
const fileList = ref([])
/**
* 自定义上传方法
* @param file
* @param formData
* @param options
*/
const customUpload: UploadMethod = (file, formData, options) => {
const userStore = useUserStore()
const uploadTask = uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
header: {
'X-Access-Token': userStore.userInfo.token,
'X-Tenant-Id': userStore.userInfo.tenantId,
...options.header,
},
name: options.name,
fileName: options.name,
fileType: options.fileType,
formData,
filePath: file.url,
success(res: any) {
if (res.statusCode === options.statusCode) {
let data = res.data
if (data && isString(data)) {
data = JSON.parse(data)
}
// 设置上传成功
if (data && data.success) {
const file = {
id: new Date().getTime(),
name: options.name,
path: data.message,
url: getFileAccessHttpUrl(data.message),
}
fileList.value.unshift(file)
changeOnlineFormValue()
}
} else {
// 设置上传失败
options.onError({ ...res, errMsg: res.errMsg || '' }, file, formData)
}
},
fail(err) {
console.info('upload fail', err)
// 设置上传失败
options.onError(err, file, formData)
},
})
// 设置当前文件加载的百分比
uploadTask.onProgressUpdate((res) => {
options.onProgress(res, file)
})
}
const changeOnlineFormValue = () => {
console.log('changeOnlineFormValue fileList.value', fileList)
const arr = fileList.value.map((item) => item['path'])
const str = arr.join(',')
emit('change', str)
emit('update:value', str)
}
const delFile = ({ file, fileList, resolve }) => {
uni.showModal({
title: '提示',
content: '确定要删除吗?',
cancelText: '取消',
confirmText: '确定',
success: (res) => {
if (res.confirm) {
console.log('当前删除文件', file)
changeOnlineFormValue()
toast.success('删除成功')
resolve(true)
}
},
fail: (err) => {
console.log(err)
resolve(false)
},
})
}
const loadFile = () => {
if (!props.value || props.value.length === 0) {
return
}
const pathArr = props.value.split(',')
const fileArray = []
pathArr.forEach((path) => {
const seg = path.lastIndexOf('/')
fileArray.push({
name: path.substr(seg < 0 ? 0 : seg),
path: path,
url: getFileAccessHttpUrl(path),
})
})
console.log('当前图片回显数据', fileArray)
fileList.value = [...fileArray]
}
// 监听 value 的变化
watch(
() => props.value,
() => {
loadFile()
},
{ immediate: true },
)
// 组件挂载时加载文件
onMounted(() => {
loadFile()
})
</script>

View File

@@ -0,0 +1,150 @@
<template>
<wd-select-picker
:label-width="labelWidth"
:label="label"
v-model="selected"
filterable
clearable
:columns="options"
:disabled="disabled"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { isArray, isString } from 'lodash'
import {http} from "@/utils/http";
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [Array, String],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits([ 'change', 'update:modelValue'])
// 定义响应式数据
const selected = ref([]);
const options = ref([]);
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
}
else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
console.log("options.value ",options.value )
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
console.log("下拉多选handleChange",value);
let valueStr = "";
if (value && isArray(value)) {
valueStr = value.join(',');
}
emit('update:modelValue', valueStr);
emit('change', valueStr);
}
// 监听 dict 和 value 的变化
watch(() => props.dict, () => {
initSelections();
});
// 监听 value 的变化
watch(
() => props.modelValue,
() => {
if(props?.modelValue){
selected.value = isString(props.modelValue)? props.modelValue.split(','):[];
}else{
selected.value = []
}
},
{ deep: true, immediate: true }
)
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style lang="scss" scoped>
:deep(.wd-checkbox__shape){
border-radius:0 !important;
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<view>
<wd-picker
:columns="columns"
:label-width="labelWidth"
:label="label"
:required="required"
:disabled="disabled"
:placeholder="placeholder"
v-model="selected"
:column-change="onChangeDistrict"
@confirm="handleConfirm"
/>
</view>
</template>
<script lang="ts" setup>
import { getAreaArrByCode,getPcaOptionData } from '@/common/areaData/Area'
import {isString} from "@/utils/is";
// 接收 props
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '130px',
required: false,
},
value: {
type: [String,Array],
required: false,
},
placeholder: {
type: String,
required: false,
default: '请选择省市区',
},
disabled: {
type: Boolean,
default: false,
required: false,
},
required: {
type: Boolean,
default: false,
required: false,
},
backAll: {
type: Boolean,
default: true,
required: false,
},
})
// 定义 emits
const emits = defineEmits(['input', 'change', 'update:value'])
// 定义响应式数据
const selected = ref([])
const district = { ...getPcaOptionData() }
const columns = ref([
district[0],
district[district[0][0].value],
district[district[district[0][0].value][0].value]
])
const onChangeDistrict = (pickerView, value, columnIndex, resolve) => {
const item = value[columnIndex]
if (columnIndex === 0) {
pickerView.setColumnData(1, district[item.value])
pickerView.setColumnData(2, district[district[item.value][0].value])
} else if (columnIndex === 1) {
pickerView.setColumnData(2, district[item.value])
}
resolve()
}
const handleConfirm = ({value}) => {
if(value){
emits('update:value', props.backAll?value:value[2]);
}
}
// 监听 value 变化
watch(
() => props.value,
async (val) => {
if(props.value && isString(props.value)){
let arr = getAreaArrByCode(props.value);
selected.value = arr;
await initColumnData(arr);
}
},
{ immediate: true },
)
/**
* 初始化列数据
* @param val
*/
function initColumnData(val){
if(val && val.length){
let first = district[0];
let second = district[selected.value[0]];
let third = district[selected.value[1]];
columns.value = [first, second, third]
}
}
</script>

View File

@@ -0,0 +1,175 @@
<template>
<view class="Popup">
<view @click="handleClick">
<wd-input
:placeholder="`请选择${$attrs.label}`"
type="text"
readonly
v-model="showText"
clearable
v-bind="$attrs"
/>
</view>
<LinkRecordsModal
v-if="reportModal.show"
:selected="value"
ref="lrmRef"
:dictCode="dictCode"
:dictTable="dictTable"
:dictText="dictText"
:multi="multi"
:imageField="imageField"
@close="handleClose"
@change="handleChange"
></LinkRecordsModal>
</view>
</template>
<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast } from 'wot-design-uni'
import LinkRecordsModal from './link-records-modal.vue'
import {http} from "@/utils/http";
defineOptions({
name: 'onlinePopupLinkRecord',
options: {
styleIsolation: 'shared',
},
})
const props = defineProps({
value: {
type: String,
required: false,
},
name: {
type: String,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
required: {
type: Boolean,
default: true,
required: false,
},
formSchema: {
type: Object,
required: true,
},
})
const emit = defineEmits(['change', 'update:value','selected'])
const toast = useToast()
const lrmRef = ref()
const showText = ref('')
const selectVal = ref([])
const attrs: any = useAttrs()
const reportModal = reactive({
show: false,
})
//字典code
const dictCode = computed(() => props.formSchema?.dictCode)
//字典table
const dictTable = computed(() => props.formSchema?.dictTable)
//字典文本
const dictText = computed(() => props.formSchema?.dictText)
//是否多选
const multi = computed(() => {
if(props.formSchema?.fieldExtendJson){
const extendJson = JSON.parse(props.formSchema.fieldExtendJson)
return extendJson?.multiSelect
}
return false
})
//图片字段
const imageField = computed(() => {
if(props.formSchema?.fieldExtendJson){
const extendJson = JSON.parse(props.formSchema.fieldExtendJson)
return extendJson?.imageField
}
return ''
})
//首次加载
const firstLoad = ref(true);
/**
* 监听value数值
*/
watch(
() => props.value,
(val) => {
val && loadValue()
},
{ immediate: true },
)
//加载数据
function loadValue(){
console.log('关联记录loadValue',firstLoad.value)
if(!firstLoad.value){
return
}
let linkTableSelectFields = dictCode.value + ',' + dictText.value;
let superQueryParams = [{"field":"id","rule":"in","val": props.value}];
let param = {
linkTableSelectFields,
superQueryMatchType:"and",
superQueryParams: encodeURI(JSON.stringify(superQueryParams))
};
let titleField = props.formSchema?.dictText && props.formSchema?.dictText.split(",")[0];
http.get(`/online/cgform/api/getData/${dictTable.value}`,param).then((res:any)=>{
if(res.success){
let selectedList = res.result.records || [];
let labels = [];
let values = [];
selectedList.forEach(item=>{
if(item.id){
values.push(item.id);
labels.push(item[titleField]);
}
})
showText.value = labels.join(',');
selectVal.value = values;
emit('selected', selectedList,props.name);
}
})
firstLoad.value = false;
}
//回显数值
function callBack(rows) {
//匹配popup设置的回调值
let values = []
let labels = []
let titleField = props.formSchema?.dictText && props.formSchema?.dictText.split(",")[0];
rows.forEach(item=>{
if(item.id){
values.push(item.id);
labels.push(item[titleField]);
}
})
showText.value = labels.join(',')
selectVal.value = values
emit('selected', rows,props.name)
emit('change', values.join(','))
emit('update:value', values.join(','))
}
//点击事件
const handleClick = () => {
if (!props.disabled) {
reportModal.show = true
//lrmRef.value.beforeOpen(selectVal.value)
}
}
//关闭事件
const handleClose = () => {
reportModal.show = false
}
const handleChange = (data) => {
console.log('选中的值:', data)
callBack(data)
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,157 @@
<template>
<!-- #ifdef MP-WEIXIN -->
<wd-picker
:label-width="labelWidth"
:label="label"
clearable
filterable
v-model="selected"
:columns="options"
:disabled="disabled"
placeholder="请选择"
@confirm="handleChange"
></wd-picker>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<wd-select-picker
:label-width="labelWidth"
:show-confirm="false"
:label="label"
v-model="selected"
type="radio"
filterable
clearable
:columns="options"
:disabled="disabled"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
<!-- #endif -->
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { isArray, isString } from 'lodash'
import { http } from "@/utils/http";
import { isNullOrUnDef } from "@/utils/is"; // 假设使用 lodash 来判断类型
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [Array, String],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:modelValue'])
// 定义响应式数据
const selected = ref('');
const options = ref([]);
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
}
else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result;
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
emit('update:modelValue', value);
emit('change', value);
}
// 监听 dict 和 value 的变化
watch(() => props.dict, () => {
initSelections();
});
// 监听 value 的变化
watch(
() => props.modelValue,
(val) => {
selected.value = isNullOrUnDef(val)?'':val;
},
{ immediate: true },
)
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style></style>

View File

@@ -0,0 +1,163 @@
<template>
<!-- 带搜索 -->
<!-- #ifndef MP-WEIXIN -->
<wd-select-picker
:label-width="labelWidth"
:label="label"
type="radio"
filterable
v-model="selected"
:columns="options"
:disabled="disabled"
:show-confirm="false"
placeholder="请选择"
@change="handleChange"
></wd-select-picker>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<!-- 常规下拉 -->
<wd-picker
:label-width="labelWidth"
:label="label"
filterable
v-model="selected"
:columns="options"
:disabled="disabled"
placeholder="请选择"
@confirm="handleChange"
></wd-picker>
<!-- #endif -->
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue'
import { isArray, isString } from 'lodash'
import { http } from '@/utils/http'
import { isNullOrUnDef } from '@/utils/is'
// 定义 props
const props = defineProps({
dict: {
type: [Array, String],
default: () => [],
required: true,
},
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
dictStr: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
modelValue: {
type: [String, Number],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:modelValue'])
// 定义响应式数据
const selected = ref<any>('')
const options = ref<any>([])
// 初始化选项
const initSelections = async () => {
options.value = []
if (props.type === 'sel_search' && props.dictStr) {
let temp = props.dictStr
if (temp.indexOf(' ') > 0) {
temp = encodeURI(props.dictStr)
}
try {
const res: any = await http.get('/sys/dict/getDictItems/' + temp)
if (res.success) {
options.value = (res.result ?? []).filter((item) => item !== null)
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
if (!props.dict || props.dict.length === 0) {
return
}
if (isString(props.dict)) {
try {
let code = props.dict;
if (code.indexOf(',') > 0 && code.indexOf(' ') > 0) {
// 编码后类似sys_user%20where%20username%20like%20xxx' 是不包含空格的,这里判断如果有空格和逗号说明需要编码处理
code = encodeURI(code);
}
const res: any = await http.get('/sys/dict/getDictItems/' + code)
if (res.success) {
options.value = res.result
}
} catch (error) {
console.error('请求数据出错:', error)
}
} else {
props.dict.forEach((item) => {
options.value.push(item)
})
}
}
}
// 选择器改变事件处理函数
const handleChange = ({value}) => {
emit('update:modelValue', value)
emit('change', value)
}
// 监听 dict 和 value 的变化
watch(
() => props.dict,
() => {
initSelections()
},
)
// 监听 value 的变化
watch(
() => props.modelValue,
(val) => {
selected.value = isNullOrUnDef(props.modelValue) ? '' : val
},
{ immediate: true },
)
// 组件挂载时初始化选项
onMounted(() => {
initSelections()
})
</script>
<style lang="scss" scoped>
:deep(.wd-checkbox__shape) {
border-radius: 0 !important;
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<wd-datetime-picker :disabled="disabled" type="time" :labelWidth="labelWidth" v-model="currentTime" :label="label" @confirm="handleConfirm" />
</template>
<script setup>
// 定义 props
import dayjs from "dayjs";
const props = defineProps({
label: {
type: String,
default: '',
required: false,
},
labelWidth: {
type: String,
default: '80px',
required: false,
},
name: {
type: String,
default: '',
required: false,
},
type: {
type: String,
default: '',
required: false,
},
value: {
type: [String, Number,Date],
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
})
// 定义 emits
const emit = defineEmits(['input', 'change', 'update:value'])
// 定义响应式数据;
const currentTime = ref('')
// 监听 value 的变化
watch(
() => props.value,
(val) => {
currentTime.value = initData(val);
},
{
immediate:true
}
)
/**
*
* @param val
* @returns {*|string}
*/
function initData(time){
try {
if(typeof time == 'object' ){
time = dayjs(time).format('HH:mm:ss');
}
}catch (e) {
console.log("initData>>e",e);
}
return time?time:'';
}
// 选择器改变事件处理函数
const handleConfirm = (e) => {
emit('update:value', currentTime.value+':00')
emit('change', currentTime.value+':00')
}
</script>
<style></style>

View File

@@ -0,0 +1,109 @@
import type { App } from 'vue';
import StatusTip from '@/pages-work/components/statusTip.vue';
import JBar from '@/pages-work/components/echarts/JBar/index.vue';
import JBackgroundBar from '@/pages-work/components/echarts/JBackgroundBar/index.vue';
import JDynamicBar from '@/pages-work/components/echarts/JDynamicBar/index.vue';
import JStackBar from '@/pages-work/components/echarts/JStackBar/index.vue';
import JMultipleBar from '@/pages-work/components/echarts/JMultipleBar/index.vue';
import JNegativeBar from '@/pages-work/components/echarts/JNegativeBar/index.vue';
import JMixLineBar from '@/pages-work/components/echarts/JMixLineBar/index.vue';
import JProgress from '@/pages-work/components/echarts/JProgress/index.vue';
import JLine from '@/pages-work/components/echarts/JLine/index.vue';
import JMultipleLine from '@/pages-work/components/echarts/JMultipleLine/index.vue';
import JSmoothLine from '@/pages-work/components/echarts/JSmoothLine/index.vue';
import JStepLine from '@/pages-work/components/echarts/JStepLine/index.vue';
import JPie from '@/pages-work/components/echarts/JPie/index.vue';
import JRing from '@/pages-work/components/echarts/JRing/index.vue';
import JFunnel from '@/pages-work/components/echarts/JFunnel/index.vue';
import JPyramidFunnel from '@/pages-work/components/echarts/JPyramidFunnel/index.vue';
import JRadar from '@/pages-work/components/echarts/JRadar/index.vue';
import JCircleRadar from '@/pages-work/components/echarts/JCircleRadar/index.vue';
import JGauge from '@/pages-work/components/echarts/JGauge/index.vue';
import JColorGauge from '@/pages-work/components/echarts/JColorGauge/index.vue';
import JScatter from '@/pages-work/components/echarts/JScatter/index.vue';
import JBubble from '@/pages-work/components/echarts/JBubble/index.vue';
import DoubleLineBar from '@/pages-work/components/echarts/DoubleLineBar/index.vue';
import JRose from '@/pages-work/components/echarts/JRose/index.vue';
import JHorizontalBar from '@/pages-work/components/echarts/JHorizontalBar/index.vue';
import JArea from '@/pages-work/components/echarts/JArea/index.vue';
import JPictorial from '@/pages-work/components/echarts/JPictorial/index.vue';
import JPictorialBar from '@/pages-work/components/echarts/JPictorialBar/index.vue';
import JAreaMap from '@/pages-work/components/echarts/map/JAreaMap/index.vue';
import JBubbleMap from '@/pages-work/components/echarts/map/JBubbleMap/index.vue';
import JBarMap from '@/pages-work/components/echarts/map/JBarMap/index.vue';
import JHeatMap from '@/pages-work/components/echarts/map/JHeatMap/index.vue';
import JFlyLineMap from '@/pages-work/components/echarts/map/JFlyLineMap/index.vue';
import JTotalFlyLineMap from '@/pages-work/components/echarts/map/TotalFlyLineMap/index.vue';
import JTotalBarMap from '@/pages-work/components/echarts/map/TotalBarMap/index.vue';
//非echart组件
import JCarousel from '@/pages-work/components/drag/carousel/index.vue';
import JIframe from '@/pages-work/components/drag/iframe/index.vue';
import JDragEditor from '@/pages-work/components/drag/editor/index.vue';
import JImg from '@/pages-work/components/drag/img/index.vue';
import JNumber from '@/pages-work/components/drag/number/index.vue';
import JText from '@/pages-work/components/drag/text/index.vue';
import JCalendar from '@/pages-work/components/drag/calendar/index.vue';
import JCurrentTime from '@/pages-work/components/drag/currentTime/time.vue';
import JList from '@/pages-work/components/drag/list/index.vue';
import JRadioButton from '@/pages-work/components/drag/radiobutton/index.vue';
import JCommonTable from '@/pages-work/components/drag/table/index.vue';
import JQuickNav from '@/pages-work/components/drag/JQuickNav/index.vue';
import JForm from '@/pages-work/components/drag/form/index.vue';
// 全局注册组件
export function registerGlobComp(app: App) {
app.component('statusTip', StatusTip)
app.component('JBar', JBar)
app.component('JMultipleBar', JMultipleBar)
app.component('JNegativeBar', JNegativeBar)
app.component('JLine', JLine)
app.component('JMultipleLine', JMultipleLine)
app.component('JPie', JPie)
app.component('JRing', JRing)
app.component('JFunnel', JFunnel)
app.component('JPyramidFunnel', JPyramidFunnel)
app.component('JRadar', JRadar)
app.component('JCircleRadar', JCircleRadar)
app.component('JGauge', JGauge)
app.component('JColorGauge', JColorGauge)
app.component('JScatter', JScatter)
app.component('JBubble', JBubble)
app.component('DoubleLineBar', DoubleLineBar)
app.component('JRose', JRose)
app.component('JHorizontalBar', JHorizontalBar)
app.component('JArea', JArea)
app.component('JBackgroundBar', JBackgroundBar)
app.component('JDynamicBar', JDynamicBar)
app.component('JMixLineBar', JMixLineBar)
app.component('JStackBar', JStackBar)
app.component('JStepLine', JStepLine)
app.component('JSmoothLine', JSmoothLine)
app.component('JProgress', JProgress)
app.component('JPictorial', JPictorial)
app.component('JPictorialBar', JPictorialBar)
app.component('JAreaMap', JAreaMap)
app.component('JBubbleMap', JBubbleMap)
app.component('JBarMap', JBarMap)
app.component('JHeatMap', JHeatMap)
app.component('JFlyLineMap', JFlyLineMap)
app.component('JTotalFlyLineMap', JTotalFlyLineMap)
app.component('JTotalBarMap', JTotalBarMap)
//非echart组件
app.component('JCarousel', JCarousel)
app.component('JIframe', JIframe)
app.component('JDragEditor', JDragEditor)
app.component('JImg', JImg)
app.component('JNumber', JNumber)
app.component('JText', JText)
app.component('JCalendar', JCalendar)
app.component('JCurrentTime', JCurrentTime)
app.component('JList', JList)
app.component('JRadioButton', JRadioButton)
app.component('JCommonTable', JCommonTable)
app.component('JQuickNav', JQuickNav)
app.component('JForm', JForm)
}

33
JeecgUniapp-master/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,33 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
/** 网站标题,应用名称 */
readonly VITE_APP_TITLE: string
/** 服务端口号 */
readonly VITE_SERVER_PORT: string
/** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string
/** H5是否需要代理 */
readonly VITE_APP_PROXY: 'true' | 'false'
/** H5是否需要代理需要的话有个前缀 */
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
/** 上传图片地址 */
readonly VITE_UPLOAD_BASEURL: string
/** 是否清除console */
readonly VITE_DELETE_CONSOLE: string
/** 是否开启mock */
readonly VITE_USE_MOCK: 'true' | 'false'
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -0,0 +1,295 @@
import { geoDistance } from '@/common/uitls'
// #ifdef MP-WEIXIN || APP-PLUS
import amap from '@/common/js-sdk/js-amap/amap-wx'
// #endif
// #ifdef H5
import H5Map from '@/common/js-sdk/js-amap/amap-h5'
// #endif
export default function useGeo<T>({ compLatitude, compLongitude }, emit?) {
// 定义响应式数据
const amapPlugin = ref(null)
//高德地图微信key
const wxMapKey = import.meta.env.VITE_GD_MAP_KEY__WEIXIN
console.log('wxMapKey', wxMapKey)
//打卡距离
const distance = ref(0)
//地址
const address = ref('')
//纬度
const latitude = ref(compLatitude)
//经度
const longitude = ref(compLongitude)
//地图缩放程度
const scale = ref(16)
//地图标记
const marker = ref([])
//在地图上显示圆
const circles = ref([
{
latitude: compLatitude,
longitude: compLongitude,
radius: 80,
fillColor: '#ffffffAA',
color: '#55aaffAA',
strokeWidth: 1,
},
])
//地图配置
const resAmap = ref(null)
// 在打卡范围内
const inCircle = () => {
return distance.value <= 80
}
//刷新定位
const refreshLocation = () => {
// #ifdef MP-WEIXIN
getAuthorizeInfo()
// #endif
// #ifdef APP-PLUS
getLocationInfo()
// #endif
// #ifdef H5
initH5Map()
// #endif
}
/**
* 权限提示
*/
const getAuthorizeInfo = () => {
//1. uniapp弹窗弹出获取授权地理个人微信信息等授权信息弹窗
uni.authorize({
scope: 'scope.userLocation',
success() {
//1.1 允许授权
getLocationInfo()
},
fail() {
console.log('你拒绝了授权,无法获得周边信息')
openConfirm()
},
})
}
const getLocationInfo = () => {
uni.showLoading({
title: '定位中...',
mask: true,
})
amapPlugin.value.getRegeo({
type: 'gcj02',
success: function (res) {
setTimeout(() => {
uni.hideLoading()
}, 2000)
latitude.value = res[0].latitude
longitude.value = res[0].longitude
address.value = res[0].name + res[0].desc
distance.value = geoDistance(longitude.value, latitude.value, compLongitude, compLatitude)
console.log('getLocationInfoWx***distance>>>', distance.value)
let tipTextVal = (distance.value > 80 ? '不在' : '已在') + '打卡范围'
let bgColorVal = distance.value > 80 ? '#ff0000' : '#00c16f'
let markerVal = {
id: 0,
latitude: latitude.value,
longitude: longitude.value,
iconPath: '/static/location.png',
width: 35,
height: 35,
// #ifdef MP-WEIXIN
//为标记点旁边增加标签
label: {
content: tipTextVal, //文本
color: '#ffffff', //文本颜色
fontSize: 14, //文字大小
borderWidth: 2, //边框宽度
borderColor: bgColorVal, //边框颜色
bgColor: bgColorVal, //背景颜色
borderRadius: 2, //边框圆角
padding: 5, //文本边缘留白
textAlign: 'center', //文本对齐方式
x: 0, //label的坐标原点是 marker 对应的经纬度
y: 0, //label的坐标原点是 marker 对应的经纬度
},
// #endif
// #ifdef APP-PLUS
//自定义标记点上方的气泡窗口 点击有效
callout: {
content: tipTextVal, //文本
color: '#ffffff', //文字颜色
fontSize: 14, //文本大小
bgColor: bgColorVal, //背景颜色
display: 'ALWAYS', //常显
textAlign: 'center',
},
// #endif
}
marker.value = [markerVal]
emit && emit('success', {})
},
fail: (res) => {
emit && emit('fail', res)
setTimeout(() => {
uni.hideLoading()
}, 2000)
},
})
//TODO h5端可能因为非https域名会导致一直处在定位中的状态因此设置10s后清除加载状态
setTimeout(() => {
uni.hideLoading()
}, 10000)
}
/**
* 获取地理授权
*/
const openConfirm = () => {
uni.showModal({
title: '请求授权当前位置',
content: '需要获取您的地理位置,请确认授权',
success: (res) => {
if (res.confirm) {
uni.openSetting()
} else if (res.cancel) {
uni.showToast({
title: '你拒绝了授权,无法获得位置信息',
icon: 'none',
duration: 1000,
})
}
},
})
}
/**
* 获取地理位置信息
*/
const getMyAddress = computed(()=>{
return address.value
})
/**
* 根据坐标返回地址(逆地理编码)
* @param points
* @returns {Promise<void>}
*/
const getAddress = async (points) => {
try {
resAmap.value = await H5Map()
resAmap.value.plugin('AMap.Geocoder', () => {
const geocoder = new resAmap.value.Geocoder({
radius: 1000,
})
geocoder.getAddress(points, (status, result) => {
if (status === 'complete' && result.regeocode.formattedAddress) {
address.value = result.regeocode.formattedAddress
}
})
})
} catch (e) {
console.log(e)
}
}
// #ifdef H5
const initH5Map = async () => {
try {
uni.showLoading({
title: '定位中...',
mask: true,
})
setTimeout(() => {
uni.hideLoading()
}, 10000)
resAmap.value = await H5Map()
resAmap.value.plugin('AMap.Geolocation', () => {
const geolocation = new resAmap.value.Geolocation({
enableHighAccuracy: true,
timeout: 10000,
buttonPosition: 'RB',
zoomToAccuracy: true,
})
geolocation.getCurrentPosition(function (status, result) {
if (status == 'complete') {
onComplete(result)
} else {
onError(result)
}
})
})
const onComplete = (data) => {
console.log('H5高德定位', data)
console.log('当前位置的经度:' + data.position.lat)
console.log('当前位置的纬度:' + data.position.lng)
distance.value = geoDistance(
data.position.lng,
data.position.lat,
compLongitude,
compLatitude,
)
console.log('打卡距离distance.value' + distance.value)
let tipTextVal = (distance.value > 80 ? '未在' : '已在') + '打卡范围内'
let bgColorVal = distance.value > 80 ? '#ff0000' : '#00c16f'
longitude.value = data.position.lng
latitude.value = data.position.lat
address.value = data?.formattedAddress
let markerVal = {
latitude: latitude.value,
longitude: longitude.value,
iconPath: '/static/location.png',
callout: {
content: tipTextVal,
color: '#ffffff',
fontSize: 14,
borderRadius: 2,
bgColor: bgColorVal,
display: 'BYCLICK',
},
}
marker.value = [markerVal]
uni.hideLoading()
getAddress([longitude.value, latitude.value])
emit && emit('success', {})
}
const onError = (data) => {
console.log(data)
emit && emit('fail', data)
}
} catch (e) {
console.log(e)
uni.hideLoading()
}
}
// #endif
onBeforeMount(() => {
// #ifdef MP-WEIXIN || APP-PLUS
amapPlugin.value = new amap.AMapWX({
key: wxMapKey,
})
// #endif
})
// 生命周期钩子函数
onMounted(() => {
// #ifdef MP-WEIXIN
getAuthorizeInfo()
// #endif
// #ifdef H5
initH5Map()
// #endif
// #ifdef APP-PLUS
getLocationInfo()
// #endif
})
return [
{ inCircle, distance, getAddress,getMyAddress,latitude,longitude,marker,scale,circles },
{ getAuthorizeInfo, getLocationInfo,refreshLocation },
]
}

View File

@@ -0,0 +1,45 @@
import {useToast} from "wot-design-uni";
import {useRouter} from "@/plugin/uni-mini-router";
import {useParamsStore} from "@/store/page-params";
import {ref} from "vue";
import {http} from "@/utils/http";
export default function usePageList<T = string>(list,params={}) {
const toast = useToast()
const router = useRouter()
const paramsStore = useParamsStore()
const paging = ref(null)
const pageNo = ref(1)
const pageSize = ref(10)
const pageTotal = ref(1)
const dataList = ref([])
const queryParams = () => {
return {
pageNo: pageNo.value,
pageSize: pageSize.value,
order: 'desc',
column: 'createTime',
hasQuery: true,
}
}
const queryList = (_pageNo, _pageSize) => {
pageNo.value = _pageNo
pageSize.value = _pageSize
let allParams = { ...queryParams(),...params };
http
.get(list, allParams)
.then((res: any) => {
if (res.success) {
paging.value.complete(res.result?.records ?? [])
} else {
toast.warning(res.message)
}
})
.catch((res) => {
toast.error('加载数据失败~')
})
}
return { toast, router, paging,paramsStore, dataList, queryParams, queryList }
}

View File

@@ -0,0 +1,44 @@
import { UnwrapRef } from 'vue'
type IUseRequestOptions<T> = {
/** 是否立即执行 */
immediate?: boolean
/** 初始化数据 */
initialData?: T
}
/**
* useRequest是一个定制化的请求钩子用于处理异步请求和响应。
* @param func 一个执行异步请求的函数返回一个包含响应数据的Promise。
* @param options 包含请求选项的对象 {immediate, initialData}。
* @param options.immediate 是否立即执行请求默认为false。
* @param options.initialData 初始化数据默认为undefined。
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
*/
export default function useRequest<T>(
func: () => Promise<IResData<T>>,
options: IUseRequestOptions<T> = { immediate: false },
) {
const loading = ref(false)
const error = ref(false)
const data = ref<T>(options.initialData)
const run = async () => {
loading.value = true
return func()
.then((res) => {
data.value = res.data as UnwrapRef<T>
error.value = false
return data.value
})
.catch((err) => {
error.value = err
throw err
})
.finally(() => {
loading.value = false
})
}
options.immediate && run()
return { loading, error, data, run }
}

View File

@@ -0,0 +1,86 @@
// TODO: 别忘加更改环境变量的 VITE_UPLOAD_BASEURL 地址。
import { getEnvBaseUploadUrl } from '@/utils'
import { useUserStore } from '@/store/user'
const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}`
/**
* useUpload 是一个定制化的请求钩子,用于处理上传图片。
* @param formData 额外传递给后台的数据,如{name: '张三'}。
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
*/
export default function useUpload<T = string>(
formData: Record<string, any> = {},
{ url, sizeType = ['original', 'compressed'], sourceType = ['album', 'camera'] },
) {
const loading = ref(false)
const error = ref(false)
const data = ref<T>()
const run = () => {
// #ifdef MP-WEIXIN
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
// 微信小程序在2023年10月17日之后使用本API需要配置隐私协议
uni.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType,
sizeType, //可以指定是原图还是压缩图,默认二者都有
success: (res) => {
loading.value = true
const tempFilePath = res.tempFiles[0].tempFilePath
const fileName = res.tempFiles[0].name || res.tempFiles[0].tempFilePath.substring(tempFilePath.lastIndexOf('/') + 1)
formData.fileName = fileName;
uploadFile<T>({ url, tempFilePath, formData, data, error, loading, fileName })
},
fail: (err) => {
console.error('uni.chooseMedia err->', err)
error.value = true
},
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage({
count: 1,
sourceType, //从相册选择
sizeType, //可以指定是原图还是压缩图,默认二者都有
success: (res) => {
loading.value = true
const tempFilePath = res.tempFilePaths[0]
const fileName = res.tempFiles[0].name || tempFilePath.substring(tempFilePath.lastIndexOf('/') + 1)
formData.fileName = fileName;
uploadFile<T>({ url, tempFilePath, formData, data, error, loading })
},
fail: (err) => {
console.error('uni.chooseImage err->', err)
error.value = true
},
})
// #endif
}
return { loading, error, data, run }
}
function uploadFile<T>({ url, tempFilePath, formData, data, error, loading }) {
const userStore = useUserStore()
uni.uploadFile({
url: url ?? VITE_UPLOAD_BASEURL,
filePath: tempFilePath,
name: formData.name ?? 'file',
formData,
header: {
'X-Access-Token': userStore.userInfo.token,
'X-Tenant-Id': userStore.userInfo.tenantId,
},
success: (uploadFileRes) => {
data.value = JSON.parse(uploadFileRes.data)
},
fail: (err) => {
console.error('uni.uploadFile err->', err)
error.value = true
},
complete: () => {
loading.value = false
},
})
}

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JEECG BOOT</title>
<style type="text/css">
#iframe {
width: 100%;
z-index: 99;
}
</style>
</head>
<body>
<iframe id="iframe" frameborder="0" ></iframe>
<!-- uni SDK -->
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
<script type="text/javascript">
let mId;
document.addEventListener('UniAppJSBridgeReady', () => {
let {src,mid,height} = plus.webview.currentWebview();
let iframe = document.getElementById("iframe");
iframe.height = height;
document.getElementById("iframe").src=src;
mId=mid;
});
window.addEventListener('message', function(event) {
let { messageId, type, data } = event.data
if ( mId !== messageId) {
return
}
let dataString=JSON.stringify(data);
location.href = `callback?data=${escape(dataString)}&type=${escape(type)}`;
}, false)
</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More