初始化产品库

This commit is contained in:
aixianling
2021-11-15 10:29:05 +08:00
parent 8f735a4ffe
commit 5440b43b9c
306 changed files with 54508 additions and 3 deletions

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
.DS_Store
node_modules/
unpackage/
dist/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.project
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
/package-lock.json
/.hbuilderx/launch.json

View File

@@ -1,3 +0,0 @@
# dv_cp_wechat_app
数字乡村2.0 - 企微工作台APP库

64
babel.config.js Normal file
View File

@@ -0,0 +1,64 @@
const plugins = []
if (process.env.UNI_OPT_TREESHAKINGNG) {
plugins.push(require('@dcloudio/vue-cli-plugin-uni-optimize/packages/babel-plugin-uni-api/index.js'))
}
if (
(
process.env.UNI_PLATFORM === 'app-plus' &&
process.env.UNI_USING_V8
) ||
(
process.env.UNI_PLATFORM === 'h5' &&
process.env.UNI_H5_BROWSER === 'builtin'
)
) {
const path = require('path')
const isWin = /^win/.test(process.platform)
const normalizePath = path => (isWin ? path.replace(/\\/g, '/') : path)
const input = normalizePath(process.env.UNI_INPUT_DIR)
try {
plugins.push([
require('@dcloudio/vue-cli-plugin-hbuilderx/packages/babel-plugin-console'),
{
file(file) {
file = normalizePath(file)
if (file.indexOf(input) === 0) {
return path.relative(input, file)
}
return false
}
}
])
} catch (e) {
}
}
process.UNI_LIBRARIES = process.UNI_LIBRARIES || ['@dcloudio/uni-ui']
process.UNI_LIBRARIES.forEach(libraryName => {
plugins.push([
'import',
{
'libraryName': libraryName,
'customName': (name) => {
return `${libraryName}/lib/${name}/${name}`
}
}
])
})
module.exports = {
presets: [
[
'@vue/app',
{
modules: 'commonjs',
useBuiltIns: process.env.UNI_PLATFORM === 'h5' ? 'usage' : 'entry'
}
]
],
plugins
}

84
package.json Normal file
View File

@@ -0,0 +1,84 @@
{
"name": "dv_cp_wechat",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve --minimize",
"build": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
"info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js",
"test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@dcloudio/uni-app-plus": "^2.0.0-31820210406002",
"@dcloudio/uni-h5": "^2.0.0-31820210406002",
"@dcloudio/uni-helper-json": "*",
"@dcloudio/uni-mp-360": "^2.0.0-31820210406002",
"@dcloudio/uni-mp-alipay": "^2.0.0-31820210406002",
"@dcloudio/uni-mp-baidu": "^2.0.0-31820210406002",
"@dcloudio/uni-mp-kuaishou": "^2.0.0-31820210406002",
"@dcloudio/uni-mp-qq": "^2.0.0-31820210406002",
"@dcloudio/uni-mp-toutiao": "^2.0.0-31820210406002",
"@dcloudio/uni-mp-vue": "^2.0.0-31820210406002",
"@dcloudio/uni-mp-weixin": "^2.0.0-31820210406002",
"@dcloudio/uni-quickapp-native": "^2.0.0-31820210406002",
"@dcloudio/uni-quickapp-webview": "^2.0.0-31820210406002",
"@dcloudio/uni-stat": "^2.0.0-31820210406002",
"@vue/shared": "^3.0.0",
"axios": "^0.21.1",
"core-js": "^3.11.0",
"dayjs": "^1.10.6",
"echarts": "^4.9.0",
"recorder-core": "^1.1.21080800",
"regenerator-runtime": "^0.12.1",
"vue": "^2.6.11",
"vuedraggable": "^2.24.3",
"vuex": "^3.2.0",
"vuex-persistedstate": "^4.0.0-beta.3"
},
"devDependencies": {
"@babel/runtime": "~7.12.0",
"@dcloudio/types": "^2.2.1",
"@dcloudio/uni-automator": "^2.0.0-31820210406002",
"@dcloudio/uni-cli-i18n": "^2.0.0-32920211029001",
"@dcloudio/uni-cli-shared": "^2.0.0-31820210406002",
"@dcloudio/uni-i18n": "^2.0.0-32920211029001",
"@dcloudio/uni-migration": "^2.0.0-31820210406002",
"@dcloudio/uni-template-compiler": "^2.0.0-31820210406002",
"@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.0-31820210406002",
"@dcloudio/vue-cli-plugin-uni": "^2.0.0-31820210406002",
"@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.0-31820210406002",
"@dcloudio/webpack-uni-mp-loader": "^2.0.0-31820210406002",
"@dcloudio/webpack-uni-pages-loader": "^2.0.0-31820210406002",
"@vue/cli-plugin-babel": "^4.5.12",
"@vue/cli-service": "~4.5.0",
"babel-plugin-import": "^1.11.0",
"cross-env": "^7.0.2",
"jest": "^25.4.0",
"mini-types": "*",
"miniprogram-api-typings": "^3.3.2",
"node-sass": "npm:dart-sass@^1.25.0",
"postcss-comment": "^2.0.0",
"sass-loader": "^7.1.0",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [
"Android >= 4",
"ios >= 8"
],
"uni-app": {
"scripts": {
"cp-weixin": {
"title": "企业微信端",
"env": {
"UNI_PLATFORM": "mp-weixin"
},
"define": {
"CP-WEIXIN": true
}
}
}
}
}

22
postcss.config.js Normal file
View File

@@ -0,0 +1,22 @@
const path = require('path')
module.exports = {
parser: require('postcss-comment'),
plugins: [
require('postcss-import')({
resolve (id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
}),
require('autoprefixer')({
remove: process.env.UNI_PLATFORM !== 'h5'
}),
require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
]
}

View File

@@ -0,0 +1 @@
BeLokK3oa5SpR9ER

View File

@@ -0,0 +1 @@
mSayjTrg7ZGXGC2D

View File

@@ -0,0 +1 @@
ual6jgcTCMb4cdLb

28
public/index.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.documentElement.style.fontSize = document.documentElement.clientWidth / 20 + 'px'
})
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>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css"/>
</head>
<body>
<noscript>
<strong>Please enable JavaScript to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

164
src/App.vue Normal file
View File

@@ -0,0 +1,164 @@
<script>
import {mapMutations, mapState} from 'vuex'
export default {
provide() {
return {
root: this,
}
},
onLaunch: function () {
this.initConfig()
},
onShow: function () {
this.initWaterMarker()
},
onPageNotFound() {
this.logout()
},
computed: {
...mapState(['token', 'user']),
},
methods: {
...mapMutations(['initWaterMarker', 'logout', 'getConfig']),
initDev() {
let baseURL = 'http://192.168.1.87:9000'
this.getConfig({baseURL})
// this.$store.commit('login', 'bearer 88dd207a-dfe3-4f81-b9bd-e379de427d0b')
},
initConfig() {
if (process.env.NODE_ENV == 'development') this.initDev()
else {
let baseURL = location.origin
this.getConfig({baseURL})
}
},
},
}
</script>
<style lang="scss">
@import 'uview/index.scss';
@import './common/iconfont.css';
body {
font-family: 'Microsoft YaHei', serif;
}
uni-page-body {
min-height: 100%;
background: #f5f5f5;
position: relative;
}
div[bottom] {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 128px;
padding: 24px 32px;
box-sizing: border-box;
background: #ffffff;
border-top: 1px solid #d4d4d4;
display: flex;
gap: 32px;
& > * {
flex: 1;
min-width: 0;
& + * {
margin-left: 32px;
}
}
}
div[flex] {
display: flex;
align-items: center;
&.spb {
justify-content: space-between;
}
&.wrap {
flex-wrap: wrap;
}
&.column {
flex-direction: column;
}
&.start {
align-items: flex-start;
}
}
div[shrink] {
flex-shrink: 0;
}
uni-button {
border-radius: 4px;
&.u-btn--primary {
background-color: $uni-color-primary !important;
border-color: $uni-color-primary !important;
}
&:after {
border-radius: 4px;
}
}
.fill {
flex: 1;
min-width: 0;
min-height: 0;
}
.u-form-item {
width: 100%;
min-height: 100px;
background: #ffffff;
padding: 0 32px !important;
box-sizing: border-box;
display: flex;
line-height: normal !important;
.u-form-item__body {
height: inherit;
flex: 1;
.u-form-item--left {
min-height: 100px;
}
.u-form-item--right__content__slot {
padding-bottom: 10px;
}
}
.u-form-item__message {
margin: 15px 0;
}
}
@keyframes mapWarn {
0% {
transform: scale(.5);
opacity: 1
}
30% {
opacity: .5
}
to {
transform: scale(1.4);
opacity: 0
}
}
</style>

61
src/common/axios.js Normal file
View File

@@ -0,0 +1,61 @@
import axios from 'axios'
import store from '../store'
import util from "./util";
let instance = axios.create({
timeout: 600000,
withCredentials: true,
})
instance.interceptors.request.use(config => {
store.commit('initWaterMarker')
if (!config.withoutToken && store.state.token) {
config.headers["Authorization"] = store.state.token
}
return config
}, err => {
console.error(err)
})
instance.interceptors.response.use(res => {
if (res.data) {
if (res.data.code) {
if (res.data.code == 0) {
return res.data
} else if (res.data.code == 401) {
store.commit("logout");
let reg = new RegExp('.*code=(.+$)', "g")
if (reg.test(location.search)) {
let code = location.search.replace(reg, '$1')
store.commit('bindAccount', {
code, then: res => {
store.commit("login", [res?.token_type, res?.access_token].join(" ").trim())
location.href = location.href.replace('code=' + code, '')
}
})
} else util.confirm("用户信息验证失效,是否要重新登录?").then(() => {
store.commit('redirectCode')
// let app = store.state.apps?.find(e => location.href.indexOf(e.path) > -1)
// const goto = path => {
// location.href = location.origin + "/pages/loading?" + path
// }
// if (app) {
// goto(location.search + `&app=${app.key}`)
// } else {
// goto(location.search + `#error`)
// }
}).catch(() => 0)
} else {
console.error(res.data.msg || "请求失败!")
return Promise.reject(res.data.msg)
}
} else {
return res.data
}
} else {
console.error("服务器异常,请联系管理员!")
}
}, err => {
console.error(err)
})
export default instance

50
src/common/dict.js Normal file
View File

@@ -0,0 +1,50 @@
import request from "./axios";
import store from "../store";
/**
* 封装字典工具类
*/
const $dict = {
vueStore: null,
url: "/admin/dictionary/queryValsByCodeList",
setUrl(v) {
this.url = v
},
load(...code) {
if (!this.vueStore) this.vueStore = store
return request.post(this.url, null, {
params: {
codeList: code.join(',')
}
}).then((res) => {
this.vueStore.commit("setDicts", res.data)
})
},
getDict(key) {
let dict = this.vueStore.getters.getDict(key)
return dict ? dict.values : []
},
getValue(key, label) {
let dict = this.vueStore.getters.getDict(key)
if (dict) {
let item = dict.values.find(v => v.dictName == label)
return item ? item.dictValue : label
} else return label
},
getLabel(key, value) {
let dict = this.vueStore.getters.getDict(key)
if (dict) {
let item = dict.values.find(v => v.dictValue == value)
return item ? item.dictName : value
} else return value
},
getColor(key, value) {
let dict = this.vueStore.getters.getDict(key)
if (dict) {
let item = dict.values.find(v => v.dictValue == value)
return item ? item.dictColor : value
} else return value
},
}
export default $dict

712
src/common/iconfont.css Normal file

File diff suppressed because one or more lines are too long

245
src/common/permission.js Normal file
View File

@@ -0,0 +1,245 @@
/// null = 未请求1 = 已允许0 = 拒绝|受限, 2 = 系统未开启
var isIOS
function album() {
var result = 0;
var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");
var authStatus = PHPhotoLibrary.authorizationStatus();
if (authStatus === 0) {
result = null;
} else if (authStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(PHPhotoLibrary);
return result;
}
function camera() {
var result = 0;
var AVCaptureDevice = plus.ios.import("AVCaptureDevice");
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
if (authStatus === 0) {
result = null;
} else if (authStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
}
function location() {
var result = 0;
var cllocationManger = plus.ios.import("CLLocationManager");
var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
if (!enable) {
result = 2;
} else if (status === 0) {
result = null;
} else if (status === 3 || status === 4) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(cllocationManger);
return result;
}
function push() {
var result = 0;
var UIApplication = plus.ios.import("UIApplication");
var app = UIApplication.sharedApplication();
var enabledTypes = 0;
if (app.currentUserNotificationSettings) {
var settings = app.currentUserNotificationSettings();
enabledTypes = settings.plusGetAttribute("types");
if (enabledTypes == 0) {
result = 0;
console.log("推送权限没有开启");
} else {
result = 1;
console.log("已经开启推送功能!")
}
plus.ios.deleteObject(settings);
} else {
enabledTypes = app.enabledRemoteNotificationTypes();
if (enabledTypes == 0) {
result = 3;
console.log("推送权限没有开启!");
} else {
result = 4;
console.log("已经开启推送功能!")
}
}
plus.ios.deleteObject(app);
plus.ios.deleteObject(UIApplication);
return result;
}
function contact() {
var result = 0;
var CNContactStore = plus.ios.import("CNContactStore");
var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
if (cnAuthStatus === 0) {
result = null;
} else if (cnAuthStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(CNContactStore);
return result;
}
function record() {
var result = null;
var avaudiosession = plus.ios.import("AVAudioSession");
var avaudio = avaudiosession.sharedInstance();
var status = avaudio.recordPermission();
console.log("permissionStatus:" + status);
if (status === 1970168948) {
result = null;
} else if (status === 1735552628) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(avaudiosession);
return result;
}
function calendar() {
var result = null;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
if (ekAuthStatus == 3) {
result = 1;
console.log("日历权限已经开启");
} else {
console.log("日历权限没有开启");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
function memo() {
var result = null;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
if (ekAuthStatus == 3) {
result = 1;
console.log("备忘录权限已经开启");
} else {
console.log("备忘录权限没有开启");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
function requestIOS(permissionID) {
return new Promise((resolve, reject) => {
switch (permissionID) {
case "push":
resolve(push());
break;
case "location":
resolve(location());
break;
case "record":
resolve(record());
break;
case "camera":
resolve(camera());
break;
case "album":
resolve(album());
break;
case "contact":
resolve(contact());
break;
case "calendar":
resolve(calendar());
break;
case "memo":
resolve(memo());
break;
default:
resolve(0);
break;
}
});
}
function requestAndroid(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID],
function(resultObj) {
var result = 0;
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('已获取的权限:' + grantedPermission);
result = 1
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log('拒绝本次申请的权限:' + deniedPresentPermission);
result = 0
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);
result = -1
}
resolve(result);
},
function(error) {
console.log('result error: ' + error.message)
resolve({
code: error.code,
message: error.message
});
}
);
});
}
function gotoAppPermissionSetting() {
if (permission.isIOS) {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
var setting2 = NSURL2.URLWithString("app-settings:");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var Uri = plus.android.importClass("android.net.Uri");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
const permission = {
get isIOS(){
return typeof isIOS === 'boolean' ? isIOS : (isIOS = uni.getSystemInfoSync().platform === 'ios')
},
requestIOS: requestIOS,
requestAndroid: requestAndroid,
gotoAppSetting: gotoAppPermissionSetting
}
module.exports = permission

59
src/common/util.js Normal file
View File

@@ -0,0 +1,59 @@
import dict from "./dict";
import toast from '../uview/libs/function/toast'
import addUnit from '../uview/libs/function/addUnit'
import $parent from '../uview/libs/function/$parent'
import guid from '../uview/libs/function/guid'
import deepClone from '../uview/libs/function/deepClone'
import debounce from '../uview/libs/function/debounce'
import throttle from '../uview/libs/function/throttle'
import trim from '../uview/libs/function/trim'
import {sys} from '../uview/libs/function/sys'
import test from '../uview/libs/function/test'
import config from '../uview/libs/config/config'
import zIndex from '../uview/libs/config/zIndex'
import $moment from 'dayjs'
const confirm = (content, title, config) => {
let ops = {content}
if (typeof title == 'object') {
ops = {...ops, ...title}
} else ops = {...ops, title: title || "提示"}
return new Promise((resolve, reject) => {
uni.showModal({
...ops, ...config, success: res => {
if (res?.confirm) {
resolve()
} else if (res?.cancel) {
reject()
}
}
})
})
}
/**
* 获取年龄
* @param code
*/
const calcAge = (code) => {
let birthday
if (typeof code == 'string' && code.length == 18) {
birthday = $moment(code.substring(6, 14), 'YYYYMMDD')
} else if (typeof code == 'object') {
birthday = code
}
return Math.ceil($moment().year() - $moment(birthday).year())
}
const u = {toast, $parent, addUnit, guid, config, zIndex, deepClone, throttle, debounce, trim, test, sys}
export default {
dict,
confirm,
calcAge,
u,
dateFormat: (time, format) => {
return $moment(time).format(format || 'YYYY-MM-DD').replace("Invalid Date", "")
}
}

69
src/components/AiAdd.vue Normal file
View File

@@ -0,0 +1,69 @@
<template>
<movable-area class="movableArea">
<movable-view direction="all" x="300" y="500">
<div class="AiAdd" @click.stop="add"></div>
</movable-view>
</movable-area>
</template>
<script>
export default {
name: "AiAdd",
props: {
},
data() {
return {}
},
methods: {
add() {
this.$emit("add")
}
}
}
</script>
<style lang="scss" scoped>
.movableArea {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
uni-movable-view {
pointer-events: auto;
}
}
.AiAdd {
width: 96px;
height: 96px;
background: #1365DD;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
&:before , &:after{
content: "";
background: #FFFFFF;
display: block;
position: absolute;
border-radius: 4px;
}
&:before{
height: 48px;
width: 4px;
}
&:after{
height: 4px;
width: 48px;
}
}
</style>

87
src/components/AiBack.vue Normal file
View File

@@ -0,0 +1,87 @@
<template>
<ai-fixed-btn v-if="!isTopPage||custom">
<div class="AiBack" @click.stop="back">
<img :src="imgHomeUrl + 'back.png'" alt="">
<text>返回</text>
</div>
</ai-fixed-btn>
</template>
<script>
import AiFixedBtn from "./AiFixedBtn";
export default {
name: "AiBack",
components: {AiFixedBtn},
props: {
delta: {
type: Number,
default: 1
},
eventName: {
type: String,
default: ''
},
data: {
type: Object | Boolean,
default: () => {
}
},
custom: Boolean,
visible: Boolean,
},
data() {
return {
isTopPage: false
}
},
methods: {
back() {
if (this.visible)
return this.$parent.$emit(this.eventName, this.data)
if (this.custom) {
this.$emit("back")
} else uni.navigateBack({
delta: this.delta,
success: () => {
if (this.eventName != '') {
uni.$emit(this.eventName, this.data)
}
},
fail: (err) => {
console.error(err)
}
})
}
},
mounted() {
this.isTopPage = window.history.length <= 1
}
}
</script>
<style lang="scss" scoped>
.AiBack {
width: 108px;
height: 108px;
background: #6BA1F9;
box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.12);
border-radius: 50%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
img {
width: 40px;
height: 40px;
}
text {
font-size: 26px;
font-weight: 800;
color: #FFFFFF;
line-height: 40px;
}
}
</style>

115
src/components/AiCard.vue Normal file
View File

@@ -0,0 +1,115 @@
<template>
<section class="AiCard">
<div flex v-if="$slots.custom" class="start">
<div class="fill">
<slot name="custom"/>
</div>
<div v-if="$slots.menu" class="iconfont iconfont-iconMore" @tap.stop="handleMore"/>
</div>
<template v-else>
<u-row>
<div class="content">
<slot/>
</div>
<div btn @tap="$emit('send')">发送</div>
</u-row>
<u-row justify="space-between">
<slot v-if="$slots.title" name="title"/>
<div v-else>{{ cardTitle }}</div>
<div v-if="$slots.menu" class="iconfont iconfont-iconMore" @tap.stop="handleMore"/>
</u-row>
</template>
<div v-if="menu" class="mask" @click="menu=false">
<div class="moreMenu" :style="menuPos">
<slot name="menu"/>
</div>
</div>
</section>
</template>
<script>
export default {
name: "AiCard",
props: {
cardTitle: String
},
data() {
return {
menuPos: {},
menu: false
}
},
methods: {
handleMore({detail}) {
this.menuPos = {
left: detail.x - 10 + 'px',
top: detail.y + 'px'
}
this.menu = !this.menu
}
}
}
</script>
<style lang="scss" scoped>
.AiCard {
width: 100%;
background: #FFFFFF;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
border-radius: 8px;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
color: #999;
font-size: 26px;
flex-shrink: 0;
.content {
background: #F9F9F9;
border-radius: 4px;
min-height: 120px;
flex: 1;
min-width: 0;
margin-bottom: 26px;
padding: 20px;
box-sizing: border-box;
color: #333;
}
.u-row {
flex-wrap: nowrap;
}
div[btn] {
color: #1365DD;
padding: 0 0 0 18px;
cursor: pointer;
}
.iconfont-iconMore {
font-size: 38px;
transform: rotate(90deg);
}
.moreMenu {
position: fixed;
background: #FFFFFF;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
transform: translate(-100%, -100%);
min-width: 100px;
min-height: 100px;
z-index: 9;
}
.mask {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 11;
}
}
</style>

76
src/components/AiCell.vue Normal file
View File

@@ -0,0 +1,76 @@
<template>
<section class="AiCell" :class="{bottomBorder,alignCenter,topLabel}">
<div class="label" :class="{title}">
<slot v-if="$slots.label" name="label"/>
<span v-else>{{ label }}</span>
</div>
<div class="content" :class="{topLabel}">
<slot/>
</div>
</section>
</template>
<script>
export default {
name: "AiCell",
props: {
label: {default: ""},
bottomBorder: Boolean,
topLabel: Boolean,
title: Boolean,
alignCenter: Boolean
}
}
</script>
<style lang="scss" scoped>
.AiCell {
display: flex;
min-height: 72px;
font-size: 30px;
color: #333;
padding: 14px 0;
box-sizing: border-box;
justify-content: space-between;
&.bottomBorder {
border-bottom: 1px solid #eee;
}
&.alignCenter {
align-items: center;
}
&.topLabel {
flex-direction: column;
}
.label {
min-width: 60px;
flex-shrink: 0;
width: auto;
color: #999;
&.title {
color: #333;
font-weight: bold;
font-size: 34px;
}
}
.content {
flex: 1;
min-width: 100px;
min-height: 40px;
max-width: 500px;
text-align: right;
&.topLabel {
text-align: start;
margin-top: 16px;
max-width: 100%;
}
}
}
</style>

60
src/components/AiDate.vue Normal file
View File

@@ -0,0 +1,60 @@
<template>
<section class="AiDate">
<u-calendar v-model="show" @change="handleSelect" :mode="mode"/>
<div flex @click="show=true">
<div v-if="label" v-html="label"/>
<div v-else v-html="placeholder"/>
<i class="iconfont iconfont-iconArrow_Down"/>
</div>
</section>
</template>
<script>
import UCalendar from "../uview/components/u-calendar/u-calendar";
import dayjs from 'dayjs'
export default {
name: "AiDate",
components: {UCalendar},
computed: {
label() {
let arr = (this.selected || this.value)?.toString()?.split(",") || []
arr = arr.map(e => dayjs(e).format("MM-DD").replace("Invalid Date", ''))
return arr.join('至')
}
},
data() {
return {
show: false,
selected: ""
}
},
props: {
value: {default: ""},
placeholder: {default: "请选择"},
mode: {default: "date"},//date 单个日期|range 日期范围
},
methods: {
handleSelect(v) {
if (this.mode == 'date') {
this.selected = v.result
this.$emit('change', v.result)
} else if (this.mode == 'range') {
this.selected = [v.startDate, v.endDate]
this.$emit('change', v)
}
}
}
}
</script>
<style lang="scss" scoped>
.AiDate {
color: #333333;
.iconfont-iconArrow_Down {
margin-left: 4px;
font-size: 32px;
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="emptyWrap">
<img class="emptyImg" src="./static/Empty.png">
<div class="emptyText">{{description}}</div>
</div>
</template>
<script>
export default {
name:"emptyData",
props:{
description:{
default:'暂无相关信息',
type:String
}
}
}
</script>
<style lang="scss" scoped>
.emptyWrap {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.emptyImg{
width: 400rpx;
height: 240rpx;
margin-top: 112px;
}
.emptyText{
font-size:29rpx;
font-family:PingFangSC-Regular,PingFang SC;
font-weight:400;
color:rgba(183,183,183,1);
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,36 @@
<template>
<section class="AiFixedBtn">
<movable-area class="movableArea">
<movable-view direction="all" x="300" y="500" @tap.stop>
<slot/>
</movable-view>
</movable-area>
</section>
</template>
<script>
export default {
name: "AiFixedBtn"
}
</script>
<style lang="scss" scoped>
.AiFixedBtn {
}
.movableArea {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
uni-movable-view {
pointer-events: auto;
width: auto;
height: auto;
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<section class="AiImage">
<div v-if="$slots.default" @tap="prev">
<slot/>
</div>
<u-image v-else :src="src" @tap="prev">
<image v-if="link" class="errorImage" slot="error" :src="$cdn+'link.png'"/>
<image v-else-if="miniapp" class="errorImage" slot="error" :src="$cdn+'miniwxmp.jpg'"/>
<image v-else class="errorImage" slot="error" :src="$cdn+'file.png'"/>
</u-image>
</section>
</template>
<script>
import UImage from "../uview/components/u-image/u-image";
import UModal from "../uview/components/u-modal/u-modal";
import {mapActions} from "vuex";
export default {
name: "AiImage",
components: {UModal, UImage},
data() {
return {
dialog: false
}
},
props: {
src: String,
preview: Boolean,
link: Boolean,
miniapp: Boolean,
file: {
default: () => {
}
}
},
methods: {
...mapActions(['previewFile', 'injectJWeixin']),
prev() {
if (this.preview) {
if (!!this.src) {
uni.previewImage({
current: this.src,
urls: [this.src]
})
} else {
this.previewFile({size: 1, ...this.file})
}
}
},
}
}
</script>
<style lang="scss" scoped>
.AiImage {
::v-deep image {
width: 160px;
height: 160px;
object-fit: cover;
}
::v-deep .u-image__error {
position: relative;
}
.errorImage {
width: 80px;
height: 80px;
}
}
</style>

View File

@@ -0,0 +1,28 @@
<template>
<section class="AiLoading">
<image :src="image"/>
<span>{{ tips }}</span>
</section>
</template>
<script>
export default {
name: "AiLoading",
props: {
tips: {default: "应用加载中"},
image: {default: "https://cdn.cunwuyun.cn/wxAdmin/img/message.png"}
}
}
</script>
<style lang="scss" scoped>
.AiLoading {
font-size: 32px;
color: #666;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>

68
src/components/AiMap.vue Normal file
View File

@@ -0,0 +1,68 @@
<template>
<section class="AiMap">
<div ref="amap" class="map"/>
</section>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
export default {
name: "AiMap",
props: {
plugins: {default: () => ['AMap.DistrictSearch']},
map: Object,
lib: Object
},
data() {
return {
amap: null
}
},
methods: {
initMap() {
let {plugins} = this
AMapLoader.load({
key: '54a02a43d9828a8f9cd4f26fe281e74e',
version: '2.0',
plugins
}).then(AMap => {
this.amap = new AMap.Map(this.$refs.amap, {
resizeEnable: true,
zoom: 14,
})
this.$emit('update:lib', AMap)
this.$emit('update:map', this.amap)
})
},
},
mounted() {
this.initMap()
},
destroyed() {
this.amap?.destroy()
}
}
</script>
<style lang="scss" scoped>
.AiMap {
.map {
height: 100%;
}
::v-deep .amap-logo, ::v-deep .amap-copyright {
display: none !important;
}
::v-deep .amap-icon {
width: 40px !important;
height: 40px !important;
img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<section class="AiResult">
<slot v-if="$slots.default"/>
<template v-else>
<image :src="result.image"/>
<span class="tips">{{ result.tips }}</span>
<slot name="extra" class="extra" v-if="$slots.extra"/>
<div v-if="result.btn" class="btn" @tap="handleTap">{{ result.btn }}</div>
</template>
</section>
</template>
<script>
export default {
name: "AiResult",
props: {
tips: {default: "提交成功!"},
image: {default: "https://cdn.cunwuyun.cn/dvcp/h5/result/success.png"},
btn: {default: ""},
status: {default: "success"},
btnTap: Function
},
computed: {
result() {
let obj = {
image: this.image,
tips: this.tips,
btn: this.btn
}
if (this.status == "error") {
obj.image = this.$cdn + "result/fail.png"
obj.tips = this.tips || "提交失败!"
}
return obj
}
},
methods: {
handleTap() {
this.btnTap && this.btnTap()
}
}
}
</script>
<style lang="scss" scoped>
.AiResult {
padding-top: 96px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: bold;
& > image {
width: 192px;
height: 192px;
}
.tips {
margin: 16px auto 0;
color: #333;
}
.extra {
margin-top: 48px;
}
.btn {
cursor: pointer;
margin-top: 80px;
width: calc(100% - 192px);
height: 88px;
background: #197DF0;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
border-radius: 8px;
color: #FFF;
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,87 @@
<template>
<section class="AiSearchPopup">
<u-popup v-model="show" length="100%" closeable :mode="mode">
<slot v-if="$slots.default"/>
<div class="searchPane" v-else>
<div class="title">{{ title }}</div>
<u-search v-model="search" :placeholder="placeholder" :show-action="false" @search="getList()" :focus="show"/>
<div class="result">
<div class="option" v-for="(op,i) in list" :key="i" @tap="handleSelect(op)">{{ op[ops.label] }}</div>
</div>
</div>
</u-popup>
<div @tap="show=true">
<slot name="btn"/>
</div>
</section>
</template>
<script>
export default {
name: "AiSearchPopup",
props: {
title: {default: "搜索"},
placeholder: {default: "请搜索"},
ops: {default: () => ({label: 'label', search: 'name'})},
url: String,
mode: {default: "right"}
},
data() {
return {
show: false,
search: "",
list: []
}
},
methods: {
getList() {
this.url && this.$instance.post(this.url, null, {
params: {[this.ops.search]: this.search}
}).then(res => {
if (res?.data) {
this.list = res.data
}
})
},
handleSelect(op) {
this.$emit('select', op)
this.show = false
}
}
}
</script>
<style lang="scss" scoped>
.AiSearchPopup {
::v-deep .searchPane {
padding: 0 16px;
display: flex;
flex-direction: column;
height: 100%;
.title {
width: 100%;
height: 100px;
text-align: center;
line-height: 100px;
}
.result {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-bottom: 30px;
}
.option {
display: flex;
align-items: center;
height: 80px;
border-bottom: 1px solid #eee;
font-size: 32px;
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<section class="AiSelect">
<div class="display" v-if="$slots.default" @tap="handleShowOptions">
<slot/>
</div>
<div v-else class="display" @tap="handleShowOptions">
<div class="selectedLabel" v-if="selectedLabel">{{ selectedLabel }}</div>
<i v-else>{{ placeholder }}</i>
<u-icon name="arrow-right" color="#ddd"/>
</div>
<u-select v-model="show" :list="options" :mode="mode" @confirm="handleConfirm"/>
</section>
</template>
<script>
export default {
name: "AiSelect",
props: {
value: String,
placeholder: {default: "请选择"},
list: {default: () => []},
mode: {default: "single-column"},
dict: {default: ""},
disabled: Boolean
},
computed: {
selectedLabel() {
let label = this.options.find(e => e.value == this.value)?.label
return this.selected?.map(e => e.label)?.join(",") || label
},
options() {
return this.dict ? this.$dict.getDict(this.dict).map(e => ({
value: e.dictValue,
label: e.dictName
})) : this.list
}
},
data() {
return {
show: false,
selected: []
}
},
methods: {
handleConfirm(v) {
this.selected = v
this.$emit("data", this.selected)
this.$forceUpdate()
},
handleShowOptions() {
if (!this.disabled) this.show = true
}
}
}
</script>
<style lang="scss" scoped>
.AiSelect {
max-width: 100%;
::v-deep .u-icon {
margin-left: 8px;
}
.display {
display: flex;
align-items: center;
.selectedLabel {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
i {
font-style: normal;
color: $uni-text-color-grey;
}
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<div class="AiSelectEnterprise">
<tree :checkList="checkList" :props="prop" @sendValue="(val)=>checkList = val" :multiple="multiple" :isCheck="true"
:rootId="rootId"/>
<div class="footer">
<scroll-view scroll-x class="scroll" style="width: 100%;">
</scroll-view>
<div class="btn" @click="confirm">确定选择</div>
<AiBack :visible="true" eventName="update:visible" :data="false" @click.native="confirm"></AiBack>
</div>
</div>
</template>
<script>
import tree from "./tree";
import AiBack from "../AiBack";
export default {
name: "AiSelectEnterprise",
components: {tree, AiBack},
props: {
value: {
type: Array,
default: () => []
},
multiple: {
type: Boolean,
default: true
},
rootId: Object,
},
data() {
return {
tree: [],
checkList: this.value,
prop: {
label: 'name',
multiple: this.multiple,
},
map: {},
}
},
created() {
uni.pageScrollTo({
duration: 0,
scrollTop: 0
})
},
methods: {
confirm() {
let filter = []
this.map = {}
this.recursion(this.checkList)
Object.keys(this.map).map(e => filter.push(this.map[e]))
this.$emit("change", filter)
this.$emit('update:visible', false)
},
recursion(arr) {
if (arr?.length) {
arr.map(e => {
if ((e.type == 0 || e.openId) && e.checked && !this.map[e.id]) {
this.map[e.id] = e
this.recursion(e.childrenUser)
}
if (e.childrenDept?.length) {
this.recursion(e.childrenDept)
}
})
}
}
},
}
</script>
<style lang="scss" scoped>
.AiSelectEnterprise {
min-height: 100%;
background-color: #F5F5F5;
position: relative;
.footer {
width: 100%;
display: flex;
align-items: center;
z-index: 10;
background: #F4F8FB;
position: fixed;
left: 0;
bottom: 0;
box-sizing: border-box;
padding: 0 32px;
.scroll {
height: 118px;
::v-deep .uni-scroll-view-content {
display: flex;
align-items: center;
.tag {
width: 236px;
height: 72px;
background: #EAEEF1;
border-radius: 8px;
display: flex;
align-items: center;
margin-right: 16px;
& > img {
width: 48px;
height: 45px;
margin-right: 8px;
flex-shrink: 0;
}
& > label {
width: 148px;
height: 42px;
font-size: 30px;
font-weight: 600;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.btn {
width: 192px;
height: 80px;
background: #1365DD;
border-radius: 4px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #FFFFFF;
margin-left: 8px;
}
}
}
</style>

View File

@@ -0,0 +1,481 @@
<template>
<div class="tree">
<ai-top-fixed>
<div class="top pad">
<u-search v-if="searchIf" placeholder="搜索" @change="confirmSearch" @search="confirmSearch" :clearabled="true"
v-model="keyword" :show-action="false" @clear="clear"></u-search>
<u-tabs :list="list" :current="current" item-width="50%" height="96" bar-width="192"
@change="tabChange"></u-tabs>
</div>
</ai-top-fixed>
<div class="tree-list">
<scroll-view scroll-x class="scroll pad" style="width:100%" :scroll-left="scrollLeft">
<div v-for="(item,index) in parent" class="inline-item" :key="index">
<div class="inline-item" v-if="index==0" @click.stop="backTree(item,-1)">
<text v-if="index==parent.length-1&&!isSear" class="none">可选范围</text>
<text v-else class="active">可选范围</text>
</div>
<div v-if="index==0 && isSear" @click.stop="backTree(item,-2)"
:class="[index==parent.length-1 && isSear] ? 'none inline-item':'active inline-item'">
<span style="margin: 0 8px">/</span>
搜索结果
</div>
<div class="inline-item" @click.stop="backTree(item,index)" v-if="index!=0">
<span style="margin: 0 8px">/</span>
<text v-if="index==parent.length-1" class="none inline-item">
{{item[tag]}}
</text>
<text v-else class="active">
{{item[tag]}}
</text>
</div>
</div>
</scroll-view>
<div class="container-list">
<div class="common" v-for="(item, index) in tree" @click.stop="toNext(item)" :key="index">
<label class="content">
<div class="checkbox" v-if="multiple" @click.stop="checkboxChange(item,index)">
<img :src="$cdn + 'common/xzh.png'" v-if="item.checked" alt="">
<img :src="$cdn + 'common/xzn.png'" v-else alt="">
</div>
<div class="checkbox" v-if="!multiple && (item.type==0 || item.openId)" @click.stop="checkbox(item,index)">
<img :src="$cdn + 'common/xzh.png'" v-if="item.checked" alt="">
<img :src="$cdn + 'common/xzn.png'" v-else alt="">
</div>
<div class="person" v-if="item.type==0">
<u-avatar :src="item.avatar || ($cdn + 'common/xztx.png')" mode="square" :size="74"></u-avatar>
</div>
<u-row justify="between" style="width: 100%;">
<div class="word" v-if="tag=='name'">
<img :src="$cdn + 'common/xzbq.png'" v-if="item.type==1" alt="">
<span class="ellipsis">{{item[tag]}}</span>
</div>
<div class="word" v-else-if="tag=='tagname'">
<template v-if="!item.openId">
<img :src="$cdn + 'common/xzbqbottom.png'" alt="">
<span class="ellipsis">{{item[tag]}}</span>
</template>
<template v-else>
<u-avatar :src="item.avatar || ($cdn + 'common/xztx.png')" mode="square" :size="74"
style="margin: 0 17px;"></u-avatar>
<span class="ellipsis">{{item["name"]}}</span>
</template>
</div>
<div class="right"
v-if="item.type==1 && (item.childrenDept.length || item.childrenUser.length) && tag=='name'"></div>
<div class="right" v-if="tag=='tagname' && !item.openId"></div>
</u-row>
</label>
</div>
</div>
</div>
</div>
</template>
<script>
import AiTopFixed from "../AiTopFixed";
export default {
name: "tree",
components: {AiTopFixed},
props: {
checkList: {
type: Array,
default: () => []
},
searchIf: {
type: Boolean,
default: () => true
},
multiple: {
type: Boolean,
default: true
},
rootId: Object,
},
data() {
return {
isSear: false,
tree: [],
parent: [1],
searchResult: [],
allData: [],
newCheckList: this.checkList,
scrollLeft: Infinity,
keyword: "",
current: 0,
tag: "name",
}
},
methods: {
tabChange(e) {
this.tag = e == 0 ? "name" : "tagname"
this.current = e
this.parent = [1]
// this.newCheckList = []
this.getTree()
},
clear() {
this.keyword = ""
this.tree = this.allData
this.parent = [1]
this.isSear = false
},
checkboxChange(item, index) {
if (item.checked) {
this.$set(this.tree[index], 'checked', false)
this.delChild(item)
for (let index = 0, n = this.newCheckList.length; index < n; index++) {
let temp = this.newCheckList[index];
if (temp.id == item.id) {
this.newCheckList.splice(index, 1)
break
}
}
} else {
(item.type == 0 || item.openId) && this.newCheckList.push(item)
this.$set(this.tree[index], 'checked', true)
this.chooseChild(item)
}
this.$emit('sendValue', this.newCheckList)
},
delUser(id) {
for (let i = 0, len = this.newCheckList.length; i < len; i++) {
if (this.newCheckList[i].id === id) {
return this.newCheckList.splice(i, 1)
}
}
},
chooseChild(arr) {
if (arr.childrenDept?.length) {
for (let i = 0, len = arr.childrenDept.length; i < len; i++) {
let item = arr.childrenDept[i]
item.checked = true
this.newCheckList.push(item)
this.chooseChild(item)
}
}
if (arr.childrenUser?.length) {
for (let i = 0, len = arr.childrenUser.length; i < len; i++) {
let item = arr.childrenUser[i]
item.checked = true
this.newCheckList.push(item)
this.chooseChild(item)
}
}
if (arr.users?.length) {
for (let i = 0, len = arr.users.length; i < len; i++) {
let item = arr.users[i]
item.checked = true
this.newCheckList.push(item)
this.chooseChild(item)
}
}
this.newCheckList = Array.from(new Set(this.newCheckList))
},
delChild(arr) {
if (arr.childrenDept?.length) {
for (let i = 0, len = arr.childrenDept.length; i < len; i++) {
let item = arr.childrenDept[i];
item.checked = false
for (let index = 0, n = this.newCheckList.length; index < n; index++) {
let temp = this.newCheckList[index];
if (temp.id == item.id) {
this.newCheckList.splice(index, 1)
break
}
}
this.delChild(item)
}
}
if (arr.childrenUser?.length) {
for (let i = 0, len = arr.childrenUser.length; i < len; i++) {
let item = arr.childrenUser[i];
item.checked = false
for (let index = 0, n = this.newCheckList.length; index < n; index++) {
let temp = this.newCheckList[index];
if (temp.id == item.id) {
this.newCheckList.splice(index, 1)
break
}
}
this.delChild(item)
}
}
},
//单选
checkbox(item, index) {
let status = !this.tree[index].checked
this.$set(this.tree[index], 'checked', status)
if (this.newCheckList.length <= 0) {
this.newCheckList = [this.tree[index]]
} else if (this.newCheckList.length == 1) {
this.tree.forEach(item => {
if (item.id != this.tree[index].id) {
item.checked = false
}
})
this.newCheckList = []
if (this.tree[index].checked) {
this.newCheckList.push(this.tree[index])
}
}
this.$emit('sendValue', this.newCheckList)
},
toNext(item) {
if (this.tag == "name") {
if (item.type == 1 && (item["childrenDept"].length || item["childrenUser"].length)) {
this.tree = []
if (item["childrenDept"].length) {
this.tree = item["childrenDept"]
}
if (item["childrenUser"].length) {
this.tree = [...this.tree, ...item["childrenUser"]]
}
this.checkIf()
if (this.parent[0].id !== item.id) {
this.parent.push(item)
}
}
} else if (this.tag == "tagname" && !item.openId) {
this.tree = item.users
if (this.parent[0].id !== item.id) {
this.parent.push(item)
}
}
this.$nextTick(() => {
this.scrollLeft += 200
})
},
checkIf() {
for (let i = 0, len = this.tree.length; i < len; i++) {
for (let j = 0, lens = this.newCheckList.length; j < lens; j++) {
if (this.newCheckList[j].id == this.tree[i].id) {
this.$set(this.tree[i], 'checked', true)
break
} else {
this.$set(this.tree[i], 'checked', false)
}
}
}
},
confirmSearch(val) {
this.searchResult = []
this.search(this.tree, val)
this.isSear = true
this.parent.splice(1, Infinity)
this.tree = this.searchResult
if(!val) this.clear()
},
search(data, keyword) {
if (data.length) {
for (let i = 0, len = data.length; i < len; i++) {
if (data[i].name?.indexOf(keyword) != -1) {
this.searchResult.push(data[i])
}
if (data[i]["childrenDept"]?.length || data[i]["childrenUser"]?.length) {
this.search(data[i]["childrenDept"].concat(data[i]["childrenUser"]), keyword)
}
if (data[i]["users"]?.length) {
this.search(data[i]["users"], keyword)
}
}
}
},
backTree(item, index) {
if (index == -1) {
this.tree = this.allData
this.parent.splice(1, Infinity)
this.isSear = false
this.keyword = ""
} else if (index == -2) {
this.tree = this.searchResult
this.parent.splice(1, Infinity)
} else {
if (this.parent.length - index > 2) {
this.parent.forEach((item, i) => {
if (i > index) {
this.parent.splice(i, Infinity)
}
})
} else if (index != this.parent.length - 1) {
this.parent.splice(this.parent.length - 1, 1)
}
this.tree = item["childrenDept"].concat(item["childrenUser"] || [])
}
if (this.multiple) return
this.checkIf()
},
getTree() {
this.$http.post(this.current == 0 ? "/app/wxcp/wxuser/tree" : "/app/wxcp/wxtag/tree", null, {
params: {
rootId: this.rootId
}
}).then(res => {
if (res && res.data) {
let result = this.tag == 'name' ? [res.data] : res.data
this.tree = result
this.allData = result
}
})
},
},
computed: {
list() {
return [
{name: "组织架构"},
{name: "标签"}
]
}
},
created() {
this.getTree()
}
}
</script>
<style lang="scss" scoped>
.tree {
min-height: 100%;
background-color: #F5F5F5;
.top {
background-color: #FFFFFF;
}
.tree-list {
margin-top: 24px;
background-color: #FFFFFF;
.scroll {
white-space: nowrap;
border-bottom: 1px solid #f4f4f4;
.inline-item {
height: 112px;
font-size: 30px;
display: inline-block;
line-height: 112px;
.active {
color: #4297ED !important;
font-weight: 600;
}
.none {
color: #666666;
font-weight: 600;
}
}
}
.container-list {
min-height: 1000px;
overflow-y: scroll;
overflow-x: hidden;
.common {
background-color: #fff;
border-bottom: 1px solid #f4f4f4;
box-sizing: border-box;
padding: 0 30px;
.content {
display: flex;
align-items: center;
height: 100px;
width: 100%;
line-height: 100px;
position: relative;
font-size: 32px;
.right {
width: 16px;
height: 16px;
border-right: 4px solid #CCCCCC;
border-top: 4px solid #CCCCCC;
transform: rotate(45deg);
}
.word {
display: flex;
align-items: center;
& > img {
width: 74px;
height: 74px;
margin: 0 34px;
}
.ellipsis{
width: 450px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.checkbox {
position: relative;
display: flex;
align-items: center;
& > img {
width: 48px;
height: 48px;
border-radius: 50%;
}
.color {
color: #00aaff;
background-color: #00aaff;
}
}
.person {
display: flex;
align-items: center;
color: #f57a00;
font-size: 36px;
text-align: center;
margin: 0 34px;
flex-shrink: 0;
}
}
}
}
}
::v-deep .content {
padding: 0 !important;
}
.pad {
box-sizing: border-box;
padding: 20px 32px 0;
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<section class="AiTabbar">
<div class="tabPane" v-for="(op,i) in tabbars" :key="i"
@click="$emit('update:active',i)">
<img :src="op.icon" alt=""/>
<span :class="{active:i==active}">{{ op.text }}</span>
</div>
</section>
</template>
<script>
export default {
name: "AiTabbar",
props: {
active: {default: 0},
list: {default: () => []},
},
computed: {
tabbars() {
return this.list.map((e, i) => ({
...e,
icon: i == this.active ? e.selectedIconPath : e.iconPath
}))
}
}
}
</script>
<style lang="scss" scoped>
.AiTabbar {
height: 98px;
width: 100%;
position: fixed;
bottom: 0;
background: #FFFFFF;
border-top: 1px solid #ddd;
display: flex;
z-index: 9;
.tabPane {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 22px;
cursor: pointer;
& > img {
height: 44px;
}
& > span {
color: #C4CAD4;
&.active {
color: #3267F0;
}
}
}
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<section class="AiTable">
<u-table color="#333">
<u-tr>
<u-th v-for="(col,i) in colConfigs" :key="i" :width="col.width">{{ col.label }}</u-th>
</u-tr>
<u-tr v-for="(row,j) in data" :key="j">
<u-td v-for="(col,i) in colConfigs" :key="i" :width="col.width">
<slot v-if="col.slot" :name="col.slot"/>
<p v-else-if="col.dict">{{ $dict.getLabel(col.dict, row[col.prop]) }}</p>
<p v-else>{{ row[col.prop] || "-" }}</p>
</u-td>
</u-tr>
</u-table>
</section>
</template>
<script>
import UTable from "../uview/components/u-table/u-table";
import UTd from "../uview/components/u-td/u-td";
import UTh from "../uview/components/u-th/u-th";
import UTr from "../uview/components/u-tr/u-tr";
export default {
name: "AiTable",
components: {UTr, UTh, UTd, UTable},
props: {
data: {default: () => []},
colConfigs: {default: () => []},
}
}
</script>
<style lang="scss" scoped>
.AiTable {
border-radius: 8px;
min-height: 100px;
overflow: hidden;
.u-table, .u-th {
border-color: #D0D4DC !important;
}
.u-th {
background-color: #DFE6F4;
color: #646D7F;
}
.u-tr {
height: 80px;
}
}
</style>

81
src/components/AiTabs.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<section class="AiTabs" :class="{wrap}">
<div class="tabItem" v-for="(op,i) in ops" :key="i"
:class="{active:value==op.value,plain}"
:style="{width:itemWidth}"
@tap="$emit('change',op.value)">
{{ op.name }}
</div>
<div class="end">
<slot name="end"/>
</div>
</section>
</template>
<script>
export default {
name: "AiTabs",
model: {
prop: "value",
event: "change"
},
props: {
value: {default: ""},
ops: {default: () => []},
wrap: Boolean,
plain: Boolean,
itemWidth: String
},
}
</script>
<style lang="scss" scoped>
.AiTabs {
display: flex;
flex: 1;
min-width: 0;
max-height: 240px;
overflow-y: auto;
&.wrap {
flex-wrap: wrap;
}
.tabItem {
flex-shrink: 0;
min-width: 144px;
max-width: 100%;
min-height: 64px;
font-size: 28px;
font-weight: 400;
color: #666;
background: #FFFFFF;
border-radius: 4px;
border: 1px solid #CCC;
text-align: center;
line-height: 64px;
margin-bottom: 16px;
margin-right: 16px;
padding: 0 16px;
box-sizing: border-box;
&.active {
border-color: $uni-color-primary;
color: $uni-color-primary;
&.plain {
color: #fff;
border-color: transparent;
background: $uni-color-primary;
}
}
}
.end {
flex: 1;
min-width: 0;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<section class="AiTextarea" :class="{border}">
<u-input type="textarea" v-bind="$attrs" :value="value" :maxlength="maxlength"
@input="handleInput" :disabled="disabled"/>
<div class="bottomBar">
<div class="leftPane">
<slot name="bar"/>
</div>
<div v-if="!!maxlength">{{ value.length }}/{{ maxlength }}</div>
</div>
</section>
</template>
<script>
import UInput from "../uview/components/u-input/u-input";
export default {
name: "AiTextarea",
components: {UInput},
model: {
prop: "value",
event: "change"
},
props: {
value: {default: ""},
maxlength: {default: 0},
border: Boolean,
disabled: Boolean
},
methods: {
handleInput(v) {
this.$emit('change', v)
}
}
}
</script>
<style lang="scss" scoped>
.AiTextarea {
width: 100%;
position: relative;
&.border {
::v-deep textarea {
border-radius: 4px;
border: 1px solid #e2e1e1;
padding: 16px 16px 36px;
box-sizing: border-box;
}
.bottomBar {
position: absolute;
bottom: 8px;
right: 16px;
}
::v-deep .u-input__right-icon {
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
}
}
.bottomBar {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 36px;
color: #999999;
.leftPane {
display: flex;
& > * + * {
margin-left: 32px;
}
}
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<section class="AiTopFixed" :style="{background}">
<!--占位区-->
<div class="placeholder">
<div v-if="$slots.tabs">
<slot name="tabs"/>
</div>
<div class="content" v-if="$slots.default">
<slot/>
</div>
</div>
<!--悬浮区-->
<div class="fixed" :style="{background}">
<div v-if="$slots.tabs">
<slot name="tabs"/>
</div>
<div class="content" v-if="$slots.default">
<slot/>
</div>
</div>
</section>
</template>
<script>
export default {
name: "AiTopFixed",
props: {
background: {default: "#fff"}
}
}
</script>
<style lang="scss" scoped>
.AiTopFixed {
width: 100%;
& > div {
width: 100%;
}
.fixed {
width: 100%;
top: 0;
position: fixed;
z-index: 9;
}
.placeholder {
visibility: hidden;
opacity: 0;
}
.content {
padding: 20px 32px;
box-sizing: border-box;
}
::v-deep .u-search {
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
margin-bottom: 32px !important;
}
}
</style>

View File

@@ -0,0 +1,229 @@
<template>
<div class="ai-uploader">
<div class="fileList">
<div class="item" v-for="(item, i) in fileList" :key="i">
<template v-if="type == 'image'">
<ai-image :src="item.url" :preview="preview"/>
<div class="info">
<i>{{ item.fileSizeStr }}</i>
</div>
</template>
<template v-else>
<ai-image :preview="preview" :file="item"/>
<div class="info">
<span>{{ item.name }} </span>
<i>{{ item.fileSizeStr }}</i>
</div>
</template>
<template v-if="!disabled">
<div btn @tap="handleReUpload(i)">
重新上传
</div>
<div btn @tap="remove(i)">
删除
</div>
</template>
</div>
<div v-if="!disabled&&(fileList.length == 0 || (multiple && fileList.length < limit))" class="default"
@click="upload">
<i class="iconfont iconfont-iconAdd"/>
<span>{{ placeholder }}</span>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import AiImage from './AiImage'
export default {
name: 'AiUploader',
components: {AiImage},
props: {
limit: {default: 1}, //数量
placeholder: {default: '添加图片'}, // 文字提示
type: {default: 'image'}, // 文件类型image还是file
multiple: {
type: Boolean,
default: false,
},
fileId: String,
mediaId: String,
def: {default: () => []},
action: {default: '/app/wxcp/upload/uploadFile'},
preview: Boolean,
size: {default: 0},
disabled: Boolean
},
computed: {
...mapState(['baseURL', 'token']),
errorImage() {
return this.$cdn + 'file.png'
},
},
watch: {
def: {
handler(v) {
if (!!v?.toString() && v?.url) {
if (this.multiple) {
this.fileList = v
} else {
this.fileList = [v]
}
}
},
immediate: true,
},
},
data() {
return {
fileList: [],
}
},
methods: {
remove(index) {
this.fileList.splice(index, 1)
this.$emit('list', this.fileList)
},
upload(wait) {
let params = {
count: this.limit,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
let count = this.fileList?.length + (res.tempFiles?.length || res.tempFile ? 1 : 0)
if (count > this.limit && this.limit !== 1) {
return this.$u.toast(`不能超过${this.limit}`)
}
if (res.tempFiles) {
res.tempFiles?.map((item) => {
this.uploadFile(item)
})
} else if (res?.tempFile) {
this.uploadFile(res.tempFile)
}
},
}
typeof wait == 'function' && wait()
if (this.type == 'image') {
uni.chooseImage(params)
} else if (this.type == 'video') {
uni.chooseVideo(params)
} else {
uni.chooseFile(params)
}
},
uploadFile(img) {
if (this.size > 0 && img.size > this.size) {
return this.$u.toast(`不能超过${Math.ceil(this.size / 1024 / 1024)}MB`)
}
uni.showLoading({title: '上传中'})
let formData = new FormData()
formData.append('file', img)
this.$http
.post(this.action, formData, {
params: {type: this.type},
})
.then((res) => {
uni.hideLoading()
if (res?.data) {
this.$emit('data', res.data)
this.$u.toast('上传成功!')
if (this.action == '/app/wxcp/upload/uploadFile') {
this.$emit('update:mediaId', res.data?.media?.mediaId)
this.$emit('update:fileId', res.data.file.id)
this.fileList.push(res.data.file)
} else if (this.action == '/admin/file/add2') {
let info = res.data
this.$emit('update:fileId', info?.id)
this.fileList.push(res.data)
}
this.$emit("update:def", this.fileList)
this.$emit("list", this.fileList)
} else {
this.$u.toast(res.msg)
}
})
.catch(() => uni.hideLoading())
},
handleReUpload(i) {
this.upload(() => this.remove(i))
},
},
}
</script>
<style lang="scss" scoped>
.ai-uploader {
width: 100%;
line-height: normal;
margin-bottom: 16px;
.fileList {
.item {
display: flex;
align-items: center;
margin-bottom: 10px;
image {
width: 160px;
height: 160px;
}
i {
font-style: normal;
color: #9b9b9b;
}
.info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
& > span {
overflow: hidden;
text-overflow: ellipsis;
}
}
div[btn] {
color: $uni-color-primary;
}
div:nth-child(4) {
color: #f72c27;
}
& > * + * {
margin-left: 20px;
}
}
.default {
width: 240px;
height: 240px;
box-sizing: border-box;
border-radius: 8px;
background: #f3f4f7;
color: #89b;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.iconfont-iconAdd {
font-size: 64px;
}
span {
display: block;
text-align: center;
font-size: 28px;
}
}
}
}
</style>

185
src/components/AiVideo.vue Normal file
View File

@@ -0,0 +1,185 @@
<template>
<view class="imt-audio">
<view class="audio-wrapper">
<view class="audio-number">{{format(current)}}</view>
<slider class="audio-slider" :activeColor="color" block-size="16" :value="current" :max="duration || 10" @changing="seek=true,current=$event.detail.value" @change="audio.seek($event.detail.value)"></slider>
<view class="audio-number">{{format(duration)}}</view>
</view>
<view class="audio-control-wrapper" :style="{color}">
<image
class="audio-control audio-control-switch"
@click="audio.paused?play():audio.pause()"
:src="paused ? playImg : stopImg" />
<p>{{ paused ? '点击播放' : '点击停止播放' }}</p>
</view>
</view>
</template>
<script>
import stopImg from '../pages/resourcesManage/img/stop-img.png'
import playImg from '../pages/resourcesManage/img/play-icon.png'
export default {
data() {
return {
audio: uni.createInnerAudioContext(),
current: 0, //当前进度(s)
duration: 0, //总时长(s)
paused: true, //是否处于暂停状态
loading: false, //是否处于读取状态
seek: false,
stopImg,
playImg
}
},
props: {
src: String, //音频链接
autoplay: Boolean, //是否自动播放
continue: Boolean, //播放完成后是否继续播放下一首,需定义@next事件
control: {
type: Boolean,
default: true
}, //是否需要上一曲/下一曲按钮
color: {
type: String,
default: '#007BFF'
} //主色调
},
methods: {
//返回prev事件
prev() {
this.$emit('prev')
},
//返回next事件
next() {
this.$emit('next')
},
//格式化时长
format(num) {
return '0'.repeat(2 - String(Math.floor(num / 60)).length) + Math.floor(num / 60) + ':' + '0'.repeat(2 - String(Math.floor(num % 60)).length) + Math.floor(num % 60)
},
//点击播放按钮
play() {
this.audio.play()
this.loading = true
}
},
created() {
if (this.src) {
this.audio.src = this.src
this.autoplay && this.play()
}
this.audio.obeyMuteSwitch = false
//音频进度更新事件
this.audio.onTimeUpdate(() => {
if (!this.seek) {
this.current = this.audio.currentTime
}
if (!this.duration) {
this.duration = this.audio.duration
}
})
//音频播放事件
this.audio.onPlay(() => {
this.paused = false
this.loading = false
})
//音频暂停事件
this.audio.onPause(() => {
this.paused = true
})
//音频结束事件
this.audio.onEnded(() => {
if (this.continue) {
this.next()
} else {
this.paused = true
this.current = 0
}
})
//音频完成更改进度事件
this.audio.onSeeked(() => {
this.seek = false
})
},
beforeDestroy(){
this.audio.destroy()
},
watch: {
src(src, old) {
this.audio.src = src
this.current = 0
this.duration = 0
if (old || this.autoplay) {
this.play()
}
}
}
}
</script>
<style>
.imt-audio {
background: #fff;
border-radius: 20upx;
}
.audio-wrapper {
display: flex;
align-items: center;
}
.audio-number {
width: 120upx;
font-size: 24upx;
line-height: 1;
color: #999999;
text-align: center;
}
.audio-slider {
flex: 1;
margin: 0;
}
.audio-control-wrapper {
margin-top: 40upx;
text-align: center;
}
.audio-control-wrapper p {
color: #999999;
font-size: 26rpx;
}
.audio-control-wrapper image {
width: 128rpx;
height: 128rpx;
}
.audio-control {
font-size: 32upx;
line-height: 1;
border-radius: 50%;
}
.audio-control-next {
transform: rotate(180deg);
}
.audio-control-switch {
font-size: 40upx;
margin: 0 100upx;
}
.audioLoading {
animation: loading 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
@keyframes loading {
to {
transform: rotate(360deg);
}
}
</style>

29
src/components/VDrag.vue Normal file
View File

@@ -0,0 +1,29 @@
<template>
<section class="VDrag">
<vuedraggable v-bind="$attrs" @change="handleChange">
<slot/>
</vuedraggable>
</section>
</template>
<script>
import vuedraggable from 'vuedraggable'
export default {
name: "VDrag",
components: {vuedraggable},
data: () => ({
moveEvt: null
}),
methods: {
handleChange(moved) {
this.$emit('move', moved)
}
}
}
</script>
<style lang="scss" scoped>
.VDrag {
}
</style>

39
src/main.js Normal file
View File

@@ -0,0 +1,39 @@
import Vue from 'vue';
import App from './App';
import store from './store';
import axios from './common/axios';
import utils from './common/util';
import dayjs from 'dayjs';
import mixin from './uview/libs/mixin/mixin';
Vue.config.productionTip = false;
Vue.prototype.$store = store;
//初始化接口工具类
axios.defaults.baseURL = store.state.baseURL;
Vue.prototype.$http = axios;
Vue.prototype.$cdn = 'https://cdn.cunwuyun.cn/dvcp/h5/';
Vue.prototype.imgHomeUrl = 'https://cdn.cunwuyun.cn/dvcp/h5/home/';
Vue.prototype.imgOtherUrl = 'https://cdn.cunwuyun.cn/dvcp/h5/other/';
Vue.prototype.$formatName = (name) => {
if (name == undefined) {
return;
}
return name.substr(name.length - 2, name.length > 2 ? name.length - 1 : name.length);
};
Object.keys(utils).map((e) => (Vue.prototype['$' + e] = utils[e]));
let relativeTime = require('dayjs/plugin/relativeTime');
require('dayjs/locale/zh-cn');
let dayjs_plugin_duration = require('dayjs/plugin/duration');
dayjs.extend(dayjs_plugin_duration);
dayjs.extend(relativeTime);
Vue.prototype.$dayjs = dayjs;
Vue.mixin(mixin);
App.mpType = 'app';
process.env.NODE_ENV == 'development' && new VConsole();
const app = new Vue({
store,
...App
});
app.$mount();

37
src/manifest.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "dv_cp_weixin",
"description": "企业微信应用",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": true,
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "演示定位能力"
}
}
},
"h5": {
"title": "数字乡村",
"template": "template.h5.html",
"router": {
"mode": "history",
"base": ""
},
"devServer": {
"disableHostCheck": true,
"port": "10323"
},
"optimization": {
"preload": true,
"treeShaking": {
"enable": true
}
}
}
}

411
src/pages.json Normal file
View File

@@ -0,0 +1,411 @@
{
"easycom": {
"^u-(.*)": "@/uview/components/u-$1/u-$1.vue"
},
"pages": [
{
"path": "pages/loading",
"style": {
"navigationBarTitleText": "欢迎使用村微..."
}
},
{
"path": "pages/app",
"style": {
"navigationBarTitleText": "应用"
}
},
{
"path": "pages/login"
},
{
"path": "pages/notification/notification",
"style": {
"navigationBarTitleText": "通知公告"
}
},
{
"path": "pages/notification/components/read",
"style": {
"navigationBarTitleText": "接收情况"
}
},
{
"path": "pages/notification/components/detail",
"style": {
"navigationBarTitleText": "公告详情"
}
},
{
"path": "pages/notification/components/add",
"style": {
"navigationBarTitleText": "新增公告"
}
},
{
"path": "pages/workTask/workTask",
"style": {
"navigationBarTitleText": "工作任务"
}
},
{
"path": "pages/workTask/components/create",
"style": {
"navigationBarTitleText": "创建任务"
}
},
{
"path": "pages/workTask/components/detail",
"style": {
"navigationBarTitleText": "任务详情"
}
},
{
"path": "pages/workTask/components/subDetail",
"style": {
"navigationBarTitleText": "任务详情"
}
},
{
"path": "pages/workTask/components/finishDetail",
"style": {
"navigationBarTitleText": "完成详情"
}
},
{
"path": "pages/workTask/components/finish",
"style": {
"navigationBarTitleText": "完成任务"
}
},
{
"path": "pages/documentFlow/documentFlow",
"style": {
"navigationBarTitleText": "公文流转"
}
},
{
"path": "pages/documentFlow/components/detail",
"style": {
"navigationBarTitleText": "公文详情"
}
},
{
"path": "pages/documentFlow/components/approval",
"style": {
"navigationBarTitleText": "批示"
}
},
{
"path": "pages/meetingNotice/meetingNotice",
"style": {
"navigationBarTitleText": "会议通知"
}
},
{
"path": "pages/meetingNotice/components/addMeeting",
"style": {
"navigationBarTitleText": "发起会议"
}
},
{
"path": "pages/meetingNotice/components/belongToMe",
"style": {
"navigationBarTitleText": "我发起的"
}
},
{
"path": "pages/meetingNotice/components/detail",
"style": {
"navigationBarTitleText": "会议详情"
}
},
{
"path": "pages/meetingNotice/components/meetingList"
},
{
"path": "pages/askForm/askForm"
},
{
"path": "pages/askForm/index",
"style": {
"navigationBarTitleText": "问卷表单"
}
},
{
"path": "pages/askForm/formSetting",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "pages/askForm/filedConfig",
"style": {
"navigationBarTitleText": "组件设置"
}
},
{
"path": "pages/askForm/previewForm",
"style": {
"navigationBarTitleText": "表单预览"
}
},
{
"path": "pages/askForm/addForm",
"style": {
"navigationBarTitleText": "新建调查表单"
}
},
{
"path": "pages/resident/resident"
},
{
"path": "pages/resident/comp",
"style": {
"navigationBarTitleText": "居民管理"
}
},
{
"path": "pages/resident/groupResident"
},
{
"path": "pages/whereabouts/whereabouts"
},
{
"path": "pages/interview/interview",
"style": {
"navigationBarTitleText": "调查走访"
}
},
{
"path": "pages/interview/detail",
"style": {
"navigationBarTitleText": "新增走访"
}
},
{
"path": "pages/quickReply/quickReply"
},
{
"path": "pages/quickReply/typeManage"
},
{
"path": "pages/quickReply/replyDetail"
},
{
"path": "pages/supermarket/supermarket",
"style": {
"navigationBarTitleText": "信用好超市"
}
},
{
"path": "pages/supermarket/balance",
"style": {
"navigationBarTitleText": "结算"
}
},
{
"path": "pages/supermarket/search",
"style": {
"navigationBarTitleText": "搜索"
}
},
{
"path": "pages/supermarket/components/resultPage/resultPage",
"style": {
"navigationBarTitleText": "结算提交"
}
},
{
"path": "pages/casuallyask/casuallyask",
"style": {
"navigationBarTitleText": "随心问"
}
},
{
"path": "pages/casuallyask/casuallyaskDetail",
"style": {
"navigationBarTitleText": "随心问(详情页)"
}
},
{
"path": "pages/casuallyask/closemsg",
"style": {
"navigationBarTitleText": "关闭留言"
}
},
{
"path": "pages/casuallyask/truemsg",
"style": {
"navigationBarTitleText": "提交留言"
}
},
{
"path": "pages/casuallyask/reply",
"style": {
"navigationBarTitleText": "发表回复"
}
},
{
"path": "pages/workonline/workonline",
"style": {
"navigationBarTitleText": "网上办事"
}
},
{
"path": "pages/workonline/detail",
"style": {
"navigationBarTitleText": "进度详情"
}
},
{
"path": "pages/workonline/approvalopinion",
"style": {
"navigationBarTitleText": "进度详情"
}
},
{
"path": "pages/workonline/truemsg",
"style": {
"navigationBarTitleText": "进度详情"
}
},
{
"path": "pages/snapshot/snapshot",
"style": {
"navigationBarTitleText": "随手拍"
}
},
{
"path": "pages/snapshot/snapshotDetail",
"style": {
"navigationBarTitleText": "随手拍详情"
}
},
{
"path": "pages/snapshot/handleResult",
"style": {
"navigationBarTitleText": "处理结果"
}
},
{
"path": "pages/snapshot/components/handlePage/handlePage",
"style": {
"navigationBarTitleText": "处理结果"
}
},
{
"path": "pages/villageQRCode/villageQRCode",
"style": {
"navigationBarTitleText": "一村一码"
}
},
{
"path": "pages/guardianship/guardianship",
"style": {
"navigationBarTitleText": "智慧监护"
}
},
{
"path": "pages/guardianship/userDetail",
"style": {
"navigationBarTitleText": "个人详情"
}
},
{
"path": "pages/guardianship/historyList",
"style": {
"navigationBarTitleText": "历史记录"
}
},
{
"path": "pages/guardianship/warningDetail",
"style": {
"navigationBarTitleText": "预警"
}
},
{
"path": "pages/bigHorn/bigHorn",
"style": {
"navigationBarTitleText": "广播通知"
}
},
{
"path": "pages/bigHorn/playList",
"style": {
"navigationBarTitleText": "播放记录"
}
},
{
"path": "pages/bigHorn/onlineList",
"style": {
"navigationBarTitleText": "在线设备"
}
},
{
"path": "pages/bigHorn/onlinePlayList",
"style": {
"navigationBarTitleText": "在播设备"
}
},
{
"path": "pages/bigHorn/addPlay",
"style": {
"navigationBarTitleText": "音频播放"
}
},
{
"path": "pages/bigHorn/selectMp3",
"style": {
"navigationBarTitleText": "选择内容"
}
},
{
"path": "pages/bigHorn/selectEquipment",
"style": {
"navigationBarTitleText": "选择设备"
}
},
{
"path": "pages/resourcesManage/resourcesManage",
"style": {
"navigationBarTitleText": "媒资管理"
}
},
{
"path": "pages/resourcesManage/addPlay",
"style": {
"navigationBarTitleText": "媒资管理"
}
},
{
"path": "pages/resourcesManage/talking",
"style": {
"navigationBarTitleText": "实时喊话"
}
},
{
"path": "pages/resourcesManage/recording",
"style": {
"navigationBarTitleText": "音频录制"
}
},
{
"path": "pages/videoSurveillance/videoSurveillance",
"style": {
"navigationBarTitleText": "视频监控"
}
},
{
"path": "pages/videoSurveillance/monitorDetail",
"style": {
"navigationBarTitleText": "实时监控",
"pageOrientation": "landscape"
}
}
],
"globalStyle": {
"pageOrientation": "auto",
"navigationStyle": "custom"
}
}

174
src/pages/app.vue Normal file
View File

@@ -0,0 +1,174 @@
<template>
<section class="app">
<u-swiper :list="list" height="240" bg-color="#408CFF"></u-swiper>
<label class="useful-app">常用应用</label>
<u-grid
:col="o.length"
:border="false"
v-for="(o, i) in gridGroup"
:key="i"
>
<u-grid-item v-for="(i, j) in o" :key="j" @click="linkTo(i)">
<u-icon :name="i.icon" :size="100"></u-icon>
<view class="grid-text">{{ i.label }}</view>
</u-grid-item>
</u-grid>
<label class="useful-app notice">通知公告</label>
<section class="list">
<div class="item" v-for="(item, index) in dataList" :key="index">
<label class="title hidden">{{ item.title }}</label>
<div class="content hidden">{{ item.content }}</div>
<div class="date">{{ item.createTime }}</div>
</div>
<u-loadmore :status="status" />
</section>
</section>
</template>
<script>
export default {
name: 'app',
data() {
return {
list: [this.imgHomeUrl + 'banner.png'],
gridGroup: [
[
{
icon: `${this.imgHomeUrl}icon1.png`,
label: '随心问',
link: '/pages/casuallyask/casuallyask'
},
{
icon: `${this.imgHomeUrl}icon2.png`,
label: '随手拍',
link: '/pages/snapshot/snapshot'
},
{
icon: `${this.imgHomeUrl}icon3.png`,
label: '网上办事',
link: '/pages/workonline/workonline'
},
{ icon: `${this.imgHomeUrl}icon4.png`, label: '调查走访' }
],
[
{
icon: `${this.imgHomeUrl}icon5.png`,
label: '信用好超市',
link: '/pages/supermarket/supermarket'
},
{ icon: `${this.imgHomeUrl}icon6.png`, label: '中心工作' },
{ icon: `${this.imgHomeUrl}icon7.png`, label: '会务通知' },
{ icon: `${this.imgHomeUrl}icon8.png`, label: '工作去向' },
{ icon: `${this.imgHomeUrl}icon8.png`, label: '大喇叭' }
]
],
status: 'nomore',
dataList: [],
current: 0
}
},
onLoad() {
this.getList()
},
methods: {
getList() {
if (this.loadingStatus == 'nomore') return
this.$http
.post(`/app/appworkcenternotice/list`, null, {
params: {
withoutToken: true,
current: this.current + 1,
size: 10
}
})
.then(res => {
if (res && res.data) {
this.dataList = res.data.records
}
})
},
linkTo(val) {
if (val.link) {
uni.navigateTo({ url: val.link })
}
}
},
onReachBottom() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.app {
min-height: 100%;
background-color: #ffffff;
box-sizing: border-box;
padding: 32px;
position: relative;
.grid-text {
font-size: 28px;
color: #333333;
line-height: 46px;
}
.useful-app {
margin-top: 80px;
display: inherit;
font-size: 36px;
font-weight: bold;
color: #333333;
line-height: 50px;
}
.notice {
margin-top: 56px;
}
.list {
margin-top: 38px;
.item {
height: 192px;
background: #f7f9ff;
border-radius: 8px;
box-sizing: border-box;
padding: 20px;
margin-bottom: 32px;
.title {
font-size: 32px;
font-weight: 800;
color: #343d65;
}
.content {
font-size: 28px;
color: #333333;
font-weight: 500;
margin: 20px 0 8px 0;
}
.date {
font-size: 28px;
color: #666666;
}
}
}
.hidden {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
::v-deep .u-indicator-item-round {
background-color: #d8dde6;
}
::v-deep .u-indicator-item-round-active {
background-color: #408cff !important;
}
}
</style>

View File

@@ -0,0 +1,700 @@
<template>
<div class="add-form" v-if="pageShow">
<div class="header-pic">
<image v-if="form.headPicture" :src="form.headPicture" />
<span @click="upload">更换图片</span>
</div>
<div class="form-info">
<h2>文本选项</h2>
<div class="form-info__wrapper">
<textarea class="title" placeholder="请输入标题 (必填)" :maxlength="30" :auto-height="true" v-model="form.title"></textarea>
<u-input class="content" :clearable="false" type="textarea" v-model="form.tableExplain" placeholder="请输入表单描述 (选填)" :height="80" :auto-height="true" :maxlength="255"></u-input>
</div>
</div>
<draggable
class="components-list"
v-model="targetList"
:animation="340"
scroll
element="div"
:options="{
animation: 340,
handle: '.components-item__title'
}"
draggable=".components-item"
:sort="true">
<div class="components-item" v-for="(item, index) in targetList" :key="index" @click="toFiledSetting(item, index)">
<div class="components-item__title">
<div class="components-item__title--left">
<em :style="{opacity: item.required ? 1 : 0}">*</em>
<i>{{ index + 1 }}.</i>
<h2>{{ item.label }}</h2>
</div>
<image :src="`${$cdn}askform/sc1.png`" @click.stop="removeComponent(index)" @touchstart.stop="removeComponent(index)" />
</div>
<div class="components-item__filed">
<template v-if="(item.type === 'radio')">
<u-radio-group v-model="item.value" wrap>
<u-radio disabled :name="field.label" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-radio>
</u-radio-group>
</template>
<template v-if="(item.type === 'checkbox')">
<u-checkbox-group v-model="item.value" wrap>
<u-checkbox disabled :name="field.label" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-checkbox>
</u-checkbox-group>
</template>
<template v-if="(item.type === 'select')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
<u-icon name="arrow-down" color="#DEDFDF" />
</div>
</template>
<template v-if="(item.type === 'upload')">
<div class="components-item__select components-item__textarea components-item__upload">
<image :src="`${$cdn}askform/upload.png`" />
<span>选择图片2M以内</span>
</div>
</template>
<template v-if="(item.type === 'input')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
</div>
</template>
<template v-if="(item.type === 'textarea')">
<div class="components-item__select components-item__textarea">
<span>{{ item.placeholder }}</span>
</div>
</template>
</div>
</div>
</draggable>
<div class="add-form__btn" @click="isShow = true">
<image :src="`${$cdn}askform/add.png`" />
<span>添加问题</span>
</div>
<div class="add-form__footer">
<div>
<span @click="toPreview">预览</span>
<span @click="toSetting">设置</span>
</div>
<div @click="onConfirm">立即发布</div>
</div>
<u-popup v-model="isShow" :closeable="false" mode="bottom">
<div class="add-popup">
<div class="add-popup__title">
<h2>添加问题</h2>
<image :src="`${$cdn}askform/zk.png`" mode="aspectFit" @click="isShow = false" />
</div>
<div class="add-popup__list">
<span @click="toFiledSetting('radio')">单选题</span>
<span @click="toFiledSetting('checkbox')">多选题</span>
<span @click="toFiledSetting('select')">单下拉框</span>
<span @click="toFiledSetting('input')">单行填空</span>
<span @click="toFiledSetting('textarea')">多行填空</span>
<span @click="toFiledSetting('upload')">上传图片</span>
</div>
</div>
</u-popup>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
import draggable from 'vuedraggable'
export default {
data () {
return {
pageShow: false,
form: {
tableExplain: '详细描述',
title: '问卷调查',
isShowheadPicture: true,
isShowTableExplain: true,
isShowBtn: true,
headPicture: '',
commitType: '1',
periodValidityType: '0',
actionNotice: '1',
dynamicNotice: '1',
periodValidityEndTime: '',
shareStatus: '0',
count: 0,
wechatId: '0',
type: 0,
buttonExplain: '提交',
tips: true
},
templateType: 0,
targetList: [],
isShow: false,
type: 0,
id: '',
isQuote: false,
touchStart: 0,
formConfig: {}
}
},
components: {
AiBack,
draggable
},
onLoad (query) {
this.type = Number(query.type)
if (query.isQuote) {
this.isQuote = true
}
if (query.id) {
this.id = query.id
this.getInfo(query.id)
} else {
this.pageShow = true
}
this.init()
uni.$on('setting', res => {
this.form = {
...this.form,
...res
}
this.formConfig = res
})
uni.$on('filedConfig', res => {
if (res.index === '-1') {
this.targetList.push(res.config)
} else {
this.$set(this.targetList, [res.index], res.config)
}
})
},
methods: {
toSetting () {
uni.navigateTo({
url: `/pages/askForm/formSetting?id=${this.id}&formConfig=${JSON.stringify(this.formConfig)}`
})
},
removeComponent (index) {
this.targetList.splice(index, 1)
},
toPreview () {
uni.navigateTo({
url: `/pages/askForm/previewForm?targetList=${JSON.stringify(this.targetList)}&form=${JSON.stringify(this.form)}`
})
},
upload() {
let params = {
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
let count = this.fileList?.length + (res.tempFiles?.length || res.tempFile ? 1 : 0)
if (count > 1) {
return this.$u.toast(`不能超过1个`)
}
if (res.tempFiles) {
res.tempFiles.map((item) => {
this.uploadFile(item)
})
} else if (res?.tempFile) {
this.uploadFile(res.tempFile)
}
}
}
uni.chooseImage(params)
},
uploadFile (img) {
uni.showLoading({title: '上传中'})
let formData = new FormData()
formData.append('file', img)
this.$http.post('/admin/file/add2', formData).then((res) => {
uni.hideLoading()
if (res?.data) {
this.$u.toast('上传成功!')
this.form.headPicture = res.data.url
}
}).catch(res => {
this.$u.toast(res)
uni.hideLoading()
})
},
onConfirm () {
for (let item of this.targetList) {
if (item.isShowPoints) {
if (item.pointType === '0') {
if (!item.answer || JSON.stringify(item.answer) === '[]') {
return this.$u.toast(`请输入${item.label}正确答案`)
}
if (!item.points) {
return this.$u.toast(`请输入${item.label}的分值`)
}
}
if (item.pointType === '1') {
for (let option of item.options) {
if (!option.point) {
return this.$u.toast(`请输入${item.label}${option.label}的分值`)
}
}
}
if (item.pointType === '2') {
for (let option of item.options) {
if (!option.point) {
return this.$u.toast(`请输入${item.label}${option.label}的分值`)
}
}
if (!item.points) {
return this.$u.toast(`请输入${item.label}全部答对分值`)
}
}
}
}
const fields = this.targetList.map(item => {
return {
fieldType: item.type,
fieldName: item.label,
fieldInfo: JSON.stringify(item)
}
})
this.$http.post(`/app/appquestionnairetemplate/addOrUpdate`, {
...this.form,
fields,
status: 1,
id: this.isQuote ? '' : this.id,
headPicture: this.form.headPicture,
type: this.type,
templateType: 0
}).then(res => {
if (res.code == 0) {
this.$u.toast('提交成功')
setTimeout(() => {
uni.$emit('reload')
uni.navigateBack({
delta: 1
})
}, 600)
}
}).catch(e => {
this.$u.toast(e)
})
},
getInfo (id) {
uni.showLoading()
this.$http.post(`/app/appquestionnairetemplate/queryDetailById?id=${id}`).then(res => {
if (res.code == 0) {
this.form = {
...res.data,
headPicture: res.data.headPicture
}
this.type = res.data.type
this.targetList = res.data.fields.map(item => {
return JSON.parse(item.fieldInfo)
})
this.pageShow = true
} else {
this.$u.toast(res.msg)
}
uni.hideLoading()
}).catch(() => {
uni.hideLoading()
})
},
toFiledSetting (type, index) {
if (index != undefined) {
uni.navigateTo({
url: `/pages/askForm/filedConfig?config=${JSON.stringify(type)}&index=${index}`
})
return false
}
this.isShow = false
uni.navigateTo({
url: `/pages/askForm/filedConfig?type=${type}`
})
},
init () {
if (this.type == 0) {
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/interview.png'
}
if (this.type == 1) {
this.form.title = '考试测评'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/exam.png'
}
if (this.type == 2) {
this.form.title = '报名登记'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/apply.png'
}
if (this.type == 3) {
this.form.title = '满意调查'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/satisfaction.png'
}
if (this.type == 4) {
this.form.title = '投票评选'
this.form.headPicture = 'https://cdn.cunwuyun.cn/dvcp/h5/form/vote.png'
}
}
}
}
</script>
<style lang="scss" scoped>
.add-form {
min-height: 100vh;
padding-bottom: 140px;
box-sizing: border-box;
background: #F3F6F9;
* {
box-sizing: border-box;
}
::v-deep .u-drawer-bottom {
background-color: transparent;
}
.components-list {
padding: 0 20px;
.components-item {
margin-top: 24px;
padding: 32px;
box-shadow: 0 4px 8px 4px rgba(233, 233, 233, 0.39);
border-radius: 8px;
overflow: hidden;
border: 1px solid #EEEFF0;
background: #fff;
::v-deep .u-radio, ::v-deep .u-checkbox {
position: relative;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.u-checkbox__icon-wrap, .u-radio__icon-wrap {
position: absolute;
top: 4rpx;
}
.u-radio__label, .u-checkbox__label {
display: flex;
align-items: center;
margin-right: 0;
margin-left: 40px;
text-align: justify;
span {
line-height: 1.5;
}
image {
width: 100px;
height: 100px;
margin: 0 10px;
}
}
}
span {
flex: 1;
color: #666;
font-size: 26px;
}
.components-item__select {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 80px;
margin-bottom: 8px;
padding: 0 26px;
border: 1px solid #DEDFDF;
&.components-item__textarea {
align-items: flex-start;
height: 160px;
padding-top: 20px;
image {
width: 46px;
height: 34px;
margin-right: 16px;
}
span {
color: #666;
font-size: 26px;
}
}
&.components-item__upload {
justify-content: center;
align-items: center;
}
}
.components-item__title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
em {
margin-right: 4px;
font-style: normal;
color: rgb(226, 33, 32);;
}
image {
position: relative;
flex-shrink: 1;
right: -20px;
width: 32px;
height: 32px;
box-sizing: content-box;
padding: 30px 20px 30px 20px;
}
div {
display: flex;
align-items: baseline;
max-width: 550px;
color: #333333;
font-size: 32px;
i {
font-style: normal;
}
h2 {
font-weight: 600;
font-size: 32px;
}
}
}
}
}
.add-popup {
height: 440px;
border-radius: 20px 20px 0 0;
background: #fff;
.add-popup__title {
display: flex;
position: relative;
align-items: center;
justify-content: center;
height: 96px;
border-bottom: 1px solid #E4E5E6;
h2 {
color: #333333;
font-size: 32px;
font-weight: 600;
}
image {
position: absolute;
right: 32px;
top: 50%;
width: 30px;
height: 20px;
transform: translateY(-50%);
}
}
.add-popup__list {
display: flex;
flex-wrap: wrap;
padding: 0 34px;
span {
width: calc((100% - 64px) / 3);
height: 78px;
line-height: 78px;
margin-top: 32px;
margin-right: 32px;
text-align: center;
color: #333333;
font-size: 28px;
border-radius: 8px;
border: 1px solid #E4E5E6;
&:nth-of-type(3n) {
margin-right: 0;
}
}
}
}
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
.add-form__btn {
display: flex;
align-items: center;
justify-content: center;
width: 214px;
height: 66px;
line-height: 66px;
margin: 64px auto 0;
background: #FFFFFF;
border-radius: 34px;
&:active {
opacity: 0.8;
}
image {
width: 28px;
height: 28px;
}
span {
margin-left: 16px;
color: #4392E6;
font-size: 28px;
}
}
* {
box-sizing: border-box;
}
.form-info {
padding: 0 20px;
& > h2 {
height: 76px;
line-height: 76px;
color: #999999;
font-weight: normal;
font-size: 28px;
}
.form-info__wrapper {
padding: 0 18px;
background: #fff;
.title {
width: 100%;
padding: 22px 0;
font-size: 36px;
border-bottom: 1px solid #F1F2F3;
}
.content {
padding: 30px 0!important;
font-size: 28px;
::v-deep textarea {
color: #333;
font-size: 28px!important;
}
}
}
}
.header-pic {
position: relative;
font-size: 0;
span {
position: absolute;
bottom: 16px;
right: 16px;
z-index: 1;
width: 148px;
height: 56px;
line-height: 56px;
text-align: center;
color: #fff;
font-size: 26px;
background: rgba(0, 0, 0, 0.16);
border-radius: 28px;
}
image {
width: 100%;
height: 320px;
}
}
::v-deep .u-radio, ::v-deep .u-checkbox {
align-items: baseline;
}
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<section class="askForm">
<template v-if="showDetail&&!isManager">
<form-detail/>
</template>
<template v-else-if="isManager">
<form-list ref="FormList"/>
</template>
<ai-loading v-else :tips="errMsg"/>
</section>
</template>
<script>
import AiLoading from "../../components/AiLoading";
import {mapState} from "vuex";
import FormDetail from "./formDetail";
import FormList from "./formList";
export default {
name: "askForm",
components: {FormList, FormDetail, AiLoading},
computed: {
...mapState(['openUser', 'user']),
showDetail() {
return !!this.$route.query?.id
},
isManager() {
let {hash, query: {preview}} = this.$route
if (preview) return false
else if (hash == "#dev") return true
else return hash != '#form' && !!this.user.id
}
},
data() {
return {
errMsg: "加载表单中..."
}
},
onReachBottom() {
this.$refs?.FormList?.reachBottom()
}
}
</script>
<style lang="scss" scoped>
.askForm {
position: absolute;
width: 100%;
height: 100%;
background: #fff;
}
</style>

View File

@@ -0,0 +1,182 @@
<template>
<div class="form">
<div class="form-list">
<div
class="form-list__item"
@click="toAdd(`/pages/askForm/addForm?type=${index}`)"
:style="{'background-image': `url(${$cdn}askform/${index + 1}.png)`}"
v-for="(item, index) in itemList"
:key="index">
<h2>{{ item.name }}</h2>
<div>立即创建</div>
</div>
</div>
<div class="template" v-if="list.length">
<h2>共享模板</h2>
<div class="template-list">
<div class="template-item" v-for="(item, index) in list" :key="index" hover-class="bg-hover" @click="quote(item.id)">
<image :src="`${$cdn}askform/6.png`" />
<h2>{{ item.title }}</h2>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'addList',
label: '新建项目',
data () {
return {
itemList: [{
name: '调查问卷'
}, {
name: '考试测评'
}, {
name: '报名登记'
}, {
name: '满意调查'
}, {
name: '投票评选'
}],
list: []
}
},
mounted () {
this.getList()
},
methods: {
toAdd (url) {
uni.navigateTo({
url
})
},
quote (id) {
uni.navigateTo({
url: `/pages/askForm/addForm?isQuote=1&id=${id}`
})
},
getList () {
this.$http.post(`/app/appquestionnairetemplate/list`, null, {
params: {
current: 1,
templateType: 1,
size: 10000
}
}).then(res => {
if (res.code == 0) {
this.list = res.data.records
}
})
}
}
}
</script>
<style lang="scss" scoped>
.template {
margin: 32px 32px 0;
background: #fff;
border-radius: 8px;
overflow: hidden;
& > h2 {
height: 88px;
line-height: 88px;
padding: 0 24px;
color: #333333;
font-size: 30px;
}
.template-item {
display: flex;
align-items: center;
height: 104px;
padding: 0 24px;
border-bottom: 1px solid #D8DDE6;
&:active {
background-color: #eee;
}
&:last-child {
border: none;
}
image {
width: 36px;
height: 42px;
}
i {
font-size: 30px;
color: #E1E2E3;
}
h2 {
flex: 1;
padding: 0 18px;
color: #333333;
font-size: 28px;
font-weight: normal;
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
}
}
.form-list {
display: flex;
flex-wrap: wrap;
padding: 0 32px 0;
div {
box-sizing: border-box;
}
.form-list__item {
width: calc(50% - 13px);
height: 216px;
margin: 32px 24px 0 0;
padding: 40px 20px 52px;
background-color: #FFFFFF;
border-radius: 8px;
background-size: 100% 100%;
&:active {
background-color: #eee;
}
&:nth-of-type(2n) {
margin-right: 0;
}
div {
width: 148px;
height: 48px;
line-height: 48px;
text-align: center;
color: #fff;
font-size: 28px;
background: #6BA1F9;
border-radius: 24px;
}
h2 {
margin-bottom: 32px;
padding-left: 10px;
color: #333333;
font-weight: 700;
font-size: 32px;
}
}
}
</style>

View File

@@ -0,0 +1,497 @@
<template>
<div class="form">
<ai-top-fixed>
<u-search placeholder="请输入标题" :show-action="false" search-icon-color="#ccc" v-model="search.title"
@search="isMore = false, search.current = 1, getList()"/>
</ai-top-fixed>
<div class="form-list">
<div class="form-item" v-for="(item, index) in list" :key="index"
@click="info = item, id = item.id, isShow = true">
<div class="form-item__top">
<div class="form-item__left">
<h2>{{ item.title }}</h2>
<div class="form-item__left--info">
<span>{{ item.createUserName }}</span>
<span>{{ item.createUnitName }}</span>
<span>{{ item.createTime.substr(0, item.createTime.length - 3) }}</span>
<span>{{ $dict.getLabel('questionnaireType', item.type) }}</span>
</div>
</div>
<div class="form-item__right">
<h2>{{ item.dataCount }}</h2>
<span>答卷数量</span>
</div>
</div>
<div class="form-item__bottom form-item__bottom--active">
<i :style="{background: $dict.getColor('questionnaireStatus', item.status)}"></i>
<span>{{ $dict.getLabel('questionnaireStatus', item.status) }}</span>
</div>
</div>
<ai-empty v-if="!list.length && isMore"></ai-empty>
</div>
<u-popup v-model="isShow" :closeable="false" mode="bottom" :z-index="11">
<div class="popup">
<h2>{{ info.title }}</h2>
<div class="operate-list">
<div class="operate-item" @click="toEdit">
<div>
<image :src="`${$cdn}askform/bj.png`"/>
</div>
<h3>编辑</h3>
</div>
<div class="operate-item" @click="linkTo('/pages/askForm/askForm?preview=1&id=' + id)">
<div>
<image :src="`${$cdn}askform/yl.png`"/>
</div>
<h3>预览</h3>
</div>
<div class="operate-item" @click="publish" v-if="info.status !== '1'">
<div>
<image :src="`${$cdn}askform/fb.png`"/>
</div>
<h3>发布</h3>
</div>
<div class="operate-item" @click="toStop" v-if="info.status === '1'">
<div>
<image :src="`${$cdn}askform/stop.png`"/>
</div>
<h3>停止</h3>
</div>
<div class="operate-item" @click="showShare">
<div>
<image :src="`${$cdn}askform/fx.png`"/>
</div>
<h3>分享</h3>
</div>
<div class="operate-item" @click="share(id)">
<div>
<image :src="`${$cdn}askform/mb.png`"/>
</div>
<h3>共享为模板</h3>
</div>
<div class="operate-item" @click="remove(id)">
<div>
<image :src="`${$cdn}askform/sc.png`"/>
</div>
<h3>删除</h3>
</div>
</div>
<div class="popup-btn" @click="isShow = false">关闭</div>
</div>
</u-popup>
</div>
</template>
<script>
import AiTopFixed from '@/components/AiTopFixed'
import AiEmpty from '@/components/AiEmpty/AiEmpty'
import {mapActions} from 'vuex'
export default {
name: 'formList',
label: '表单列表',
data() {
return {
search: {
current: 1,
templateType: 0,
title: ''
},
id: '',
info: {},
isMore: false,
list: [],
isShow: false
}
},
components: {
AiEmpty,
AiTopFixed
},
mounted() {
this.injectJWeixin(['sendChatMessage', 'selectEnterpriseContact'])
this.$dict.load(['questionnaireStatus', 'questionnaireType', 'questionnaireFieldType']).then(() => {
this.getList()
})
},
methods: {
...mapActions(['injectJWeixin', 'wxInvoke']),
linkTo(url) {
this.isShow = false
uni.navigateTo({
url
})
},
toStop() {
this.$http.post(`/app/appquestionnairetemplate/stopRelease?id=${this.info.id}`).then(res => {
if (res.code === 0) {
this.$u.toast('停止成功')
this.search.current = 1
this.isShow = false
this.isMore = false
this.getList()
}
})
},
showShare() {
if (this.info.status !== '1') {
this.isShow = false
return this.$u.toast(`该表单${this.info.status === '0' ? '未发布' : '已截止'},无法分享!`)
}
uni.showActionSheet({
itemList: ['分享', '微信分享', '获取链接'],
success: data => {
this.$http.post(`/app/appquestionnairetemplate/queryQrCode?id=${this.info.id}`).then(res => {
if (res.code == 0) {
if (data.tapIndex === 2) {
this.copy(res.data.linkUrl)
this.isShow = false
}
if (data.tapIndex === 0 || data.tapIndex === 1) {
this.injectJWeixin(['shareAppMessage', 'shareWechatMessage']).then(() => {
if (data.tapIndex === 0) {
this.wxInvoke(['shareAppMessage', {
title: this.info.title,
desc: this.info.tableExplain,
link: res.data.linkUrl,
imgUrl: this.info.headPicture
}, () => {
this.isShow = false
}])
} else {
this.wxInvoke(['shareWechatMessage', {
title: this.info.title,
desc: this.info.tableExplain,
link: res.data.linkUrl,
imgUrl: this.info.headPicture
}, () => {
this.isShow = false
}])
}
})
}
}
})
}
})
},
copy(link) {
let oInput = document.createElement('input')
oInput.value = link
document.body.appendChild(oInput)
oInput.select()
document.execCommand('Copy')
this.$u.toast('已复制')
oInput.remove()
},
publish() {
if (this.info.status === '1') {
return this.$u.toast('该表单已发布')
}
this.linkTo(`/pages/askForm/formSetting?id=${this.info.id}&type=edit`)
this.isShow = false
},
toEdit() {
if (this.info.dataCount !== 0) {
return this.$u.toast('该表单已有数据,无法编辑!')
}
uni.navigateTo({
url: `/pages/askForm/addForm?id=${this.id}`
})
this.isShow = false
},
share(id) {
this.$http.post(`/app/appquestionnairetemplate/share?id=${id}`).then(res => {
if (res.code === 0) {
this.$confirm('调查表单共享成功,其他成员可在新建项目时直接使用!', '', {
showCancel: false
})
this.isShow = false
}
})
},
remove(id) {
if (this.info.dataCount !== 0) {
return this.$u.toast('该表单已有数据,无法删除!')
}
this.$confirm('确定删除该数据?').then(() => {
this.$http.post(`/app/appquestionnairetemplate/delete?id=${id}`).then(res => {
if (res.code == 0) {
this.$u.toast('删除成功')
this.isShow = false
this.search.current = 1
this.isMore = false
this.getList()
}
})
}).catch(() => {
})
},
reload() {
this.isMore = false
this.search.current = 1
this.getList()
},
getList() {
if (this.isMore) return
this.$http.post(`/app/appquestionnairetemplate/list`, null, {
params: {
...this.search,
size: 10
}
}).then(res => {
if (res.code == 0) {
if (this.search.current > 1) {
this.list = [...this.list, ...res.data.records]
} else {
this.list = res.data.records
}
uni.hideLoading()
if (res.data.records.length < 10) {
this.isMore = true
return false
}
this.search.current = this.search.current + 1
} else {
uni.hideLoading()
}
}).catch(() => {
uni.hideLoading()
})
}
}
}
</script>
<style lang="scss" scoped>
.form {
::v-deep .u-search {
margin-bottom: 0 !important;
}
.popup {
background: #F7F7F7;
& > h2 {
height: 72px;
line-height: 72px;
padding: 0 20px;
color: #999999;
font-size: 22px;
text-align: center;
border-bottom: 2px solid #D7D8DA;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.popup-btn {
height: 96px;
line-height: 96px;
text-align: center;
color: #333333;
font-size: 30px;
background: #fff;
&:active {
background: #eee;
}
}
.operate-list {
display: flex;
flex-wrap: wrap;
text-align: center;
padding-bottom: 26px;
.operate-item {
width: 25%;
font-size: 0;
margin-top: 28px;
&:active {
opacity: 0.7;
}
h3 {
margin-top: 20px;
color: #666666;
font-size: 26px;
font-weight: normal;
}
}
image {
width: 100px;
height: 100px;
border-radius: 16px;
background: #fff;
}
}
}
div {
box-sizing: border-box;
}
.form-item {
margin: 24px 25px 0;
padding: 32px;
background: #FFFFFF;
border-radius: 16px;
.form-item__bottom {
display: flex;
align-items: center;
margin-top: 28px;
color: #999999;
i {
width: 12px;
height: 12px;
margin-right: 6px;
border-radius: 50%;
background: #999999;
}
span {
font-size: 26px;
}
&.form-item__bottom--active i {
background: #3CB300;
}
}
.form-item__top {
display: flex;
justify-content: space-between;
align-items: center;
.form-item__right {
text-align: center;
h2 {
line-height: 40px;
margin-bottom: 16px;
font-size: 32px;
font-weight: 600;
color: #1EA0FA;
}
span {
color: #999999;
font-size: 22px;
}
}
.form-item__left {
flex: 1;
max-width: 80%;
position: relative;
&::after {
position: absolute;
right: -15px;
top: 50%;
width: 2px;
height: 96px;
background: #F5F5F5;
content: '';
transform: translateY(-50%);
}
h2 {
line-height: 44px;
margin-bottom: 16px;
color: #333;
font-size: 32px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.form-item__left--info {
display: flex;
align-items: center;
flex-wrap: wrap;
color: #999;
font-size: 20px;
span {
position: relative;
margin-right: 24px;
&::after {
position: absolute;
right: -12px;
top: 50%;
width: 2px;
height: 20px;
background: #D1D2D5;
content: '';
transform: translateY(-50%);
}
&:last-child {
margin-right: 0;
&::after {
display: none;
}
}
}
}
}
}
}
.type-0 {
background: #2266FF !important;
}
.type-1 {
background: rgba(34, 170, 153, 1) !important;
}
.type-2 {
background: rgba(248, 180, 37, 1) !important;
}
.type-3 {
background: rgba(102, 119, 187, 1) !important;
}
.type-4 {
background: rgba(236, 68, 97, 1) !important;
}
}
</style>

163
src/pages/askForm/config.js Normal file
View File

@@ -0,0 +1,163 @@
export const components = [
{
type: 'radio',
label: '单选',
fixedLabel: '单选',
value: '',
points: '',
icon: 'iconradio',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: '',
pointType: '0',
pointDict: [
{
dictName: '此题有唯一答案和分值',
dictValue: '0'
},
{
dictName: '每个选项都有对应分值',
dictValue: '1'
}
],
options: [
{
label: '选项1',
value: '',
point: '',
img: []
},
{
label: '选项2',
value: '',
point: '',
img: []
}
],
title: ''
},
{
type: 'checkbox',
label: '多选',
fixedLabel: '多选',
points: '',
icon: 'iconcheck_box',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: [],
value: [],
pointType: '0',
pointDict: [
{
dictName: '此题有唯一答案和分值',
dictValue: '0'
},
{
dictName: '每个选项都有对应分值',
dictValue: '1'
},
{
dictName: '答对几项得几分,答错不得分',
dictValue: '2'
}
],
options: [
{
label: '选项1',
value: '',
point: '',
img: [],
checked: false
},
{
label: '选项2',
point: '',
value: '',
img: [],
checked: false
}
],
title: ''
},
{
type: 'select',
label: '单下拉框',
fixedLabel: '单下拉框',
value: '',
points: '',
icon: 'iconSelect',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: '',
pointType: '0',
placeholder: '请选择',
pointDict: [
{
dictName: '此题有唯一答案和分值',
dictValue: '0'
},
{
dictName: '每个选项都有对应分值',
dictValue: '1'
}
],
options: [
{
label: '选项1',
value: '',
point: '',
img: []
},
{
label: '选项2',
value: '',
point: '',
img: []
}
],
title: ''
},
{
type: 'input',
label: '单行填空',
fixedLabel: '单行填空',
value: '',
pointType: '0',
icon: 'icontext_box',
isShowPoints: false,
points: '',
required: true,
hasAnswer: false,
placeholder: '请输入...',
answer: ''
},
{
type: 'textarea',
label: '多行填空',
fixedLabel: '多行填空',
pointType: '0',
icon: 'icontext_area',
points: '',
isShowPoints: false,
required: true,
hasAnswer: false,
answer: '',
placeholder: '请输入...',
value: ''
},
{
type: 'upload',
label: '上传图片',
fixedLabel: '上传图片',
value: '',
icon: 'iconpic',
isShowPoints: false,
points: '',
required: true,
hasAnswer: false,
answer: ''
}
];

View File

@@ -0,0 +1,535 @@
<template>
<div class="form-config">
<div class="config-group">
<div class="config-item">
<u-input class="form-maintitle" :maxlength="200" v-model="config.label" :placeholder="`请输入${config.fixedLabel}标题 ${config.required ? '(必填)' : ''}`" placeholder-style="color: #999999; font-weight: 600" />
</div>
<div class="config-item__select--wrapper" v-if="['radio', 'select', 'checkbox'].includes(config.type)">
<div class="config-item__select" v-for="(item, index) in config.options" :key="index">
<image class="config-icon" :src="`${$cdn}askform/del.png`" @click="removeOptions(index)" />
<div class="config-item__upload" v-if="config.type !== 'select'" @click="upload(index)">
<u-icon color="#8c9dc3" name="plus" v-if="!item.img.length"></u-icon>
<image v-else :src="item.img[0].url" />
</div>
<div class="textarea">
<textarea type="textarea" placeholder-style="color: #CDCDCF" :auto-height="true" v-model="item.label" :maxlength="100" placeholder="请输入选项" />
</div>
</div>
<div class="config-item__select config-item__select--add" @click="addOptions">
<image class="config-icon" :src="`${$cdn}askform/zj.png`" />
<span>添加选项</span>
</div>
</div>
</div>
<div class="config-group">
<div class="config-item" v-if="!['radio', 'upload', 'checkbox', 'select'].includes(config.type)">
<div class="config-item__left">
<span>说明文字</span>
</div>
<div class="config-item__right">
<u-input v-model="config.placeholder" placeholder="请输入说明文字" input-align="right" />
</div>
</div>
<div class="config-item">
<div class="config-item__left">
<span>是否必填</span>
</div>
<div class="config-item__right">
<u-switch v-model="config.required" active-value="1" inactive-value="0" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
<div class="config-item" v-if="!['upload'].includes(config.type)">
<div class="config-item__left">
<span>答案与分值</span>
</div>
<div class="config-item__right">
<u-switch v-model="config.isShowPoints" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
<div class="config-item" v-if="['input', 'textarea'].includes(config.type) && config.isShowPoints">
<div class="config-item__left">
<span>正确答案</span>
</div>
<div class="config-item__right">
<u-input v-model="config.answer" placeholder="请输入正确答案" input-align="right" />
</div>
</div>
<div class="config-item" v-if="['radio', 'select'].includes(config.type) && config.isShowPoints && config.pointType === '0'">
<div class="config-item__left">
<span>正确答案</span>
</div>
<div class="config-item__right config-item__text" @click="isShowAnswer = true">
<span>{{ config.answer ? config.answer : '请选择正确答案' }}</span>
<u-icon name="arrow-down-fill" color="#c0c4cc" size="24"></u-icon>
</div>
</div>
<div class="config-item config-item__checkbox" v-if="config.isShowPoints && ['radio', 'select', 'checkbox'].includes(config.type)">
<div class="config-item__left">
<span>计分方式</span>
</div>
<div class="config-item__right" @click="isShowType = true">
<span>{{ pointTypeName ? pointTypeName : '请选择' }}</span>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
<div class="config-item config-item__answer config-item__checkbox" v-if="['checkbox'].includes(config.type) && config.isShowPoints && config.pointType === '0'">
<div class="config-item__left">
<span>正确答案</span>
</div>
<div class="config-item__right">
<u-checkbox-group wrap @change="onCheckboxChange">
<u-checkbox v-model="field.checked" :name="field.label" v-if="field.label" v-for="(field, i) in config.options" :key="i">
<span>{{ field.label }}</span>
</u-checkbox>
</u-checkbox-group>
</div>
</div>
<div class="config-item" v-if="config.isShowPoints && config.pointType === '0'">
<div class="config-item__left">
<span>本题分值</span>
</div>
<div class="config-item__right">
<u-input v-model="config.points" type="number" placeholder="请输入本题分值" input-align="right" />
</div>
</div>
<div v-if="config.isShowPoints && config.pointType === '1'">
<div class="config-item" v-for="(item, index) in config.options" :key="index">
<div class="config-item__left">
<span>{{ item.label }}</span>
</div>
<div class="config-item__right">
<u-input v-model="item.point" placeholder="请输入分值" input-align="right" />
</div>
</div>
</div>
<div class="config-item config-item__point" v-if="config.isShowPoints && config.pointType === '2'">
<u-checkbox-group wrap @change="onCheckboxChange">
<u-checkbox v-model="field.checked" :name="field.label" v-if="field.label" v-for="(field, i) in config.options" :key="i">
<span>{{ field.label }}</span>
<u-input v-model="field.point" type="number" placeholder="请输入分值" input-align="right" />
</u-checkbox>
</u-checkbox-group>
</div>
<div class="config-item" v-if="config.isShowPoints && config.pointType === '2'">
<div class="config-item__left" style="padding-left: 20px">
<span>全部答对</span>
</div>
<div class="config-item__right">
<u-input v-model="config.points" type="number" placeholder="请输入全部答对分值" input-align="right" />
</div>
</div>
</div>
<u-select :list="config.options" :default-value="defaultAnswer" value-name="value" label-name="label" v-model="isShowAnswer" @confirm="answerChange"></u-select>
<u-select :list="config.pointDict" :default-value="defaultType" value-name="dictValue" label-name="dictName" v-model="isShowType" @confirm="pointTypeChange"></u-select>
<div class="add-form__footer">
<div @click="back">
<span>取消</span>
</div>
<div @click="confirm">确定</div>
</div>
</div>
</template>
<script>
import { components } from './config'
export default {
data () {
return {
index: '',
isShowType: false,
isShowAnswer: false,
config: {
}
}
},
onLoad (query) {
if (query.type) {
this.config = JSON.parse(JSON.stringify(components.filter(v => v.type === query.type)[0]))
} else {
this.config = JSON.parse(query.config)
this.index = query.index
}
},
computed: {
pointTypeName () {
if (!this.config.pointDict) return ''
return this.config.pointDict.filter(v => v.dictValue === this.config.pointType)[0].dictName
},
defaultType () {
if (!this.config.pointType) return [0]
return [Number(this.config.pointType)]
},
defaultAnswer () {
if (!this.config.answer) return [0]
let index = 0
if (this.config.answer) {
this.config.options.forEach((v, i) => {
if (v.label === this.config.answer) {
index = i
}
})
}
return [index]
}
},
methods: {
answerChange (e) {
this.config.answer = e[0].label
},
pointTypeChange (e) {
console.log(e)
this.config.pointType = e[0].value
},
onCheckboxChange (e) {
this.config.answer = e
},
upload (index) {
let params = {
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
let count = this.fileList?.length + (res.tempFiles?.length || res.tempFile ? 1 : 0)
if (count > 1) {
return this.$u.toast(`不能超过1个`)
}
if (res.tempFiles) {
res.tempFiles.map((item) => {
this.uploadFile(item, index)
})
}
}
}
uni.chooseImage(params)
},
uploadFile (img, index) {
uni.showLoading({title: '上传中'})
let formData = new FormData()
formData.append('file', img)
this.$http.post('/admin/file/add2', formData).then((res) => {
uni.hideLoading()
if (res?.data) {
this.$u.toast('上传成功!')
this.$set(this.config.options[index], 'img', [res.data])
}
}).catch(res => {
this.$u.toast(res)
uni.hideLoading()
})
},
removeOptions (index) {
const len = this.config.options.length
const label = this.config.options[index].label
if (len === 2) {
return this.$u.toast('选项不能少于2个')
}
if (this.config.type === 'checkbox') {
const answerIndex = this.config.answer.indexOf(label)
if (answerIndex > -1) {
this.config.answer.splice(answerIndex, 1)
}
} else {
if (label === this.config.answer) {
this.config.answer = ''
}
}
this.config.options.splice(index, 1)
},
back () {
uni.navigateBack({
delta: 1
})
},
confirm () {
uni.$emit('filedConfig', {
config: this.config,
index: this.index === '' ? '-1' : this.index
})
uni.navigateBack({
delta: 1
})
},
addOptions () {
const len = this.config.options.length
let label = `选项${len + 1}`
const index= this.config.options.findIndex(v => label === v.label)
if (index > -1) {
label = `新选项${len + 1}`
}
this.config.options.push({
label: label,
value: '',
point: '',
img: '',
checked: false
})
},
}
}
</script>
<style lang="scss" scoped>
.form-config {
box-sizing: border-box;
padding-bottom: 130px;
.form-maintitle {
::v-deep .uni-input-input {
font-size: 36px;
font-weight: 600;
}
}
.config-item__select--wrapper {
.config-item__select {
display: flex;
align-items: center;
::v-deep .u-input__input {
height: 100%;
}
.textarea {
flex: 1;
display: flex;
align-items: center;
min-height: 104px;
padding: 16px 0;
font-size: 28px;
border-bottom: 1px solid #dfe8f8;
}
textarea {
width: 100%;
font-size: 28px;
}
.config-icon {
width: 36px;
height: 36px;
margin-right: 12px;
}
}
.config-item__select--add {
height: 120px;
.config-icon {
margin-right: 18px;
}
span {
color: #1D74F4;
font-size: 30px;
}
}
}
.config-group {
margin-bottom: 32px;
padding: 0 32px;
background: #fff;
.config-item__upload {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
margin-right: 20px;
border: 1px solid rgb(208, 212, 220);
background-color: #fbfdff;
image {
width: 100%;
height: 100%;
}
}
.config-item {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 100px;
padding: 16px 0;
border-bottom: 1px solid #dfe8f8;
&:last-child {
border: none;
}
::v-deep .u-radio__label, ::v-deep .u-checkbox__label {
margin-right: 0;
font-size: 28px;
span {
max-width: 400rpx;
line-height: 1.2;
}
}
.config-item__left {
max-width: 400px;
}
.config-item__right {
flex: 1;
text-align: right;
padding-left: 30px;
&.config-item__text {
display: flex;
align-items: center;
justify-content: flex-end;
span {
max-width: 400px;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: keep-all;
}
}
.text {
}
}
&.config-item__answer {
display: block;
padding: 20px 0;
.config-item__left {
margin-bottom: 32rpx;
span {
word-break: break-all;
color: #333;
font-size: 30px;
}
}
.config-item__right {
width: 100%;
padding-left: 0;
text-align: left;
}
}
}
::v-deep .u-checkbox {
align-items: baseline;
}
.config-item__checkbox {
height: auto;
padding: 14px 0;
::v-deep .u-checkbox, ::v-deep .u-radio {
// justify-content: flex-end;
}
}
.config-item__point {
height: auto;
padding: 0;
::v-deep .u-checkbox {
justify-content: inherit;
min-height: 100px;
padding: 14px 0;
border-bottom: 1px solid #eee;
&:last-child {
border: none;
}
.u-checkbox__label {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
margin-right: 0;
.u-input {
flex: 1;
max-width: 400px;
}
}
}
}
}
* {
box-sizing: border-box;
}
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:first-child:active {
background: #eee;
}
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,405 @@
<template>
<section class="formDetail">
<ai-result v-if="result.tips" v-bind="result">
<template v-if="isExam" #extra>
<div flex class="scorePane">
<div>成绩</div>
<div class="fill"><em v-html="score"/> </div>
</div>
</template>
</ai-result>
<template v-else-if="form.id">
<image v-if="form.headPicture" class="headPicture" :src="form.headPicture"/>
<b class="title">{{ form.title || "标题" }}</b>
<div class="tableExplain">{{ form.tableExplain }}</div>
<u-form class="content" label-position="top">
<u-form-item class="item" v-for="(op,i) in fields" :key="i" :label="(i+1)+'.'+op.fieldName"
:required="op.fieldInfo.required==1">
<template v-if="op.fieldType=='input'">
<input v-model="op.fieldValue" :placeholder="op.fieldInfo.placeholder" :disabled="isResult"/>
</template>
<template v-else-if="op.fieldType=='textarea'">
<textarea v-model="op.fieldValue" :disabled="isResult" :placeholder="op.fieldInfo.placeholder"/>
</template>
<template v-else-if="op.fieldType=='upload'">
<ai-uploader @list="v=>op.fieldValue=v.map(e=>e.url)" :def="op.fieldValue" :disabled="isResult"
preview/>
</template>
<u-row v-else-if="op.fieldType=='radio'">
<radio-group @change="({detail})=>op.fieldValue=detail.value">
<div class="option" flex v-for="option in op.fieldInfo.options" :key="option.label">
<radio :value="option.label" :disabled="isResult" :checked="op.fieldValue==option.label"/>
<ai-image v-if="option.img" :src="option.img" preview/>
<div class="label fill">{{ option.label }}</div>
</div>
</radio-group>
</u-row>
<u-row v-else-if="op.fieldType=='checkbox'">
<checkbox-group @change="({detail})=>op.fieldValue=detail.value">
<div class="option" flex v-for="option in op.fieldInfo.options" :key="option.label">
<checkbox :value="option.label" :disabled="isResult"
:checked="option.checked"/>
<ai-image v-if="option.img" :src="option.img" preview/>
<div class="label fill">{{ option.label }}</div>
</div>
</checkbox-group>
</u-row>
<template v-else-if="op.fieldType=='select'">
<ai-select @data="v=>op.fieldValue=v.map(e=>e.value)" :list="op.fieldInfo.options" :disabled="isResult">
<div class="option" flex>
<div class="label fill" v-if="op.fieldValue">{{ op.fieldValue.toString() }}</div>
<i class="fill" v-else>请选择</i>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</ai-select>
</template>
</u-form-item>
</u-form>
<div class="bottom" v-if="!(isPreview||isResult)">
<div class="bottomBtn" @tap="handleSubmit">提交</div>
</div>
</template>
<ai-loading v-else tips="调查问卷加载中..."/>
</section>
</template>
<script>
import UForm from "../../uview/components/u-form/u-form";
import UFormItem from "../../uview/components/u-form-item/u-form-item";
import {mapActions, mapState} from "vuex";
import UInput from "../../uview/components/u-input/u-input";
import AiTextarea from "../../components/AiTextarea";
import AiUploader from "../../components/AiUploader";
import AiSelect from "../../components/AiSelect";
import URadio from "../../uview/components/u-radio/u-radio";
import AiLoading from "../../components/AiLoading";
import AiResult from "../../components/AiResult";
import AiImage from "../../components/AiImage";
import AiBack from "../../components/AiBack";
export default {
name: "formDetail",
components: {
AiBack,
AiImage,
AiResult,
AiLoading,
URadio,
AiSelect,
AiUploader,
AiTextarea,
UInput,
UFormItem,
UForm
},
computed: {
...mapState(['openUser', 'token']),
isExam() {
return this.form?.type == 1
},
isPreview() {
return !!this.$route.query?.preview
},
isResult() {
return !!this.$route.query?.result
}
},
data() {
return {
form: {},
fields: [],
checkUser: false,
result: {},
score: 0
}
},
watch: {
form: {
deep: true,
handler(v) {
this.fields = v?.fields?.map(e => {
let fieldInfo = JSON.parse(e.fieldInfo)
fieldInfo?.options?.map(op => {
op.img = op?.img?.[0]?.url
op.checked = !!e.fieldValue?.split(",")?.includes(op.label)
})
if (e.fieldType == 'select') {
fieldInfo.options = fieldInfo.options.map(e => ({...e, value: e.label, label: e.label}))
}
return {...e, fieldInfo}
}) || []
}
}
},
methods: {
...mapActions(['getUserInfo']),
getForm() {
let {id} = this.$route.query
this.$http.post("/app/appquestionnairetemplate/queryDetailById", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.form = res.data
}
})
},
validateForm() {
return !this.fields.some(e => {
if (!!e?.fieldInfo?.required && !e.fieldValue?.toString()) {
this.$u.toast(e.fieldName + "不能为空!")
return true
}
})
},
handleSubmit() {
if (this.validateForm()) {
this.handleScore()
let {avatar: avatarUrl, openId, name: nickName, type: userType, unionId, corpName} = this.openUser
this.$http.post("/app/appquestionnairetemplate/commit", {
fields: this.fields.map(e => ({
...e,
fieldInfo: JSON.stringify(e.fieldInfo),
fieldValue: e.fieldValue?.toString()
})),
avatarUrl, openId, nickName, userType, unionId, corpName,
totalScore: this.score,
questionnaireTemplateId: this.$route.query.id
}).then(res => {
if (res?.code == 0) {
this.result = {
tips: "提交成功!感谢参与",
}
}
})
}
},
handleScore() {
this.score = 0
this.isExam && this.fields.map(field => {
let item = field?.fieldInfo || {}
let current = 0
const calcScore = point => (current += (Number(point) || 0))
if (item?.pointType == 0) {//此题有唯一答案和分值
field.fieldValue?.toString() == item.answer?.toString() && calcScore(item?.points)
} else if (item?.pointType == 1) {//每个选项都有对应分值
item?.options?.map(op => {
if (typeof field.fieldValue == "object") {
if (field.fieldValue?.includes(op.label)) {
calcScore(op.point)
}
} else {
op.label == field.fieldValue && calcScore(op.point)
}
})
} else if (item?.pointType == 2) {//答对几项得几分,答错不得分
item?.options?.some(op => {
if (typeof field.fieldValue == "object") {
if (field.fieldValue?.includes(op.label)) {
if (item.answer?.includes(op.label)) calcScore(op.point)
else return current = 0
}
} else {
op.label == field.fieldValue && calcScore(op.point)
}
})
}
this.score += current
//打印每题打分
if (!!field.fieldValue) {
const typeResult = (reply, answer) => {
console.log("题目:%s,回答:%s,得分:%s,总分:%s \n 答案:%s", field.fieldName,
reply, current, this.score, answer)
}
typeResult(field.fieldValue?.toString(), item.answer?.toString())
}
})
},
checkForm() {
if (this.isPreview) {
this.checkUser = true
return Promise.resolve()
}
let {query: {id}, hash} = this.$route,
{openId} = this.openUser
if (hash != "#form") {
this.result = {
tips: "非法的调查问卷链接",
status: "error"
}
} else if (openId) {
return new Promise(resolve => {
this.$http.post("/app/appquestionnairetemplate/commitCheck", null, {
params: {id, openId}
}).then(res => {
if (res?.code == 0) {
this.checkUser = true
if (this.isResult && res?.data) {
this.form = res?.data
} else resolve()
} else this.result = {
tips: "调查问卷加载失败",
status: "error",
btn: "重新加载",
btnTap() {
location.reload()
}
}
}).catch(err => {
this.result = {
tips: err || "调查问卷加载失败",
}
})
})
} else {
this.getUserInfo().then(() => {
if (!!this.openUser?.openId) {
this.checkForm()?.then(() => this.checkUser && this.getForm())
} else {
this.result = {
tips: "您的信息获取失败",
status: "error",
btn: "重新加载",
btnTap() {
location.reload()
}
}
}
})
}
},
},
created() {
this.checkForm()?.then(() => this.checkUser && this.getForm())
},
mounted() {
document.title = this.form.title || "调查问卷"
}
}
</script>
<style lang="scss" scoped>
.formDetail {
display: flex;
flex-direction: column;
background: #fff;
.headPicture {
width: 100%;
height: 320px;
.img {
width: 100%;
}
}
.title {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
padding: 32px;
box-sizing: border-box;
font-size: 34px;
font-weight: bold;
color: #333;
line-height: 48px;
}
.bottom {
position: fixed;
bottom: 0;
width: 100%;
padding: 32px;
box-sizing: border-box;
background: #fff;
z-index: 99;
.bottomBtn {
width: 100%;
line-height: 96px;
background: #287DE1;
color: #fff;
text-align: center;
height: 96px;
font-size: 32px;
font-weight: bold;
border-radius: 8px;
}
}
.tableExplain {
font-size: 28px;
font-weight: 400;
color: #666;
padding: 32px 24px;
}
::v-deep .u-form-item {
.u-form-item--left {
font-size: 30px;
font-weight: bold;
color: #333;
}
.u-form-item--right__content__slot > * {
width: 100%;
}
.display {
justify-content: space-between;
min-height: 58px;
}
.uni-radio-input, .uni-checkbox-input {
height: 32px;
width: 32px;
}
.label {
flex-shrink: 0;
* + & {
margin-left: 16px;
}
}
.option {
width: 100%;
margin-right: 16px;
margin-bottom: 16px;
}
}
.content {
padding: 64px 32px 200px;
background: #fff;
}
::v-deep .scorePane {
width: calc(100% - 40px);
padding: 0 32px;
height: 124px;
background: #E9F2FF;
border-radius: 16px;
font-size: 30px;
font-weight: 500;
color: #333333;
margin-top: 48px;
box-sizing: border-box;
.fill {
text-align: center;
}
em {
font-size: 48px;
font-weight: bold;
color: #2C72FE;
font-style: normal;
margin-right: 8px;
}
}
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<section class="formList">
<ai-top-fixed>
<u-search placeholder="请输入标题" :show-action="false" search-icon-color="#ccc"
v-model="search.title" @search="page.current=1,getList()"/>
</ai-top-fixed>
<div class="mainPane">
<div class="formBox column" flex v-for="op in list" :key="op.id">
<div flex>
<div class="fill column" flex>
<b class="title">{{ op.title }}</b>
<div class="info wrap" flex>
<span v-html="op.createUserName"/>
<span v-html="op.createUnitName"/>
<span v-html="op.createTime"/>
<span v-html="$dict.getLabel('questionnaireType',op.type)"/>
</div>
</div>
<div class="split"/>
<div flex class="column submitCount">
<b>{{ op.dataCount }}</b>
<div>答卷数量</div>
</div>
</div>
<div flex class="bottom">
<div class="dot" :style="{background:$dict.getColor('questionnaireStatus',op.status)}"/>
<div>{{ $dict.getLabel("questionnaireStatus", op.status) }}</div>
</div>
</div>
</div>
</section>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
export default {
name: "formList",
components: {AiTopFixed},
data() {
return {
page: {current: 1, size: 10, total: 0},
search: {title: ""},
list: []
}
},
methods: {
getList() {
this.$http.post("/app/appquestionnairetemplate/list", null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
}
},
created() {
this.$dict.load("questionnaireStatus", 'questionnaireType')
this.getList()
}
}
</script>
<style lang="scss" scoped>
.formList {
min-height: 100%;
background: #F3F6F9;
::v-deep .mainPane {
padding: 24px 24px 126px;
.formBox {
width: 100%;
min-height: 220px;
background: #FFFFFF;
border-radius: 16px;
padding: 24px;
box-sizing: border-box;
text-align: center;
color: #999;
font-size: 20px;
margin-bottom: 24px;
& > div {
width: 100%;
}
b {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 32px;
color: #333;
margin-bottom: 16px;
}
.title {
text-align: start;
}
.info {
width: 100%;
span {
white-space: nowrap;
padding: 0 16px;
box-sizing: border-box;
border-left: 1px solid #D1D2D5;
margin-bottom: 4px;
&:first-of-type {
border-left: none;
padding-left: 0;
}
}
}
.bottom {
margin-top: 30px;
font-size: 26px;
.dot {
width: 11px;
height: 11px;
background: #3CB300;
border-radius: 50%;
margin-right: 8px;
}
}
.submitCount {
width: 118px;
b {
color: #1EA0FA;
}
& > div {
font-size: 22px;
white-space: nowrap;
}
}
.split {
width: 2px;
background: #f5f5f5;
margin: 0 24px;
}
}
}
}
</style>

View File

@@ -0,0 +1,314 @@
<template>
<div class="form-setting">
<h2>表单设置</h2>
<div class="form-setting__list">
<div class="setting-item">
<div class="setting-item__left">
<span>截止时间</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '表单截止后,用户打开表单会提示此表单已结束' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-radio-group v-model="periodValidityType" active-color="#1088F9">
<u-radio name="0">永久有效</u-radio>
<u-radio name="1">自定义时间</u-radio>
</u-radio-group>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
<div class="setting-item" v-if="periodValidityType === '1'" @click="isShowTime = true">
<div class="setting-item__left">
<span>截至时间</span>
</div>
<div class="setting-item__right">
<span>{{ periodValidityEndTime }}</span>
<u-icon name="arrow-right" color="#E1E2E3" />
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>匹配客户方式</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '将参与活动的微信客户和企业微信客户匹配' , isShowModal = true" />
</div>
<div class="setting-item__right">
<span>客户微信ID匹配</span>
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>提交次数</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '此功能发布后不可修改' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-radio-group v-model="commitType" active-color="#1088F9">
<u-radio name="0">不限次数</u-radio>
<u-radio name="1">限提交一次</u-radio>
</u-radio-group>
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>行为通知</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '当客户点击或者发布表单时,发送表单的员工将会受到消息提醒' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-switch v-model="actionNotice" active-value="1" inactive-value="0" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
<div class="setting-item">
<div class="setting-item__left">
<span>动态通知</span>
<image :src="`${$cdn}askform/bz.png`" @click="tips = '当客户点击或者发布表单时,会将客户的打开行为记录在客户动态里' , isShowModal = true" />
</div>
<div class="setting-item__right">
<u-switch v-model="dynamicNotice" active-value="1" inactive-value="0" :size="40" active-color="#1088F9"></u-switch>
</div>
</div>
</div>
<div class="add-form__footer">
<div @click="back">
<span>取消</span>
</div>
<div @click="confirm">{{ type === 'edit' ? '发布' : '确定' }}</div>
</div>
<u-modal v-model="isShowModal" :content="tips"></u-modal>
<u-picker mode="time" v-model="isShowTime" :show-time-tag="true" @confirm="onTimeChange" :params="params"></u-picker>
</div>
</template>
<script>
export default {
data () {
return {
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true
},
tips: '',
isShowModal: false,
actionNotice: true,
dynamicNotice: true,
commitType: '1',
wechatId: '0',
periodValidityEndTime: '',
isShowTime: false,
periodValidityType: '0',
type: '',
id: ''
}
},
onLoad (query) {
if (query.id) {
this.id = query.id
this.getInfo(query.id)
this.type = query.type
} else if (query.formConfig && query.formConfig !== '{}') {
const res = JSON.parse(query.formConfig)
this.periodValidityType = res.periodValidityType
this.commitType = res.commitType
this.actionNotice = res.actionNotice === '1'
this.dynamicNotice = res.dynamicNotice === '1'
if (res.periodValidityType === '1') {
this.periodValidityEndTime = res.periodValidityEndTime
}
}
},
methods: {
onTimeChange (e) {
this.periodValidityEndTime = `${e.year}-${e.month}-${e.day} ${e.hour}:${e.minute}:${e.second}`
},
back () {
uni.navigateBack({
delta: 1
})
},
getInfo (id) {
this.$http.post(`/app/appquestionnairetemplate/queryDetailById?id=${id}`).then(res => {
if (res.code == 0) {
this.periodValidityType = res.data.periodValidityType
this.commitType = res.data.commitType
this.actionNotice = res.data.actionNotice === '1'
this.dynamicNotice = res.data.dynamicNotice === '1'
if (res.data.periodValidityType === '1') {
this.periodValidityEndTime = res.data.periodValidityEndTime
}
}
}).catch(msg => {
this.$u.toast(msg)
})
},
publish () {
this.$http.post(`/app/appquestionnairetemplate/release`, null, {
params: {
commitType: this.commitType,
periodValidityType: this.periodValidityType,
actionNotice: this.actionNotice ? '1' : '0',
dynamicNotice: this.dynamicNotice ? '1' : '0',
shareStatus: '0',
wechatId: '0',
id: this.id,
periodValidityEndTime: this.periodValidityType === '1' ? this.periodValidityEndTime : ''
}
}).then(res => {
if (res.code == 0) {
uni.$emit('reload')
this.$u.toast('发布成功')
this.back()
}
}).catch(e => {
this.$u.toast(e)
})
},
confirm () {
if (this.type === 'edit') {
this.publish()
return false
}
uni.$emit('setting', {
periodValidityType: this.periodValidityType,
commitType: this.commitType,
actionNotice: this.actionNotice ? '1' : '0',
dynamicNotice: this.dynamicNotice ? '1' : '0',
periodValidityEndTime: this.periodValidityEndTime ? this.periodValidityEndTime : ''
})
this.back()
}
}
}
</script>
<style lang="scss" scoped>
.form-setting {
padding: 0 20px;
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:first-child:active {
background: #eee;
}
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
* {
box-sizing: border-box;
}
& > h2 {
height: 80px;
padding-top: 24px;
font-size: 28px;
color: #999999;
}
.form-setting__list {
padding: 0 20px;
background: #fff;
border-radius: 8px;
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
height: 104px;
border-bottom: 1px solid #D8DDE6;
.setting-item__right {
color: #999;
font-size: 28px;
span {
margin-right: 6px;
}
::v-deep .u-radio__label {
color: #999;
font-size: 28px;
}
::v-deep .u-radio {
&:last-child {
.u-radio__label {
margin-right: 6px;
}
}
}
}
&:last-child {
border: none;
}
& > div {
display: flex;
align-items: center;
}
.setting-item__left {
color: #666666;
font-size: 30px;
image {
width: 30px;
height: 30px;
margin-left: 16px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="form">
<div class="form-content">
<add-list ref="addList" v-if="currIndex === 1"></add-list>
<list ref="list" v-if="currIndex === 0"></list>
</div>
<ai-tabbar :active.sync="currIndex" :list="tabBar"/>
<div class="form-footer"></div>
</div>
</template>
<script>
import addList from './components/addList.vue'
import list from './components/list.vue'
import AiTabbar from '../../components/AiTabbar'
export default {
name: 'formIndex',
label: '问卷表单',
data () {
return {
currIndex: 0
}
},
components: {
addList,
AiTabbar,
list
},
computed: {
tabBar () {
const link = icon => `${this.$cdn}askform/${icon}.png`
return [
{text: "表单列表", iconPath: "bdlb1", selectedIconPath: "bdlb2" },
{text: "新建项目", iconPath: "xjxm1", selectedIconPath: "xjxm2" }
].map(e => ({
...e,
iconPath: link(e.iconPath),
selectedIconPath: link(e.selectedIconPath)
}))
}
},
onLoad () {
uni.$on('reload', () => {
if (this.currIndex === 0) {
this.$refs.list.reload()
} else {
this.$refs.addList.getList()
}
})
},
onReachBottom() {
if (this.currIndex === 0) {
this.$refs.list.getList()
}
}
}
</script>
<style lang="scss" scoped>
.form {
padding-bottom: 98px;
}
</style>

View File

@@ -0,0 +1,457 @@
<template>
<div class="add-form">
<div class="header-pic">
<image v-if="form.headPicture" :src="form.headPicture" />
</div>
<div class="form-info">
<div class="form-info__wrapper">
<textarea class="title" :auto-height="true" disabled placeholder="请输入标题 (必填)" v-model="form.title"></textarea>
<u-input class="content" disabled :clearable="false" type="textarea" v-model="form.tableExplain" placeholder="请输入表单描述 (选填)" :height="80" :auto-height="true" :maxlength="255"></u-input>
</div>
</div>
<div class="components-list">
<div class="components-item" v-for="(item, index) in targetList" :key="index">
<div class="components-item__title">
<div class="components-item__title--left">
<em :style="{opacity: item.required ? 1 : 0}">*</em>
<i>{{ index + 1 }}.</i>
<h2>{{ item.label }}</h2>
</div>
</div>
<div class="components-item__filed">
<template v-if="(item.type === 'radio')">
<u-radio-group v-model="item.value" wrap>
<u-radio :name="field.label" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-radio>
</u-radio-group>
</template>
<template v-if="(item.type === 'checkbox')">
<u-checkbox-group wrap>
<u-checkbox :name="field.label" v-model="field.checked1" v-for="(field, i) in item.options" :key="i">
<image :src="field.img[0].url" v-if="field.img.length"/>
<span>{{ field.label }}</span>
</u-checkbox>
</u-checkbox-group>
</template>
<template v-if="(item.type === 'select')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
<u-icon name="arrow-down" color="#DEDFDF" />
</div>
</template>
<template v-if="(item.type === 'upload')">
<div class="components-item__select components-item__textarea components-item__upload">
<image :src="`${$cdn}askform/upload.png`" />
<span>选择图片2M以内</span>
</div>
</template>
<template v-if="(item.type === 'input')">
<div class="components-item__select">
<span>{{ item.placeholder }}</span>
</div>
</template>
<template v-if="(item.type === 'textarea')">
<div class="components-item__select components-item__textarea">
<span>{{ item.placeholder }}</span>
</div>
</template>
</div>
</div>
</div>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
export default {
data () {
return {
form: {
tableExplain: '详细描述',
title: '问卷调查',
isShowheadPicture: true,
isShowTableExplain: true,
isShowBtn: true,
headPicture: '',
commitType: '1',
periodValidityType: '0',
actionNotice: '1',
dynamicNotice: '1',
periodValidityEndTime: '',
shareStatus: '0',
count: 0,
wechatId: '0',
type: 0,
buttonExplain: '提交',
tips: true
},
templateType: 0,
targetList: [],
isShow: false,
type: 0,
id: '',
touchStart: 0
}
},
onLoad (query) {
this.form = JSON.parse(query.form)
this.targetList = JSON.parse(query.targetList)
},
components: {
AiBack
},
methods: {
getInfo (id) {
uni.showLoading()
this.$http.post(`/app/appquestionnairetemplate/queryDetailById?id=${id}`).then(res => {
if (res.code == 0) {
this.form = {
...res.data,
headPicture: res.data.headPicture
}
this.type = res.data.type
this.targetList = res.data.fields.map(item => {
return JSON.parse(item.fieldInfo)
})
this.pageShow = true
} else {
this.$u.toast(res.msg)
}
uni.hideLoading()
}).catch(() => {
uni.hideLoading()
})
}
}
}
</script>
<style lang="scss" scoped>
.add-form {
min-height: 100vh;
padding-bottom: 60px;
box-sizing: border-box;
background: #F3F6F9;
* {
box-sizing: border-box;
}
::v-deep .u-drawer-bottom {
background-color: transparent;
}
.components-list {
padding: 0 20px;
.components-item {
margin-top: 24px;
padding: 32px;
box-shadow: 0 4px 8px 4px rgba(233, 233, 233, 0.39);
border-radius: 8px;
overflow: hidden;
border: 1px solid #EEEFF0;
background: #fff;
::v-deep .u-radio, ::v-deep .u-checkbox {
position: relative;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.u-checkbox__icon-wrap, .u-radio__icon-wrap {
position: absolute;
top: 4px;
}
.u-radio__label, .u-checkbox__label {
display: flex;
align-items: center;
margin-right: 0;
margin-left: 40px;
line-height: 1.5;
text-align: justify;
image {
width: 100px;
height: 100px;
margin: 0 10px;
}
}
}
span {
flex: 1;
color: #666;
font-size: 26px;
}
.components-item__select {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 80px;
margin-bottom: 8px;
padding: 0 26px;
border: 1px solid #DEDFDF;
&.components-item__textarea {
align-items: flex-start;
height: 160px;
padding-top: 20px;
image {
width: 46px;
height: 34px;
margin-right: 16px;
}
span {
color: #666;
font-size: 26px;
}
}
&.components-item__upload {
justify-content: center;
align-items: center;
}
}
.components-item__title {
display: flex;
justify-content: space-between;
margin-bottom: 32px;
em {
margin-right: 4px;
font-style: normal;
color: rgb(226, 33, 32);;
}
image {
width: 32px;
height: 32px;
box-sizing: content-box;
padding: 20px 0 20px 20px;
}
div {
display: flex;
align-items: baseline;
color: #333333;
font-size: 32px;
i {
font-style: normal;
}
h2 {
font-weight: 600;
font-size: 32px;
}
}
}
}
}
.add-popup {
height: 440px;
border-radius: 20px 20px 0 0;
background: #fff;
.add-popup__title {
display: flex;
position: relative;
align-items: center;
justify-content: center;
height: 96px;
border-bottom: 1px solid #E4E5E6;
h2 {
color: #333333;
font-size: 32px;
font-weight: 600;
}
image {
position: absolute;
right: 32px;
top: 50%;
width: 30px;
height: 20px;
transform: translateY(-50%);
}
}
.add-popup__list {
display: flex;
flex-wrap: wrap;
padding: 0 34px;
span {
width: calc((100% - 64px) / 3);
height: 78px;
line-height: 78px;
margin-top: 32px;
margin-right: 32px;
text-align: center;
color: #333333;
font-size: 28px;
border-radius: 8px;
border: 1px solid #E4E5E6;
&:nth-of-type(3n) {
margin-right: 0;
}
}
}
}
.add-form__footer {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 1;
width: 100%;
height: 112px;
text-align: center;
div {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
text-align: center;
background: #fff;
&:last-child {
color: #fff;
font-size: 36px;
background: #3192F4;
&:active {
opacity: 0.8;
}
}
span {
flex: 1;
height: 100%;
line-height: 112px;
color: #333333;
font-size: 32px;
&:active {
background: #eee;
}
}
}
}
.add-form__btn {
display: flex;
align-items: center;
justify-content: center;
width: 214px;
height: 66px;
line-height: 66px;
margin: 64px auto 0;
background: #FFFFFF;
border-radius: 34px;
&:active {
opacity: 0.8;
}
image {
width: 28px;
height: 28px;
}
span {
margin-left: 16px;
color: #4392E6;
font-size: 28px;
}
}
* {
box-sizing: border-box;
}
.form-info {
margin-top: 26px;
padding: 0 20px;
& > h2 {
height: 76px;
line-height: 76px;
color: #999999;
font-weight: normal;
font-size: 28px;
}
.form-info__wrapper {
padding: 0 18px;
background: #fff;
.title {
width: 100%;
padding: 22px 0;
font-size: 36px;
border-bottom: 1px solid #F1F2F3;
}
.content {
padding: 30px 0!important;
font-size: 28px;
::v-deep textarea {
color: #333;
font-size: 28px!important;
}
}
}
}
.header-pic {
position: relative;
font-size: 0;
span {
position: absolute;
bottom: 16px;
right: 16px;
z-index: 1;
width: 148px;
height: 56px;
line-height: 56px;
text-align: center;
color: #fff;
font-size: 26px;
background: rgba(0, 0, 0, 0.16);
border-radius: 28px;
}
image {
width: 100%;
height: 320px;
}
}
}
</style>

View File

@@ -0,0 +1,501 @@
<template>
<div class="addPlay">
<div class="content">
<div class="item">
<div class="label">播发内容</div>
<div class="value" @click="linkTo('/pages/resourcesManage/resourcesManage?isChoose=1')">
<span :class="formData.mediaName == '请选择' ? 'color-999' : ''">{{formData.mediaName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">播放设备</div>
<div class="value" @click="selectClick('showEquipment', equipmentList)">
<span :class="formData.serialName == '请选择' ? 'color-999' : ''">{{formData.serialName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">播发级别</div>
<div class="value" @click="selectClick('showMessageLevel', messageLevelList)">
<span :class="formData.messageLevelName == '请选择' ? 'color-999' : ''">{{formData.messageLevelName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
</div>
<div class="radio-content">
<div class="title">播放方式</div>
<div class="item mar-r50" :class="formData.taskType == 0 ? 'active' : ''" @click="formData.taskType = 0">立即播放<img src="./img/bigHorn-xz.png" alt=""></div>
<div class="item" :class="formData.taskType == 1 ? 'active' : ''" @click="formData.taskType = 1"><img src="./img/bigHorn-xz.png" alt="">定时播放</div>
</div>
<div class="content" v-if="formData.taskType != 0">
<div class="item">
<div class="label">定时策略</div>
<div class="value" @click="selectClick('showCyclingType', cyclingTypeList)">
<span :class="formData.cyclingTypeName == '请选择' ? 'color-999' : ''">{{formData.cyclingTypeName}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
</div>
<div class="radio-content" v-if="formData.taskType != 0 && formData.cyclingType == 2">
<div class="title">播放天数</div>
<div class="mini-item" :class="item.isCheck ? 'mini-active' : ''" v-for="(item, index) in dayList" :key="index" @click="checkClick(index)">{{item.label}}</div>
</div>
<div class="content" v-if="formData.taskType != 0 && formData.cyclingType == 3">
<div class="item">
<div class="label">播放天数</div>
<div class="value">
<u-input type="text" placeholder="请输入" height="18" input-align="right" v-model="formData.broadcastDay" maxlength="4" />
</div>
</div>
</div>
<div class="content" v-if="formData.taskType != 0">
<div class="item">
<div class="label">开始日期</div>
<div class="value" @click="timeClick(true, 'showDate')">
<span :class="formData.startDate ? 'color-999' : ''">{{formData.startDate || '请选择'}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">开始时间</div>
<div class="value" @click="timeClick(false, 'showSatrt')">
<span :class="formData.startTime ? 'color-999' : ''">{{formData.startTime || '请选择'}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
<div class="item">
<div class="label">结束时间</div>
<div class="value" @click="timeClick(false, 'showEnd')">
<span :class="formData.endTime ? 'color-999' : ''">{{formData.endTime || '请选择'}}</span>
<img src="./img/right-icon.png" alt="">
</div>
</div>
</div>
<div class="btn" @click="addConfirm">确认</div>
<u-select v-model="showSelect" :list="selectList" @confirm="confirm" label-name="dictName" value-name="dictValue"></u-select>
<u-picker v-model="showDateTime" mode="time" :params="params" @confirm="confirm"></u-picker>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
export default {
name: "addPlay",
data() {
return {
showSelect: false,
selectList: [],
showMedia: false,
mediaList: [],
showEquipment: false,
equipmentList: [],
showMessageLevel: false,
messageLevelList: [],
showCyclingType: false,
cyclingTypeList: [],
formData: {
mediaId: '',
mediaName: '请选择',
serialNo: '',
serialName: '请选择',
messageLevel: '',
messageLevelName: '请选择',
taskType: '0',
cyclingTypeName: '请选择',
cyclingType: '',
startDate: '',
startTime: '',
endTime: '',
broadcastDay: '',
cyclingDate: ''
},
dayList: [
{
isCheck: false,
value: 1,
label: '每周一'
},
{
isCheck: false,
value: 2,
label: '每周二'
},
{
isCheck: false,
value: 3,
label: '每周三'
},
{
isCheck: false,
value: 4,
label: '每周四'
},
{
isCheck: false,
value: 5,
label: '每周五'
},
{
isCheck: false,
value: 6,
label: '每周六'
},
{
isCheck: false,
value: 7,
label: '每周日'
}
],
showDateTime: false,
showDate: false,
showSatrt: false,
showEnd: false,
params: {
year: true,
month: true,
day: true,
hour: false,
minute: false,
second: false
},
}
},
components: {
AiBack
},
onLoad () {
uni.$on('choose', e => {
console.log(e)
this.formData.mediaId = e.mediaId
this.formData.mediaName = e.mediaName
})
},
methods: {
addConfirm() {
var cyclingDateList = []
this.dayList.map((item) => {
if(item.isCheck) {
cyclingDateList.push(item.value)
}
})
if(!this.formData.mediaId) {
return this.$u.toast('请选择播发内容')
}
if(!this.formData.serialNo) {
return this.$u.toast('请选择播放设备')
}
if(!this.formData.messageLevel) {
return this.$u.toast('播发级别')
}
//播放方式(定时播放)
if(this.formData.taskType != 0 && this.formData.startDate == '请选择') {
return this.$u.toast('请选择开始日期')
}
if(this.formData.taskType != 0 && this.formData.startTime == '请选择') {
return this.$u.toast('请选择开始时间')
}
if(this.formData.taskType != 0 && this.formData.endTime == '请选择') {
return this.$u.toast('请选择结束时间')
}
//播放方式(定时播放)定时策略(时长)
if(this.formData.taskType != 0 && this.formData.cyclingType == 3 && !this.formData.broadcastDay) {
return this.$u.toast('请输入播放天数')
}
//播放方式(定时播放)定时策略(自定义)
if(this.formData.taskType != 0 && this.formData.cyclingType == 2 && !cyclingDateList.length) {
return this.$u.toast('请选择播放天数')
}
if(this.formData.taskType != 0 && this.formData.cyclingType == 2) {
this.formData.cyclingDate = cyclingDateList.join(',')
}
this.formData.coverageType = '4'
this.$http.post(`/app/appzyvideobroadcast/play`, {...this.formData,}).then((res) => {
if (res.code == 0) {
this.$u.toast('提交成功')
setTimeout(() => {
uni.navigateBack()
}, 1000)
}
})
},
linkTo (path) {
uni.navigateTo({
url: path
})
},
confirm(e) {
if(this.showMedia) {
this.formData.mediaId = e[0].value
this.formData.mediaName = e[0].label
}
if(this.showEquipment) {
this.formData.serialNo = e[0].value
this.formData.serialName = e[0].label
}
if(this.showMessageLevel) {
this.formData.messageLevel = e[0].value
this.formData.messageLevelName = e[0].label
}
if(this.showCyclingType) {
this.formData.cyclingType = e[0].value
this.formData.cyclingTypeName = e[0].label
}
if(this.showDate) {
this.formData.startDate = e.year + '-' + e.month + '-' + e.day
}
if(this.showSatrt) {
var startTime = e.hour + ':' + e.minute + ':' + e.second
var myDate = new Date();
var time = myDate.getHours() + ':' + myDate.getMinutes() + ':' + myDate.getSeconds()
if (this.timeToSec(startTime) - this.timeToSec(time) > 0) {
this.formData.startTime = startTime
}else {
this.$u.toast('开始时间要大于当前时间')
}
}
if(this.showEnd) {
var endTime = e.hour + ':' + e.minute + ':' + e.second
console.log(this.timeToSec(endTime), this.timeToSec(this.formData.startTime))
if (this.timeToSec(endTime) - this.timeToSec(this.formData.startTime) > 0) {
this.formData.endTime = endTime
} else {
this.$u.toast('结束时间要大于开始时间')
}
}
this.init()
},
init() {
this.showMedia = false
this.showEquipment = false
this.showMessageLevel = false
this.showCyclingType = false
this.showDate = false
this.showSatrt = false
this.showEnd = false
},
selectClick(showType, list) {
this.showSelect = true
this[showType] = true
this.selectList = list
},
getMediaList() {
this.$http.post(`/app/appdlbresource/list?current=1&size=10000`).then((res) => {
if (res.code == 0) {
this.mediaList = []
if(res.data && res.data.records.length) {
res.data.records.map((item) => {
let info = {
dictName: item.name,
dictValue: item.id
}
this.mediaList.push(info)
})
}
}
})
},
getEquipmentList() {
this.$http.post(`/app/appdlbquipment/getDlbDeviceList?current=1&size=10000&devStatus=5&keyword=`).then((res) => {
if (res.code == 0) {
this.equipmentList = []
if(res.data && res.data.records.length) {
res.data.records.map((item) => {
let info = {
dictName: item.deviceName,
dictValue: item.serialNo
}
this.equipmentList.push(info)
})
}
}
})
},
checkClick(index) {
this.dayList[index].isCheck = !this.dayList[index].isCheck
},
timeClick(showYear, showType) {
this[showType] = true
this.showDateTime = true
if(showYear) {
this.params = {
year: true,
month: true,
day: true,
hour: false,
minute: false,
second: false
}
}else {
this.params = {
year: false,
month: false,
day: false,
hour: true,
minute: true,
second: true
}
}
},
timeToSec(time) {
var s = "";
var hour = time.split(":")[0];
var min = time.split(":")[1];
var second = time.split(":")[2];
s = Number(hour * 3600) + Number(min * 60) + Number(second)
return s;
},
},
created() {
this.$dict.load('dlbMessageUrgency', 'dlbBroadTaskType', 'dlbDyclingType').then(() => {
this.getMediaList()
this.getEquipmentList()
this.messageLevelList = this.$dict.getDict('dlbMessageUrgency')
this.cyclingTypeList = this.$dict.getDict('dlbDyclingType')
})
},
}
</script>
<style lang="scss" scoped>
.addPlay {
padding-bottom: 128px;
.content{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
padding: 34px 0;
font-size: 32px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
line-height: 44px;
border-bottom: 1px solid #ddd;
display: flex;
color: #333;
justify-content: space-between;
.label{
width: 198px;
font-size: 32px;
}
.value{
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 28px;
flex: 1;
padding-right: 32px;
max-width: calc(100% - 198px);
box-sizing: border-box;
text-align: right;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
img{
width: 32px;
height: 32px;
vertical-align: middle;
margin-left: 6px;
}
}
.color-999{
color: #999;
}
}
}
.radio-content{
padding: 34px 32px 38px;
border-bottom: 1px solid #ddd;
background-color: #fff;
.title{
font-size: 32px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #333;
line-height: 44px;
margin-bottom: 54px;
span{
font-size: 24px;
font-weight: 400;
}
}
.item{
display: inline-block;
width: 320px;
height: 112px;
line-height: 112px;
text-align: center;
background: #F5F5F5;
border-radius: 4px;
font-size: 30px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333;
img{
display: none;
}
}
.active{
background: #E7F1FE;
color: #1174FE;
position: relative;
img{
display: inline-block;
position: absolute;
bottom: 0;
right: 0;
width: 46px;
height: 46px;
}
}
.mar-r50 {
margin-right: 50px;
}
.mini-item{
display: inline-block;
width: 128px;
height: 72px;
line-height: 72px;
text-align: center;
background: #F9F9F9;
border-radius: 16px;
color: #333;
font-size: 28px;
margin-right: 58px;
margin-bottom: 32px;
}
.mini-item:nth-of-type(5) {
margin-right: 0;
}
.mini-active{
background: #F2F8FE;
border: 1px solid #89B2EE;
box-sizing: border-box;
}
}
.btn{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 112px;
line-height: 112px;
text-align: center;
background: #3975C6;
font-size: 32px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #FFFFFF;
}
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="bigHorn">
<div class="header">
<img src="./img/bigHorn-bg.png" alt="">
<div class="content">
<div class="item" @click="linkTo('./onlineList')">
<img src="./img/bigHorn-icon1@2x.png" alt="">
<div>在线设备</div>
<!-- <h2>1</h2> -->
</div>
<div class="item" @click="linkTo('./playList')">
<img src="./img/bigHorn-icon2@2x.png" alt="">
<div>播放记录</div>
</div>
<div class="item" @click="linkTo('./onlinePlayList')">
<img src="./img/bigHorn-icon3@2x.png" alt="">
<div>在播设备</div>
</div>
</div>
</div>
<div class="banner">
<div class="item" :class="item.bgClass" v-for="(item, index) in bannerList" :key="index" @click="linkTo(item.path)">
<h2>{{item.title}}</h2>
<div>{{item.text}}</div>
<img :src="item.imgUrl" alt="">
</div>
</div>
</div>
</template>
<script>
export default {
name: "bigHorn",
data() {
return {
bannerList: [
{
title: '素材播放',
text: '支持音频立即播发和定时播发',
imgUrl: require('./img/bigHorn-icon11@2x.png'),
path: '/pages/bigHorn/addPlay',
bgClass: 'bg-67A3F4'
},
{
title: '实时喊话',
text: '实时在线喊话,远程广播通知',
imgUrl: require('./img/bigHorn-icon22@2x.png'),
bgClass: 'bg-4ED5BB'
},
{
title: '音频录制',
text: '音频文件的录制',
path: '/pages/resourcesManage/addPlay?type=1',
imgUrl: require('./img/bigHorn-icon33@2x.png'),
bgClass: 'bg-E5B565'
},
{
title: '媒资管理',
path: '/pages/resourcesManage/resourcesManage',
text: '支持音频文件和录音内容添加',
imgUrl: require('./img/bigHorn-icon44@2x.png'),
bgClass: 'bg-F19661'
}
]
}
},
methods: {
linkTo(url) {
uni.navigateTo({url})
}
},
mounted() {
}
}
</script>
<style lang="scss" scoped>
.bigHorn {
.header{
position: relative;
img{
width: 100%;
height: 306px;
}
.content{
width: 686px;
padding: 40px 0;
background: #FFFFFF;
box-shadow: 0px 4px 8px 4px rgba(0, 0, 0, 0.06);
border-radius: 12px;
position: absolute;
top: 210px;
left: 50%;
transform: translateX(-50%);
display: flex;
.item{
text-align: center;
flex: 1;
img{
width: 64px;
height: 64px;
margin-bottom: 18px;
}
div{
font-size: 30px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 42px;
margin-bottom: 10px;
}
h2{
font-size: 40px;
font-family: DINAlternate-Bold, DINAlternate;
font-weight: bold;
color: #333;
line-height: 48px;
}
}
}
}
.banner{
margin-top: 150px;
.item{
width: 686px;
height: 190px;
border-radius: 12px;
margin: 0 auto 24px auto;
padding: 40px 46px 0 80px;
box-sizing: border-box;
color: #FFF;
position: relative;
h2{
font-size: 42px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
line-height: 60px;
margin-bottom: 16px;
}
div{
font-size: 26px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #FFF;
line-height: 32px;
}
img{
width: 160px;
position: absolute;
top: 10px;
right: 30px;
}
}
.bg-67A3F4{
background: #67A3F4;
}
.bg-4ED5BB{
background: #4ED5BB;
}
.bg-E5B565{
background: #E5B565;
}
.bg-F19661{
background: #F19661;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,93 @@
<template>
<div class="onlineList">
<div class="record">
<div class="item" v-for="(item, index) in list" :key="index">
<img src="./img/bigHorn-lb@2x.png" alt="">
<div class="info">
<p>{{item.deviceName}}</p>
<span>{{item.areaName}}</span>
</div>
</div>
</div>
<ai-empty v-if="!list.length"></ai-empty>
<AiBack></AiBack>
</div>
</template>
<script>
import AiEmpty from '@/components/AiEmpty/AiEmpty'
import AiBack from "@/components/AiBack";
export default {
name: "onlineList",
data() {
return {
page: {current: 1, size: 10, total: 0},
list: []
}
},
components: {
AiEmpty,
AiBack
},
methods: {
getList() {
this.$http.post("/app/appdlbquipment/getDlbDeviceList", null, {
params: {...this.page, devStatus: 5}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
},
created() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.onlineList {
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
border-bottom: 1px solid #ddd;
display: flex;
padding: 12px 40px 16px 0;
img{
width: 48px;
height: 48px;
margin: 12px 16px 0 0;
}
.info{
width: calc(100% - 100px);
p{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 48px;
margin-bottom: 12px;
}
span{
font-size: 22px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 32px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="onlinePlayList">
<div class="record">
<div class="item" v-for="(item, index) in list" :key="index">
<img src="./img/bigHorn-lb@2x.png" alt="">
<div class="info">
<p>{{item.deviceName}}</p>
<span>{{item.createTime}}</span><br />
<span>{{item.name}}</span>
</div>
</div>
</div>
<ai-empty v-if="!list.length"></ai-empty>
<AiBack></AiBack>
</div>
</template>
<script>
import AiEmpty from '@/components/AiEmpty/AiEmpty'
import AiBack from "@/components/AiBack";
export default {
name: "onlinePlayList",
data() {
return {
page: {current: 1, size: 10, total: 0},
list: []
}
},
components: {
AiEmpty,
AiBack
},
methods: {
getList() {
this.$http.post("/app/appdlbquipment/getDlbDeviceList", null, {
params: {...this.page, devStatus: 1}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
},
created() {
this.getList()
}
}
</script>
<style lang="scss" scoped>
.onlinePlayList {
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
border-bottom: 1px solid #ddd;
display: flex;
padding: 12px 40px 16px 0;
img{
width: 48px;
height: 48px;
margin: 12px 16px 0 0;
}
.info{
width: calc(100% - 100px);
p{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 48px;
margin-bottom: 12px;
}
span{
font-size: 22px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 32px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="playList">
<div class="title">
<span>播放记录</span>
<span>操作</span>
</div>
<div class="record">
<div class="item" v-for="(item, index) in list" :key="index">
<div class="info">
<p>{{item.messageName}}</p>
<span>{{item.createTime}}</span><br />
<span>{{item.deviceName}}</span>
</div>
<div class="btn bg-3975C6" v-if="item.broadcastStatus == 0 || item.broadcastStatus == 1 || item.broadcastStatus == 2" @click="cancel(item.broadcastId)">撤销</div>
<div class="btn bg-AFD0FC" v-if="item.broadcastStatus == 6">已取消</div>
</div>
</div>
<AiBack></AiBack>
</div>
</template>
<script>
import AiBack from "@/components/AiBack";
export default {
name: "playList",
data() {
return {
page: {current: 1, size: 10, total: 0},
list: []
}
},
components: {
AiBack
},
methods: {
getList() {
this.$http.post("/app/appzyvideobroadcast/getBroadcastRecords", null, {
params: {...this.page}
}).then(res => {
if (res?.data) {
if (this.page.current > 1) {
this.list = [...this.list, ...res.data.records]
} else this.list = res.data.records
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
cancel(id) {
this.$confirm('确定撤回该广播?').then(() => {
this.$http.post(`/app/appzyvideobroadcast/getBroadcastRecall?broadcastId=${id}`).then((res) => {
if (res.code == 0) {
this.$u.toast('撤回成功!')
this.getList()
}
})
})
},
},
created() {
this.$dict.load('dlbBroadcastStatus').then(() => {
this.getList()
})
}
}
</script>
<style lang="scss" scoped>
.playList {
.title{
width: 100%;
height: 88px;
line-height: 88px;
background: #FFF;
padding: 0 80px;
display: flex;
justify-content: space-between;
color: #333;
font-size: 34px;
box-sizing: border-box;
border-bottom: 1px solid #ddd;
}
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
padding: 12px 40px 16px 0;
box-sizing: border-box;
.info{
width: 480px;
margin-right: 40px;
word-break: break-all;
p{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
line-height: 48px;
margin-bottom: 12px;
}
span{
font-size: 22px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 32px;
}
}
.btn{
width: 154px;
height: 60px;
border-radius: 8px;
text-align: center;
line-height: 60px;
font-size: 30px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #FFF;
margin-top: 18px;
}
.bg-3975C6{
background: #3975C6;
}
.bg-AFD0FC{
background: #AFD0FC;
}
}
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div class="selectEquipment">
<div class="search">
<div class="search-bg">
<img src="./img/search-icon.png" alt="">
<u-input v-model="value" type="text" placeholder="搜索设备名称" class="search-input" height="18" />
</div>
</div>
<div class="record">
<div class="item">
<img src="./img/cir.png" alt="" class="check-img">
<img src="./img/lb@2x.png" alt="" class="voice-img">
<div class="info">
<div class="text">
<p>村头大喇叭</p>
<span>刘家河居委会</span>
</div>
<div class="status">在线</div>
</div>
</div>
<div class="item">
<img src="./img/cir.png" alt="" class="check-img">
<img src="./img/lb@2x.png" alt="" class="voice-img">
<div class="info">
<div class="text">
<p>村头大喇叭</p>
<span>刘家河居委会</span>
</div>
<div class="status">在线</div>
</div>
</div>
</div>
<div class="btn">
<div>确定选择</div>
</div>
</div>
</template>
<script>
export default {
name: "selectEquipment",
data() {
return {
value: ''
}
},
methods: {
},
mounted() {
}
}
</script>
<style lang="scss" scoped>
.selectEquipment {
padding-bottom: 128px;
.search{
width: 100%;
height: 104px;
background: #FFF;
margin-bottom: 4px;
padding: 20px 32px;
box-sizing: border-box;
.search-bg{
width: 686px;
height: 64px;
padding: 14px 0;
box-sizing: border-box;
background: #F5F5F5;
border-radius: 32px;
position: relative;
img{
width: 32px;
height: 32px;
position: absolute;
top: 16px;
left: 32px;
}
.search-input{
width: 590px;
height: 36px;
line-height: 36px;
font-size: 28px;
margin-left: 70px;
}
}
}
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
display: flex;
border-bottom: 1px solid #ddd;
.check-img{
width: 32px;
height: 32px;
margin: 32px 32px 0 0;
}
.voice-img{
width: 48px;
height: 48px;
margin: 28px 16px 0 0;
}
.info{
width: calc(100% - 148px);
padding: 18px 0;
line-height: 44px;
font-size: 34px;
display: flex;
justify-content: space-between;
.text{
p{
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
span{
font-size: 26px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #999;
line-height: 36px;
}
}
.status{
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #4E8EEE;
line-height: 48px;
}
}
}
}
.btn{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 128px;
background: #FFF;
border-top: 1px solid #ddd;
padding: 24px 32px 24px 0;
box-sizing: border-box;
div{
width: 192px;
height: 80px;
line-height: 80px;
text-align: center;
background: #3975C6;
border-radius: 4px;
color: #fff;
font-size: 32px;
float: right;
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="selectMp3">
<div class="record">
<div class="item">
<img src="./img/cir.png" alt="">
<div class="info">
<p>村头大喇叭</p>
</div>
</div>
</div>
<div class="btn">
<div>确定选择</div>
</div>
</div>
</template>
<script>
export default {
name: "selectMp3",
data() {
return {
}
},
methods: {
},
mounted() {
}
}
</script>
<style lang="scss" scoped>
.selectMp3 {
padding-bottom: 128px;
.record{
padding-left: 32px;
background-color: #fff;
.item{
width: 100%;
display: flex;
img{
width: 32px;
height: 32px;
margin: 40px 16px 0 0;
}
.info{
width: calc(100% - 60px);
padding-bottom: 16px;
padding: 34px 0;
line-height: 44px;
border-bottom: 1px solid #ddd;
font-size: 34px;
font-family: PingFang-SC-Medium, PingFang-SC;
font-weight: 500;
color: #333;
margin-left: 16px;
}
}
}
.btn{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 128px;
background: #FFF;
border-top: 1px solid #ddd;
padding: 24px 32px 24px 0;
box-sizing: border-box;
div{
width: 192px;
height: 80px;
line-height: 80px;
text-align: center;
background: #3975C6;
border-radius: 4px;
color: #fff;
font-size: 32px;
float: right;
}
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<div class="casuallyask">
<div class="banner">
<picker :range="dictList" @change="bindPickerChange" range-key="dictName">
<div class="picker">
{{ askType }}
<u-icon
name="arrow-down"
:custom-style="{ margin: '0 5px' }"
></u-icon>
</div>
</picker>
<u-search
placeholder="请输入标题"
:show-action="false"
v-model="keyword"
@search="onSearch"
/>
</div>
<u-tabs
class="nav"
:list="tabs"
:is-scroll="false"
:current="currentType"
font-size="32"
bar-width="192"
height="96"
@change="handleTabClick"
></u-tabs>
<div class="body" v-if="eventList.length !== 0">
<div
class="content"
v-for="(item, index) in eventList"
:key="index"
@click="detail(item)"
>
<u-row>
<div>
<div class="top">
<text :class="item.type == '0' ? 'active' : 'noactive'">
{{ $dict.getLabel('leaveMessageType', item.type) }}
</text>
<!-- -->
<text class="areaName" v-if="item.areaName">{{ item.areaName }}</text>
<!-- 问题 -->
<text class="title">{{ item.title }}</text>
</div>
<div class="cont">
<span class="name_time">留言人:</span>
<text class="text_c"> {{ item.leaveName }}</text>
</div>
<div class="cont">
<text class="name_time">留言时间:</text>
<text class="text_c"> {{ item.createTime }}</text>
</div>
</div>
</u-row>
</div>
</div>
<AiEmpty v-else/>
</div>
</template>
<script>
import AiEmpty from '../../components/AiEmpty/AiEmpty'
import URow from '../../uview/components/u-row/u-row.vue'
export default {
name: 'casuallyAsk',
// 组件
components: {URow, AiEmpty},
props: {},
data() {
return {
page: {current: 1, size: 10, total: 0},
dictList: [], // 字典
index: 0, // 留言类型 0投诉 1咨询 2建议
keyword: '',
askType: '提问类型',
askIndex: '',
currentType: 0, // 默认value的值(0待我回复)(1已回复)(2处理完成)
eventList: [] // 数据列表
// temp: { '0': '投诉', '1': '建议', '2': '咨询' }
}
},
// 计算
computed: {
tabs() {
return [
{name: '待我回复', value: 0},
{name: '我已回复', value: 1},
{name: '处理完成', value: 2}
]
}
},
// 实例创建后
onShow() {
this.$dict.load('leaveMessageType').then(() => {
this.dictList = this.$dict.getDict('leaveMessageType')
this.getList()
})
},
// 方法
methods: {
// 点击 value的值(0待我回复)(1已回复)(2处理完成)
handleTabClick(i) {
this.currentType = i
this.getList()
},
getList() {
this.$http.post(`/app/appleavemessage/list`, null, {
params: {
...this.page,
title: this.keyword,
status: this.currentType,
type: this.askIndex
}
}).then(res => {
if (res?.data) {
this.page.total = res.data.total
if (this.page.current > 1)
this.eventList = [...this.eventList, res.data.records]
else this.eventList = res.data.records
}
})
},
detail(item) {
uni.navigateTo({
url: `/pages/casuallyask/casuallyaskDetail?id=${item.id}&type=${item.type}`
// url: `/pages/casuallyask/casuallyaskDetail?id=${item.id}`
})
},
// 提问类型
bindPickerChange(e) {
let index = e.detail.value
this.askType = this.dictList[index].dictName
this.askIndex = index
this.getList()
},
onSearch(e) {
this.keyword = e
this.getList()
}
},
onReachBottom() {
if (this.eventList.length < this.page.total) {
this.page.current++
this.getList()
}
}
}
</script>
<style scoped lang="scss">
.casuallyask {
min-height: 100%;
background: #f5f5f5;
padding-top: 64px;
.banner {
position: fixed;
top: 0;
width: 100%;
background-color: #fff;
height: 80px;
line-height: 80px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 30px 0 30px;
.picker {
font-size: 30px;
font-weight: 500;
color: #333333;
margin-right: 8px;
.u-icon-wrap {
margin-left: 25px;
}
}
}
.nav {
position: fixed;
top: 80px;
background-color: #ffffff;
height: 96px;
padding-top: 8px;
width: 100%;
}
.body {
padding: 114px 0 0 0;
.content {
background-color: #ffffff;
box-sizing: border-box;
padding: 34px 32px;
margin: 32px 20px;
.top {
display: flex;
align-items: center;
width: 100%;
.typeBox {
}
.noactive {
text-align: center;
color: #2266ff;
width: 70px;
height: 44px;
line-height: 44px;
border-radius: 8px;
background-color: #e8efff;
}
.active {
text-align: center;
color: #ff4466;
line-height: 44px;
width: 70px;
height: 44px;
border-radius: 8px;
background-color: #ffebef;
}
.areaName {
display: inline-block;
color: #333;
font-size: 32px;
margin-left: 15px;
}
.title {
color: #333;
font-size: 32px;
}
}
.cont {
margin: 10px 0;
.name_time {
display: inline-block;
width: 150px;
height: 24px;
color: #999;
font-size: 30px;
// font-weight: 800;
}
.text_c {
color: #343d65;
font-size: 30px;
// font-weight: 800;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,442 @@
<template>
<div class="casuallyaskDetail">
<div class="detail">
<div class="headerTitle" flex>
<div class="temp">
[{{ $dict.getLabel('leaveMessageType', detail.type) }}]
</div>
<span class="areaName">{{ detail.areaName }}</span>
<span>{{ detail.title }}</span>
</div>
<div class="leaveName_leavePhone">
<!-- 顶部圆形头像 -->
<span class="icon">{{ detail.headPortrait || $formatName(detail.leaveName) }}</span>
<span class="leaveName">{{ detail.leaveName }}</span>
<span class="leavePhone" v-if="detail.leavePhone">({{ detail.leavePhone }})</span>
</div>
<!-- 编号 -->
<div class="info-item">
<span class="label">
<u-icon name="order"></u-icon>
</span>
<span class="value">{{ detail.msgCode }}</span>
</div>
<!-- 日期 -->
<div class="info-item">
<span class="label">
<u-icon name="clock"></u-icon>
</span>
<span class="value">{{ detail.createTime }}</span>
</div>
<!-- 进度-->
<div class="info-item">
<span class="label"><u-icon name="tags"/></span>
<text class="status">{{
$dict.getLabel('leaveMessageStatus', detail.status)
}}
</text>
</div>
<div class="content_text_img">
<!-- 提问内容 -->
<div class="content_text">
{{ detail.content }}
</div>
<!-- 提问内容的图片 -->
<div class="imageList">
<ai-image preview :src="items.url" alt="" v-for="(items, i) in imgList" :key="i"/>
</div>
</div>
<div class="reply_content" v-if="detail.status == 1 || detail.status == 2">
<div class="reply_title">
<img src="https://cdn.cunwuyun.cn/img/dialogue.svg" alt=""/>
<p>沟通记录</p>
</div>
<div class="reply_list">
<div class="item" v-for="(item, index) in appLeaveMessageReplyList" :key="index">
<div class="item_top">
<div class="item_left">
<div class="icon">
{{ detail.headPortrait || $formatName(item.createUserName) }}
</div>
<div class="name fill">
<div class="createUserName_createUnitName">
<span v-if="item.createUserId == user.id" class="reply_font">我的回复</span>
<template v-else>
<span class="createUserName">{{ item.createUserName }}&nbsp;</span>
<span class="createUserName" v-if="item.createUserPhone">({{ item.createUserPhone }})</span>
</template>
</div>
<div flex v-if="item.createUserId != user.id">
<!-- 回复单位 -->
<span class="createUnitName">{{ item.createUnitName }}</span>
<span class="reply">回复</span>
</div>
</div>
</div>
<div class="item_right">{{ item.createTime }}</div>
</div>
<!-- 回复内容 -->
<div class="myreply_con">
<div class="myreply_text">
{{ item.content }}
</div>
<!-- 回复图片 -->
<div class="imageList">
<ai-image v-for="img in item.images" :key="img.id" preview :src="img.url"/>
</div>
<!-- -->
</div>
</div>
</div>
</div>
</div>
<div class="close_reply" v-if="detail.status == 0 || detail.status == 1">
<div class="btn" @click="close">关闭留言</div>
<div class="btn reply" @click="reply">回复</div>
</div>
<u-modal
v-model="show"
:content="content"
cancel-color="#2979ff"
title=""
:async-close="true"
:show-cancel-button="true"
@confirm="handleCloseMessage"
@cancel="show = false"/>
<back/>
</div>
</template>
<script>
import back from '../../components/AiBack'
import {mapState} from 'vuex'
import AiImage from "../../components/AiImage";
import UImage from "../../uview/components/u-image/u-image";
export default {
name: 'casuallyaskDetail',
components: {UImage, AiImage, back},
props: {},
computed: {
...mapState(['user'])
},
onLoad(options) {
this.getDetail(options.id)
},
onShow() {
this.$dict.load('leaveMessageStatus', 'leaveMessageType')
},
data() {
return {
objdie: '', // id
appLeaveMessageReplyList: [], //遍历得到的数据
show: false, // 默认关闭模态框
content: '关闭留言后,双方都无法再进行回复,是否确定关闭本次留言?',
msgCode: null,
imgList: [],
detail: {}
// images:[]
}
},
methods: {
getDetail(id = this.detail.id) {
this.$http.post(`/app/appleavemessage/queryDetailById?id=${id}`,).then(res => {
if (res?.data) {
this.detail = res.data
this.imgList = JSON.parse(this.detail.images)
this.appLeaveMessageReplyList = res.data.appLeaveMessageReplyList.map(item => ({
...item,
images: JSON.parse(item.images).map(e => ({url: e.url || e?.file?.accessUrl, id: e.file?.id}))
}))
}
})
},
// 关闭留言
close() {
this.show = true
this.getDetail()
},
// 确定关闭留言
handleCloseMessage() {
this.$http.post(`/app/appleavemessage/release?id=${this.objdie}&status=2`).then(res => {
if (res?.code == 0) {
this.getDetail()
uni.navigateTo({url: `./closemsg?flag=true`})
}
}).catch(() => {
uni.navigateTo({url: `./closemsg?flag=false`})
})
this.show = false
},
// 去回复
reply() {
uni.navigateTo({url: `./reply?msgCode=${this.detail.msgCode}`})
}
},
}
</script>
<style lang="scss" scoped>
.casuallyaskDetail {
padding-bottom: 80px;
.detail {
padding: 15px 32px 112px 32px;
background-color: #fff;
font-size: 30px;
font-weight: 400;
color: #343D65;
.headerTitle {
font-size: 40px;
font-weight: bold;
color: #333;
line-height: 64px;
letter-spacing: 2px;
flex-wrap: wrap;
}
.leaveName_leavePhone {
height: 56px;
margin: 20px 0 36px 0;
.icon {
display: inline-block;
width: 56px;
height: 56px;
line-height: 56px;
border-radius: 50%;
background-color: #2266ff;
color: #fff;
font-size: 22px;
vertical-align: middle;
text-align: center;
}
.leaveName {
// display: block;
margin-left: 20px;
color: #343d65;
}
// .leavePhone {
// }
}
.info-item {
height: 40px;
line-height: 40px;
font-size: 28px;
margin-bottom: 8px;
.label {
display: inline-block;
}
.value {
display: inline-block;
margin-left: 20px;
}
.status {
display: inline-block;
margin-left: 20px;
// color: #999;
}
}
.content_text_img {
margin-top: 64px;
max-height: 700px;
color: #000;
font-size: 32px;
// font-weight: 800;
overflow: hidden;
.content_text {
width: 100%;
margin-bottom: 12px;
}
}
.reply_content {
margin-top: 25px;
.reply_title {
margin-top: 25px;
width: 225px;
height: 45px;
line-height: 45px;
img {
vertical-align: middle;
}
p {
display: inline-block;
color: #333;
// font-weight: 800;
margin-left: 10px;
}
}
.reply_list {
.item {
.item_top {
display: flex;
justify-content: space-between;
margin-top: 10px;
.item_left {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
.icon {
display: block;
flex-shrink: 0;
width: 64px;
height: 64px;
line-height: 64px;
border-radius: 50%;
background-color: #2266ff;
color: #fff;
font-size: 23px;
text-align: center;
margin-right: 10px;
}
.name {
display: flex;
flex-direction: column;
.createUnitName {
color: #135ab8;
margin-right: 8px;
}
.createUserName_createUnitName {
width: 100%;
display: flex;
.reply_font {
width: 120px;
color: #333333;
font-size: 28px;
}
.createUserName {
margin-bottom: 5px;
color: #135ab8;
}
}
}
}
.item_right {
font-size: 26px;
color: #999;
}
}
.myreply_con {
width: 570px;
background-color: #f3f6f9;
margin: 24px 0 0 80px;
padding: 14px;
.myreply_text {
overflow: auto;
word-wrap: break-word;
word-break: break-all;
}
.imageList {
display: flex;
flex-wrap: wrap;
.AiImage {
margin: 0 12px 12px 0;
}
}
}
}
.no_yes_reply {
.con_title {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 16px;
height: 64px;
line-height: 64px;
.icon {
display: inline-block;
width: 60px;
height: 60px;
line-height: 60px;
border-radius: 50%;
background-color: #2266ff;
color: #fff;
font-size: 23px;
vertical-align: middle;
text-align: center;
// font-weight: 800;
}
.my_reply {
width: 370px;
}
.right {
color: #999;
}
}
.myreply_content {
width: 606px;
height: 460px;
background-color: #f3f6f9;
margin: 24px 0 0 62px;
padding: 16px;
}
}
}
}
.no_more {
padding: 44px 0;
color: #999;
font-size: 24px;
text-align: center;
}
}
.close_reply {
position: fixed;
bottom: 0;
display: flex;
height: 112px;
width: 100%;
border-top: 1px solid #ddd;
z-index: 999;
background: #fff;
.btn {
flex: 1;
min-width: 0;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
&.reply {
background-color: #1365dd;
color: #fff;
}
}
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="closemsg">
<img :src="imgSrc" alt="" />
<text>{{ text }}</text>
<u-button
type="primary"
:custom-style="{ width: '100%', borderRadius: '4px', marginTop: '48px' }"
@click="goBack"
>{{ btnText }}</u-button
>
<back></back>
</div>
</template>
<script>
import back from '../../components/AiBack'
export default {
name: 'CloseMsg',
components: { back },
data() {
return {
flag: true
}
},
onLoad(val) {
if (val.flag) {
this.flag = val.flag
}
},
methods: {
goBack() {
uni.navigateBack({
// url: `/pages/casuallyask/casuallyask`
delta: 1
})
}
},
computed: {
text() {
return this.flag ? '关闭留言成功!' : '关闭留言失败'
},
btnText() {
return this.flag ? '确定' : '查看详情'
},
imgSrc() {
return this.flag
? this.imgOtherUrl + 'kztcg.png'
: this.imgOtherUrl + 'kztsb.png'
}
}
}
</script>
<style lang="scss" scoped>
.closemsg {
min-height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
padding: 96px;
img {
width: 192px;
height: 192px;
}
text {
font-size: 36px;
font-weight: 800;
color: #333333;
line-height: 50px;
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<div class="reply">
<div class="reply_content">
<span class="icon">*</span>
<text class="msgfont">回复内容</text>
<textarea :maxlength="500" placeholder="请输入内容(500字以内)" v-model="msg" class="textarea"></textarea>
<!-- <u-input
type="textarea"
height="50"
:auto-height="false"
placeholder="请输入内容500字以内"
placeholder-style="color:#999;"
maxlength="500"
v-model="msg"
class="textarea"
/> -->
</div>
<div class="reply_img">
<text class="img">图片资料</text>
<text class="img_text">(最多9张)</text>
<div class="upload">
<div class="info">
<ai-uploader multiple @data="data" @change="change" :limit="9" action="/admin/file/add2"></ai-uploader>
</div>
</div>
</div>
<ai-back/>
<div class="submit">
<button class="btn" @click="btn">提交</button>
</div>
</div>
</template>
<script>
import AiUploader from '../../components/AiUploader'
import AiBack from "../../components/AiBack";
export default {
// name: '',
// 组件
components: {AiBack, AiUploader},
props: {},
data() {
return {
msg: '',
files: [],
msgCode: '',
}
},
onLoad(options) {
this.msgCode = options.msgCode
},
// 计算
computed: {},
// 监听
watch: {},
// 实例创建后
onShow() {
this.$dict.load('leaveMessageType').then(() => {
this.dictList = this.$dict.getDict('leaveMessageType')
})
},
// 实例渲染后
mounted() {
},
// 方法
methods: {
data(e) {
this.files.push(e)
},
// selectEventType(selecteds) {
// this.eventType = selecteds?.[0]?.value
// },
change(e) {
this.files = e
},
btn() {
if (this.msg == '') {
return uni.showToast({
title: '请输入留言内容',
icon: 'none',
})
}
this.$http
.post(`/app/appleavemessagereply/addOrUpdate`, {
images: JSON.stringify(this.files),
content: this.msg,
msgCode: this.msgCode,
userType: '1',
}).then(res => {
if (res?.code == 0) {
uni.navigateTo({
url: `/pages/casuallyask/truemsg?flag=1`,
})
}
}).catch(err => {
uni.navigateTo({
url: `/pages/casuallyask/truemsg?flag=0`,
})
this.$u.toast(err || '网络异常')
})
},
},
}
</script>
<style scoped lang="scss">
.reply {
width: 100%;
// height: 100%;
.reply_content {
height: 288px;
padding: 0 20px 0 20px;
background-color: #fff;
.icon {
display: inline-block;
width: 16px;
height: 44px;
line-height: 44px;
font-size: 42px;
vertical-align: middle;
color: red;
margin-top: 10px;
}
.msgfont {
// overflow: auto;
// word-wrap: break-word;
// word-break: break-all;
display: inline-block;
width: 226px;
height: 44px;
margin-top: 34px;
line-height: 44px;
font-size: 32px;
// font-weight: 800;
margin-left: 10px;
}
.textarea {
height: 190px;
width: 700px;
// overflow: auto;
// word-wrap: break-word;
// word-break: break-all;
}
}
.reply_img {
margin-top: 20px;
background-color: #fff;
padding: 0 20px;
box-sizing: border-box;
.img {
display: inline-block;
margin-top: 20px;
font-size: 32px;
// font-weight: 800;
}
.img_text {
font-size: 32px;
color: #999;
margin-left: 10px;
}
.upload {
width: 100%;
margin-top: 10px;
padding: 12px 12px 12px 0;
box-sizing: border-box;
.info {
width: 100%;
text-align: center;
}
::v-deep .ai-uploader .fileList .default {
width: 160px;
height: 160px;
}
}
}
.submit {
padding: 50px 32px;
.btn {
background-color: #1365dd;
color: #fff;
font-size: 36px;
}
}
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div class="closemsg">
<img :src="imgSrc" alt=""/>
<text>{{ text }}</text>
<u-button
type="primary"
:custom-style="{ width: '100%', borderRadius: '4px', marginTop: '48px' }"
@click="goBack"
>{{ btnText }}
</u-button>
<back></back>
</div>
</template>
<script>
import back from '../../components/AiBack'
export default {
name: 'CloseMsg',
components: {back},
data() {
return {
flag: true
}
},
onLoad(val) {
this.flag = val?.flag == 1
},
methods: {
goBack() {
uni.navigateBack({
delta: 2
})
}
},
computed: {
text() {
return this.flag ? '提交成功!' : '处理失败'
},
btnText() {
return this.flag ? '查看详情' : '我知道了'
},
imgSrc() {
return this.flag
? this.imgOtherUrl + 'kztcg.png'
: this.imgOtherUrl + 'kztsb.png'
}
}
}
</script>
<style lang="scss" scoped>
.closemsg {
min-height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
padding: 96px;
img {
width: 192px;
height: 192px;
}
text {
font-size: 36px;
font-weight: 800;
color: #333333;
line-height: 50px;
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<div class="approval">
<div class="card">
<header>批示意见</header>
<textarea placeholder="请输入批示意见" v-model.trim="description" maxlength="200"></textarea>
<u-row justify="between">
<span @click="description=''">清空内容</span>
<span>{{description.length || 0}}/200</span>
</u-row>
</div>
<ai-back/>
<u-button type="primary" @click="submit">提交</u-button>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
export default {
name: "approval",
components: {AiBack},
data() {
return {
id: null,
description: ""
}
},
onLoad(opt) {
this.id = opt.id
},
methods: {
submit() {
this.$http.post("/app/appofficialdocumentinfo/instructionById", null, {
params: {
id: this.id,
description: this.description
}
}).then(res => {
if (res.code == 0) {
this.$u.toast("批示成功")
uni.navigateBack()
}
})
}
}
}
</script>
<style lang="scss" scoped>
.approval {
background: #F5F5F5;
.card {
background-color: #FFFFFF;
box-sizing: border-box;
padding: 32px;
header {
font-size: 32px;
color: #333333;
margin-bottom: 16px;
font-weight: bold;
}
textarea {
width: 100%;
}
span:first-child {
font-size: 28px;
color: #1365DD;
}
span:last-child {
font-size: 24px;
color: #999999;
}
}
.u-btn {
width: 100%;
height: 112px !important;
font-size: 32px;
color: #FFFFFF;
position: fixed;
left: 0;
bottom: 0;
background: #197DF0 !important;
}
}
</style>

View File

@@ -0,0 +1,432 @@
<template>
<div class="detail">
<template v-if="!userSelect">
<div class="card">
<header>{{detail.documentName}}</header>
<u-gap height="16"></u-gap>
<u-row>
<span>公文编号</span>
<span>{{detail.documentCode}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>公文类型</span>
<span>{{$dict.getLabel("officialDocumentName",detail.documentType)}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>紧急程度</span>
<span :style="{color:$dict.getColor('documentEmergencyLevel',detail.emergencyLevel)}">{{$dict.getLabel("documentEmergencyLevel",detail.emergencyLevel)}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>发文机关</span>
<span>{{detail.issuingUnit}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>发文字号</span>
<span>{{detail.issuingFont}}</span>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<span>签发人</span>
<span>{{detail.signer}}</span>
</u-row>
<u-gap height="16"></u-gap>
<img v-if="detail.confidentialityLevel" :src="$cdn + tag(detail.confidentialityLevel)" alt="">
</div>
<div class="card" style="margin-bottom: 0;padding-top: 0">
<div class="label">备注</div>
<span>{{detail.remark}}</span>
</div>
<div class="card" style="padding-top: 0" v-if="detail.files && detail.files.length">
<div class="label">相关附件</div>
<div class="file" v-for="(item,index) in detail.files" :key="index" @click="preFile(item)">
<u-row justify="between">
<label class="left">
<img :src="$cdn + 'common/appendix.png'" alt="">
<span>{{item.fileName}}.{{item.postfix}}</span>
</label>
<span>{{(item.size/1024).toFixed(2)}}KB</span>
</u-row>
</div>
</div>
<div class="card">
<div class="label" style="96px;">{{detail.readType ==0 ? "流转信息" : "传阅情况"}}
<em>({{$dict.getLabel("documentStatus",detail.status)}})</em></div>
<div class="progress">
<div class="item" v-for="(item,index) in detail.flowUsers" :key="index">
<div class="avatar">{{item.flowUserName && item.flowUserName.substr(-2)}}</div>
<div class="right">
<u-row justify="between">
<text class="status" :style="{color:item.readStatus==1?'#FF8822':'#1365DD'}">{{$dict.getLabel(detail.readType ==1 ? "readingStatus" :
"documentFlowStatus",detail.readType ==0 ? item.flowStatus : item.readStatus)}}
</text>
<text class="date">{{item.flowTime}}</text>
</u-row>
<u-row justify="between">
<text class="name">{{item.flowUserName}}</text>
</u-row>
<u-row justify="between">
<text class="note">{{item.description}}</text>
</u-row>
</div>
</div>
</div>
</div>
<div class="footer" v-if="detail.flowRight==1 && detail.readType==0">
<div @click="handleClick(0)">批示</div>
<div @click="handleClick(1)">流转</div>
</div>
<div class="footer" v-if="detail.readType==1 && detail.flowRight==1" @click="read" style="background-color: #1365DD;color: #FFFFFF">我已阅完</div>
</template>
<AiSelectEnterprise :visible.sync="userSelect" v-if="userSelect" :multiple="false"
@change="change"></AiSelectEnterprise>
<AiBack v-if="!userSelect"/>
</div>
</template>
<script>
import AiBack from "../../../components/AiBack";
import AiSelectEnterprise from "../../../components/AiSelectEnterprise/AiSelectEnterprise";
import {mapActions} from "vuex";
export default {
name: "detail",
components: {AiBack, AiSelectEnterprise},
data() {
return {
id: null,
detail: {},
userSelect: false,
}
},
onLoad(opt) {
this.$dict.load("officialDocumentName", "documentEmergencyLevel", "documentStatus", "readingStatus", "documentFlowStatus")
this.id = opt.id
},
methods: {
...mapActions(['previewFile', 'injectJWeixin']),
read() {
this.$http.post("/app/appofficialdocumentinfo/readById", null, {
params: {
id: this.id
}
}).then(res => {
if (res.code == 0) {
this.$u.toast("已阅读")
this.getDetail()
}
})
},
preFile(e) {
if([".jpg",".png",".gif"].includes(e.postfix.toLowerCase())){
uni.previewImage({
current: e.url,
urls: [e.url]
})
}else {
this.previewFile({...e})
}
},
change(e) {
this.$http.post("/app/appofficialdocumentinfo/flowById", null, {
params: {
flowUserId: e[0].id,
flowUserName: e[0].name,
id: this.id,
avatar: e[0].avatar,
flag: 0
}
}).then(res => {
if (res.code == 0) {
this.$u.toast("流转成功")
this.getDetail()
}
})
},
tag(status) {
return {
"0": "common/mm.png",
"1": "common/jm.png",
"2": "common/tm.png"
}[status]
},
getDetail() {
this.$http.post("/app/appofficialdocumentinfo/queryDetailById", null, {
params: {
id: this.id,
flag: 1
}
}).then(res => {
if (res && res.data) {
this.detail = res.data
}
})
},
handleClick(status) {
if (status == 0) {
uni.navigateTo({
url: "/pages/documentFlow/components/approval?id=" + this.id
})
} else {
this.userSelect = true
}
}
},
onShow() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.detail {
min-height: 100%;
background-color: #F5F5F5;
padding-bottom: 140px;
position: relative;
.card {
background-color: #FFFFFF;
margin-bottom: 8px;
box-sizing: border-box;
padding: 16px 32px;
position: relative;
header {
font-size: 40px;
font-weight: 600;
color: #333333;
line-height: 64px;
letter-spacing: 1px;
}
.u-row {
& > div {
background-color: #2266FF;
border-radius: 50%;
text-align: center;
font-size: 22px;
font-weight: bold;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
& > span:first-child {
font-size: 30px;
color: #999999;;
line-height: 48px;
}
& > span:last-child {
font-size: 30px;
color: #343D65;
margin-left: 16px;
line-height: 48px;
}
}
& > img {
width: 190px;
height: 190px;
position: absolute;
right: 0;
top: 74px;
}
& > span {
font-size: 32px;
color: #333333;
line-height: 48px;
letter-spacing: 1px;
display: inline-block;
}
.label {
height: 80px;
font-size: 32px;
color: #333333;
display: flex;
align-items: center;
margin-bottom: 16px;
& > em {
font-style: normal;
font-size: 32px;
color: #1365DD;
}
}
.file {
height: 128px;
background: #FFFFFF;
border-radius: 8px;
border: 1px solid #CCCCCC;
box-sizing: border-box;
padding: 0 16px;
margin-bottom: 32px;
& > .u-row {
height: 100%;
.left {
width: 500px;
display: flex;
align-items: center;
& > img {
flex-shrink: 0;
width: 96px;
height: 96px;
}
& > span {
font-size: 32px;
color: #333333;
display: inline-block;
line-height: 44px;
overflow: hidden;
text-overflow: ellipsis;
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
}
}
& > span {
font-size: 28px;
color: #999999;
}
}
}
.active {
background-color: #F3F6F9;
}
.progress {
margin-top: 8px;
.item {
display: flex;
align-items: center;
position: relative;
min-height: 136px;
margin-bottom: 80px;
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #2266FF;
font-size: 28px;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.right {
width: 100%;
display: flex;
flex-direction: column;
& > .u-row {
margin-left: 40px;
.status {
font-size: 32px;
color: #333333;
}
.date {
font-size: 28px;
color: #999999;
}
.name {
font-size: 28px;
color: #666666;
margin: 8px 0;
}
.note {
font-size: 28px;
color: #343D65;
}
}
}
&:after {
content: "";
width: 4px;
height: 100%;
background-color: #EEEEEE;
position: absolute;
left: 40px;
top: 112px;
}
&:last-child:after {
display: none;
}
}
}
}
.footer {
height: 112px;
width: 100%;
position: fixed;
left: 0;
bottom: 0;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
& > div {
color: #333333;
}
& > div:first-child {
width: 50%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
& > div:last-child {
width: 50%;
height: 100%;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
background-color: #1365DD;
}
& > label {
display: flex;
align-items: center;
justify-content: center;
background-color: #1365DD;
}
}
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<div class="document-flow">
<ai-top-fixed>
<header class="pad">
<u-search placeholder="请输入公文名称" v-model="documentName" @clear="documentName='',getList()" @search="getList" clearabled :show-action="false" height="64"></u-search>
</header>
</ai-top-fixed>
<div class="list pad" v-if="list.length">
<div class="card" v-for="(item,index) in list" :key="index" @click="handleClick(item)">
<u-row>
<em v-if="item.redStatus==0"></em>
<span>{{item.documentName}}</span>
</u-row>
<u-gap height="16"></u-gap>
<u-row>
<label>公文类型</label>
<text style="color: #1365DD;">{{$dict.getLabel("officialDocumentName",item.documentType)}}</text>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<label>登记人</label>
<text>{{item.createUserName}}</text>
</u-row>
<u-gap height="8"></u-gap>
<u-row>
<label>登记日期</label>
<text>{{item.createTime}}</text>
</u-row>
<img :src=" $cdn + tag(item.readType)" alt="">
</div>
</div>
<AiEmpty v-else></AiEmpty>
<u-loadmore :status="status" v-if="list.length"/>
</div>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
import AiEmpty from "../../components/AiEmpty/AiEmpty";
export default {
name: "documentFlow",
components: {AiTopFixed,AiEmpty},
data() {
return {
documentName: "",
current: 1,
list: [],
status: "加载更多"
}
},
onLoad(){
this.$dict.load("officialDocumentName")
},
methods: {
tag(status){
return {
"0": 'common/1ps.png',
"1": 'common/2cy.png'
}[status]
},
getList() {
this.$http.post("/app/appofficialdocumentinfo/appList", null, {
params:{
documentName: this.documentName,
size: 10,
current: this.current
}
}).then(res => {
if (res && res.data) {
if (this.current > 1 && this.current > res.data.pages) {
this.status = "已经到底啦"
}
this.list = this.current > 1 ? [...this.list, ...res.data.records] : res.data.records
}
})
},
handleClick({id}) {
uni.navigateTo({
url: "/pages/documentFlow/components/detail?id=" + id
})
}
},
onShow(){
this.getList()
},
onReachBottom() {
this.current = this.current + 1;
this.getList()
},
}
</script>
<style lang="scss" scoped>
.document-flow {
min-height: 100%;
background: #F5F5F5;
::v-deep .content {
padding: 0;
}
header {
height: 112px;
background-color: #FFFFFF;
display: flex;
align-items: center;
}
.list {
margin: 32px 0;
.card {
background: #FFFFFF;
border-radius: 8px;
box-sizing: border-box;
padding: 32px;
position: relative;
margin-bottom: 32px;
.u-row{
flex-wrap: nowrap !important;
}
em {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #FF4466;
font-style: normal;
margin-right: 8px;
flex-shrink: 0;
}
span {
font-size: 32px;
font-weight: 600;
color: #333333;
}
label {
font-size: 30px;
color: #999999;
}
text {
font-size: 30px;
color: #343D65;
}
img {
width: 160px;
height: 160px;
position: absolute;
right: 0;
bottom: 0;
}
}
}
.pad {
box-sizing: border-box;
padding: 32px 32px 0 32px;
}
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<section class="areaSelector">
<ai-search-popup mode="bottom" ref="areaSelector">
<template #btn>
<div class="areaSelector">
<span v-for="area in fullArea" :key="area.id" v-text="area.name"
:class="{current:area.id==areaId}" @tap="index=area.id,getChildAreas(area.id)"/>
</div>
</template>
<div class="areaSelector">
<span v-for="area in fullArea" :key="area.id" v-text="area.name"
:class="{current:area.id==index}"
@click="index=area.id,getChildAreas(area.id)"/>
</div>
<div class="pendingItem" flex v-for="op in list" :key="op.id" @tap="handleSelect(op)">
<div class="fill" :class="{self:index==op.id}" v-html="op.name"/>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</ai-search-popup>
</section>
</template>
<script>
import AiSearchPopup from "../../../components/AiSearchPopup";
import AiCell from "../../../components/AiCell";
import {mapState} from "vuex";
export default {
name: "areaSelector",
components: {AiCell, AiSearchPopup},
props: {
areaId: {default: ""}
},
computed: {
...mapState(['user']),
dataRange() {
let rules = [10, 8, 6, 3, 0], level = 2
rules.some((e, i) => {
let reg = new RegExp(`0{${e}}`, 'g')
if (reg.test(this.user.areaId)) {
return level = i
}
})
return level
}
},
data() {
return {
fullArea: [],
index: "",
list: []
}
},
watch: {
areaId(v) {
v && this.getFullArea()
}
},
methods: {
getFullArea() {
let {areaId} = this
return areaId && this.$http.post("/admin/area/getAllParentAreaId", null, {
params: {areaId}
}).then(res => {
if (res?.data) {
this.fullArea = res.data.reverse().slice(this.dataRange)
}
})
},
getChildAreas(id) {
id && this.$http.post("/admin/area/queryAreaByParentId", null, {
params: {id}
}).then(res => {
if (res?.data) {
this.list = res.data
let self = this.fullArea.find(e => e.id == this.index)
this.list.unshift(self)
}
})
},
handleSelect(op) {
this.$emit('select', op)
this.$refs.areaSelector?.handleSelect()
}
},
created() {
this.index = this.areaId
this.getFullArea()
}
}
</script>
<style lang="scss" scoped>
.areaSelector {
::v-deep .AiSearchPopup {
.areaSelector {
display: flex;
align-items: center;
span {
cursor: pointer;
&:first-of-type:before {
content: "";
padding: 0;
}
&:before {
color: #333;
content: "/";
padding: 0 16px;
}
}
.current {
color: #3F8DF5;
}
}
.u-drawer-content {
position: fixed;
.areaSelector {
padding: 0 16px;
box-sizing: border-box;
border-bottom: 16px solid #f5f5f5;
span {
line-height: 100px;
}
}
}
.pendingItem {
margin-left: 32px;
padding-right: 32px;
height: 104px;
border-bottom: 1px solid #ddd;
.self {
font-weight: bold;
}
}
}
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<section class="makeCalls">
<div v-if="$slots.default" @tap="calls=true">
<slot/>
</div>
<div v-else flex class="column" @tap="calls=true">
<img :src="`${$cdn}guardianship/dh.png`"/>
<span v-html="label"/>
</div>
<u-popup v-model="calls" mode="bottom">
<div flex class="column option" v-for="item in list" @click="handleCall(item.guardianPhone)">
{{ [item.guardianName, item.guardianPhone].join(' ') }}
</div>
<div class="option" @tap="calls=false">取消</div>
</u-popup>
</section>
</template>
<script>
export default {
name: "makeCalls",
props: {
label: {default: "联系ta"},
list: {default: () => []}
},
data() {
return {
calls: false
}
},
methods: {
handleCall(phone) {
location.href = "tel:" + phone
},
}
}
</script>
<style lang="scss" scoped>
.makeCalls {
img {
width: 40px;
height: 40px;
}
::v-deep span {
margin-left: 0;
color: #999;
font-size: 20px;
p {
color: #3D94FB;
}
}
::v-deep .u-drawer {
text-align: center;
.uni-scroll-view-content{
max-height: 672px;
}
.option {
font-size: 32px;
cursor: pointer;
line-height: 112px;
border-bottom: 1px solid #D8DDE6;
&:first-of-type {
border-radius: 16px 16px 0 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<section class="openMap">
<div flex class="column" shrink @tap="handleOpenMap">
<img :src="`${$cdn}guardianship/seat.png`"/>
<span v-text="'地图/导航'"/>
</div>
</section>
</template>
<script>
export default {
name: "openMap",
props: {
data: {default: () => ({})}
},
methods: {
handleOpenMap() {
let {lng, lat, gpsDesc} = this.data
location.href = `https://uri.amap.com/marker?callnative=1&position=${[lng, lat].toString()}&name=${gpsDesc}`
}
}
}
</script>
<style lang="scss" scoped>
.openMap {
flex-shrink: 0;
img {
width: 40px;
height: 40px;
}
span {
margin-left: 0;
color: #999;
font-size: 20px;
}
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<section class="earlyWarning">
<ai-top-fixed>
<u-search v-model="search.name" placeholder="请输入姓名" :show-action="false"
search-icon-color="#ccc" placeholder-color="#999"
@change="page.current=1,getList()"/>
<div flex>
<ai-date class="fill" placeholder="日期选择" mode="range" @change="handleDateSearch"/>
<ai-select class="fill" dict="intelligentGuardianshipItem2" @data="handleTypeSearch">
<div>{{ $dict.getLabel('intelligentGuardianshipItem2', search.item) || '全部预警' }}</div>
<i class="iconfont iconfont-iconArrow_Down"/>
</ai-select>
</div>
</ai-top-fixed>
<div class="card" v-for="row in list" :key="row.id" @tap="handleShow(row)">
<div class="header" flex>
<img :src="top.cdn(typeIcons[row.item])"/>
<b v-text="row.desc"/>
</div>
<div class="wrapper">
<div class="start" flex>
<span v-text="`上报时间:`"/>
<div v-text="row.createTime"/>
</div>
<div class="start" flex>
<span v-text="`上报地点:`"/>
<div v-text="row.gpsDesc"/>
</div>
</div>
</div>
</section>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
import {mapState} from "vuex";
import AiDate from "../../components/AiDate";
import AiSelect from "../../components/AiSelect";
export default {
name: "earlyWarning",
components: {AiSelect, AiDate, AiTopFixed},
inject: ['top'],
computed: {
...mapState(['user']),
typeIcons() {
return {
0: "icon4",
1: "icon2",
2: "icon5",
3: "icon6",
4: "icon3",
5: "icon1",
}
}
},
data() {
return {
search: {name: "", createTimeRange: ",", item: ""},
areaId: "",
page: {current: 1, size: 10, total: 0},
list: []
}
},
methods: {
getList() {
let {areaId} = this
this.$http.post("/app/appintelligentguardianshipalarm/list", null, {
params: {areaId, ...this.search, ...this.page}
}).then(res => {
if (res?.data) {
let data = res.data.records.map(e => {
return {...e, desc: [e.name, this.$dict.getLabel('intelligentGuardianshipItem2', e.item)].join('的')}
})
if (this.page.current > 1) {
this.list = [...this.list, ...data]
} else this.list = data
this.page.total = res.data.total
}
})
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getList()
}
},
handleDateSearch(v) {
let {startDate: start, endDate: end} = v
start = this.$dateFormat(start)
end = this.$dateFormat(end)
this.search.createTimeRange = [start, end || start].toString()
this.page.current = 1
this.getList()
},
handleTypeSearch(v) {
this.search.item = v?.[0]?.value
this.page.current = 1
this.getList()
},
handleShow(row) {
uni.navigateTo({url: `./warningDetail?id=${row.id}`})
}
},
created() {
this.areaId = JSON.parse(JSON.stringify(this.user.areaId))
this.getList()
}
}
</script>
<style lang="scss" scoped>
.earlyWarning {
padding-bottom: 130px;
background: #f5f5f5;
::v-deep .AiDate > div {
justify-content: center;
}
::v-deep .u-drawer-content {
padding-bottom: 100px;
}
::v-deep .display {
justify-content: center;
}
::v-deep .iconfont-iconArrow_Down {
margin-left: 4px;
font-size: 32px;
color: inherit;
}
::v-deep .card {
margin: 32px 32px 0;
background: #FFFFFF;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
border-radius: 8px;
.header {
padding: 0 32px;
height: 104px;
border-bottom: 2px solid #EFEFF4;
font-size: 36px;
img {
width: 64px;
height: 64px;
margin-right: 16px;
}
}
.wrapper {
color: #343D65;
font-size: 30px;
margin-bottom: 8px;
padding: 18px 32px;
span {
white-space: nowrap;
flex-shrink: 0;
color: #999;
margin-right: 20px;
}
}
}
}
</style>

View File

@@ -0,0 +1,461 @@
<template>
<section class="gsLocation">
<ai-map class="fill" :map.sync="amap" :lib.sync="mapLib"/>
<div class="searchZone">
<u-search v-model="search" placeholder="请输入姓名" shape="square" bg-color="#fff"
:show-action="false" @search="getList()"/>
<div class="searchResult" v-if="searchResult">
<div class="item" v-for="row in list" :key="row.id" @tap="handleSelect(row)">
<img :src="cdn(row.onlineStatus==1?'zxtx':'lxtx')"/>
<div flex class="column fill">
<b v-html="searchName(row.name)"/>
<div v-text="row.gpsDesc"/>
</div>
</div>
</div>
<u-popup v-model="popup" mode="bottom" :mask="false">
<div class="headerIcon" flex @touchstart="handleTouchStart" @touchmove="handleTouchmoveClose"/>
<div class="selectedInfo">
<div class="header" flex>
<img :src="`${$cdn}guardianship/tx.png`" @tap="handleShowDetail(selected)"/>
<b v-text="selected.name" @tap="handleShowDetail(selected)"/>
<div v-if="selected.abnormalStatus==1" class="abnormal" @tap="handleShowDetail(selected)">异常</div>
<u-icon name="arrow-right" color="#ddd" class="fill" @tap="handleShowDetail(selected)"/>
<make-calls :list="phoneList"/>
</div>
<div flex class="spb wrap">
<div class="detail" v-for="(op,i) in quotas" :key="i" flex>
<img :src="op.icon"/>
<div class="fill" v-text="op.label"/>
<div :class="{abnormal:op.abnormal}" v-text="op.value"/>
</div>
</div>
</div>
<div class="navigation">
<div class="content" flex>
<div flex class="spb wrap">
<div class="fill" v-text="selected.gpsDesc"/>
<span>最后更新{{ selected.lastUpdateTime }}</span>
<div class="battery" flex>
<img :src="batteryIcon"/>
<div v-text="`剩余${selected.electricQuantity}%`"/>
</div>
</div>
<open-map :data="selected"/>
</div>
</div>
</u-popup>
</div>
</section>
</template>
<script>
import AiSearchPopup from "../../components/AiSearchPopup";
import {mapState} from "vuex";
import UPopup from "../../uview/components/u-popup/u-popup";
import MakeCalls from "./component/makeCalls";
import OpenMap from "./component/openMap";
import AiMap from "../../components/AiMap";
export default {
name: "gsLocation",
components: {AiMap, OpenMap, MakeCalls, UPopup, AiSearchPopup},
computed: {
...mapState(['user']),
markers() {
return this.list.filter(e => e.lng).map(e => {
let abnormal = 'offline'
if (e.onlineStatus == 1) {
switch (e.abnormalStatus) {
case '1':
abnormal = 'warning';
break;
case '2':
abnormal = 'abnormal';
break;
default:
abnormal = ''
}
}
return new this.mapLib.Marker({
position: new this.mapLib.LngLat(e.lng, e.lat),
anchor: 'bottom-center',
content: `<div class="marker ${abnormal}">${e.name}</div>`,
extData: e,
topWhenClick: true
}).on('click', () => {
this.handleSelect(e)
})
})
},
quotas() {
let quota = [
{key: "0", icon: "1"},
{key: "1", icon: "2"},
{key: "2", icon: "3"},
{key: "3", icon: "4"},
]
return quota.map(e => {
let item = this.detail.find(d => d.item == e.key)
let label = this.$dict.getLabel('intelligentGuardianshipItem', e.key)
return {
label, icon: this.cdn(e.icon),
value: item?.itemValue || "-",
abnormal: item?.abnormalStatus == 1
}
})
},
batteryIcon() {
return this.cdn(this.selected.electricQuantity == 100 ? 'dcm' : 'dcq')
},
phoneList() {
let {name: guardianName, phone: guardianPhone} = this.selected
return [{guardianName, guardianPhone}, ...(this.selected.guardians || [])]
},
},
data() {
return {
mapLib: null,
amap: null,
search: "",
selected: {},
list: [],
popup: false,//被监护人信息弹窗
detail: [],
moveDistance: 0,
searchResult: false //搜索下拉弹窗
}
},
watch: {
amap(v) {
v && this.getList().then(() => {
this.amap?.add(this.markers)
this.getMapArea()
})
}
},
methods: {
cdn(icon) {
return `${this.$cdn}guardianship/${icon}.png`
},
getMapArea() {
if (this.mapLib) {
new this.mapLib.DistrictSearch({
subdistrict: 0, //获取边界不需要返回下级行政区
extensions: 'all', //返回行政区边界坐标组等具体信息
level: 'district' //查询行政级别为 市
}).search(this.user.areaId.substring(0, 6), (status, result) => {
let bounds = result?.districtList?.[0]?.boundaries;
let polygons = []
bounds?.forEach(path => polygons.push(new this.mapLib.Polygon({
strokeWeight: 1,
path,
strokeStyle: 'dashed',
fillOpacity: 0.1,
fillColor: '#80d8ff',
strokeColor: '#0091ea'
})))
this.amap.add(polygons)
this.amap.setFitView();//视口自适应
})
}
},
getList() {
this.searchResult = !!this.search
return this.$http.post("/app/appintelligentguardianshipdevice/list", null, {
params: {name: this.search, size: 999, areaId: this.user.areaId}
}).then(res => {
if (res?.data) {
this.list = res.data.records
}
})
},
getDetail(deviceId) {
this.$http.post("/app/appintelligentguardianshipdevice/queryMonitorList", null, {
params: {type: 1, deviceId}
}).then(res => {
if (res?.data) {
this.detail = res.data.records
}
})
this.$http.post("/app/appintelligentguardianshipdevice/queryDetailById", null, {
params: {id: deviceId}
}).then(res => {
if (res?.data) {
this.selected = {...this.selected, ...res.data}
}
})
},
searchName(name) {
return name?.replace(this.search, `<span>${this.search}</span>`)
},
handleSelect(e) {
this.amap.setCenter(new this.mapLib.LngLat(e.lng, e.lat))
this.selected = e
this.popup = true
this.searchResult = false
this.getDetail(e.id)
},
handleShowDetail(user) {
uni.navigateTo({url: `./userDetail?id=${user.id}`})
},
handleTouchmoveClose(e) {
if (e.touches?.[0]?.clientY > this.moveDistance + 10) {
this.popup = false
}
},
handleTouchStart(e) {
this.moveDistance = e.touches?.[0]?.clientY
}
}
}
</script>
<style lang="scss" scoped>
.gsLocation {
height: 100%;
a {
color: inherit;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
height: 112px;
border-top: 1px solid #d8dde6;
&:first-of-type {
border-top: none;
}
}
.headerIcon {
width: 100%;
height: 60px;
justify-content: center;
&:before {
content: " ";
display: block;
width: 64px;
height: 10px;
background: #CCCCCC;
border-radius: 5px;
}
}
::v-deep .makeCalls {
.option:last-of-type {
margin-bottom: 120px;
}
}
::v-deep .selectedInfo {
width: 100%;
border-radius: 20px 20px 0 0;
padding: 0 32px;
background: #fff;
overflow: hidden;
box-sizing: border-box;
.header {
margin-bottom: 40px;
font-size: 40px;
& > img {
width: 82px;
height: 82px;
margin-right: 16px;
}
.abnormal {
color: #FF4466;
font-size: 24px;
padding: 12px;
background: rgba(#EC4461, .1);
border-radius: 8px;
margin: 0 16px;
}
span {
margin-left: 0;
color: #999;
font-size: 20px;
}
}
.detail {
width: 318px;
height: 84px;
background: #F4F5F6;
border-radius: 8px;
padding: 0 24px;
box-sizing: border-box;
margin-bottom: 36px;
img {
margin-right: 16px;
width: 56px;
height: 56px;
}
.abnormal {
color: #FF4466;
}
}
}
.navigation {
padding-bottom: 100px;
.content {
padding: 32px 40px;
font-size: 26px;
& > .spb {
margin-right: 40px;
}
.fill {
min-width: 100%;
flex-shrink: 0;
margin-bottom: 10px;
}
span {
margin-left: 0;
color: #999;
font-size: 22px;
}
.battery > img {
margin-right: 14px;
}
}
&:before {
content: " ";
display: block;
width: 100%;
height: 8px;
background: #F4F5F6;
}
}
::v-deep .searchZone {
position: absolute;
top: 0;
z-index: 2;
width: 100%;
background: transparent;
padding: 24px 16px;
box-sizing: border-box;
.u-search {
box-shadow: 0 4px 8px 0 rgba(192, 185, 185, 0.5);
}
.searchResult {
margin-top: 16px;
padding: 0 28px;
background: #fff;
.item {
font-size: 24px;
display: flex;
line-height: 36px;
padding: 24px 0;
border-bottom: 3px solid #DEDFE1;
&:last-of-type {
border-bottom: none;
}
img {
width: 36px;
height: 36px;
margin-right: 16px;
}
& > .fill {
align-items: unset;
b > span {
color: #1365DD;
}
& > div {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
::v-deep .marker {
color: #fff;
font-size: 30px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 32px;
height: 56px;
white-space: nowrap;
background: #5088FF;
border-color: #5088FF;
border-radius: 52px;
position: relative;
&:after {
position: absolute;
display: block;
content: " ";
bottom: -12px;
left: 50%;
transform: translateX(-50%);
border: 12px solid transparent;
border-bottom: none;
height: 0;
width: 0;
border-top-color: inherit;
}
&.offline {
background: #C4CAD4;
border-color: #C4CAD4;
}
&.warning {
background: #FFAA44;
border-color: #FFAA44;
}
&.abnormal {
background: #F46159;
border-color: #F46159;
&:before {
position: absolute;
z-index: -1;
bottom: -40px;
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #F46159;
transform: translate(-50%, -50%);
animation: mapWarn 1s ease-out 0s infinite;
content: " ";
}
}
}
.AiMap {
width: 100%;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<section class="guardianship">
<component ref="currentTab" :is="currentTab.comp"/>
<ai-tabbar :active.sync="active" :list="bottomBar"/>
</section>
</template>
<script>
import AiLoading from "../../components/AiLoading";
import GsLocation from "./gsLocation";
import AiTabbar from "../../components/AiTabbar";
import WardList from "./wardList";
import EarlyWarning from "./earlyWarning";
export default {
name: "guardianship",
components: {AiTabbar, AiLoading},
provide() {
return {
top: this
}
},
computed: {
bottomBar() {
return [
{text: "定位", iconPath: "bardwn", selectedIconPath: "bardwh", comp: GsLocation},
{text: "人员", iconPath: "barryn", selectedIconPath: "barryh", comp: WardList},
{text: "预警", iconPath: "baryjn", selectedIconPath: "baryjh", comp: EarlyWarning},
].map(e => ({
...e,
iconPath: this.cdn(e.iconPath),
selectedIconPath: this.cdn(e.selectedIconPath)
}))
},
currentTab() {
return this.bottomBar?.[this.active] || {}
}
},
methods: {
cdn(icon) {
return `${this.$cdn}guardianship/${icon}.png`
},
},
data() {
return {
active: 0
}
},
created() {
this.$dict.load("intelligentGuardianshipItem", 'intelligentGuardianshipItem2', 'intelligentGuardianshipAbnormalStatus')
},
onReachBottom() {
if (typeof this.$refs?.currentTab?.reachBottom == 'function') this.$refs?.currentTab.reachBottom()
}
}
</script>
<style lang="scss" scoped>
.guardianship {
position: absolute;
width: 100%;
top: 0;
bottom: 0;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<section class="historyList">
<div flex>
<b class="header" v-text="itemLabel"/>
</div>
<div v-for="row in list" :key="row.id" flex class="spb row">
<div :class="{abnormal:row.abnormalStatus==1}" v-text="row.itemValue"/>
<span v-text="row.sampleTime"/>
</div>
<ai-back/>
</section>
</template>
<script>
import AiBack from "../../components/AiBack";
export default {
name: "historyList",
components: {AiBack},
computed: {
itemLabel() {
return '历史'+this.$dict.getLabel('intelligentGuardianshipItem', this.$route.query.type)+`(${this.$dict.getLabel('intelligentGuardianshipItemUnit',this.$route.query.type)})`
}
},
data() {
return {
list: []
}
},
methods: {
getHistory() {
let {type: item, id: deviceId} = this.$route.query
this.$http.post("/app/appintelligentguardianshipdevice/queryMonitorList", null, {
params: {deviceId, size: 999, item}
}).then(res => {
if (res?.data) {
this.list = res.data.records
}
})
}
},
created() {
this.$dict.load("intelligentGuardianshipItem",'intelligentGuardianshipItemUnit')
this.getHistory()
}
}
</script>
<style lang="scss" scoped>
.historyList {
font-size: 30px;
& > div {
padding: 0 32px;
border-bottom: 1px solid #ddd;
height: 96px;
background: #FFFFFF;
}
.header {
font-size: 32px;
}
.row {
color: #5AAD6A;
.abnormal {
color: #CD413A;
}
& > span {
color: #999;
}
}
}
</style>

View File

@@ -0,0 +1,279 @@
<template>
<section class="userDetail">
<div class="selectedInfo">
<div class="header" flex>
<img :src="`${$cdn}guardianship/tx.png`"/>
<b v-text="detail.name"/>
<div v-if="detail.abnormalStatus==1" class="abnormal">异常</div>
<div class="fill"/>
<make-calls :list="phoneList" :label="`<p>拨打电话</p>`"/>
</div>
<div class="content">
<ai-cell label="所属地区">{{ detail.areaName }}</ai-cell>
<ai-cell label="联系电话">{{ detail.phone }}</ai-cell>
<ai-cell label="性别">{{ $dict.getLabel('sex', detail.sex) }}</ai-cell>
<ai-cell label="年龄">{{ $calcAge(detail.idNumber) }}</ai-cell>
</div>
</div>
<div class="card">
<div flex class="spb header">
<b v-text="`设备状况`"/>
<span class="onlineStatus" v-html="detail.onlineStatus==1?'设备在线':'<p>设备离线</p>'"/>
</div>
<div flex class="spb wrap quotas">
<div class="quota" v-for="(op,i) in quotas" :key="i" flex @tap="handleShowHistory(op)">
<img :src="op.icon"/>
<div class="fill" v-text="op.label"/>
<div :class="{abnormal:op.abnormal}" v-text="op.value"/>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</div>
<div class="navigation">
<div class="content spb" flex>
<div flex class="spb wrap">
<div class="fill" v-text="detail.gpsDesc"/>
<span>最后更新{{ detail.lastUpdateTime }}</span>
<div class="battery" flex>
<img :src="batteryIcon"/>
<div v-text="`剩余${detail.electricQuantity||0}%`"/>
</div>
</div>
<open-map :data="detail"/>
</div>
</div>
</div>
<div class="card">
<div flex class="spb header">
<b v-text="`监护人信息`"/>
</div>
<div flex class="spb guardian" v-for="row in detail.guardians" :key="row.id">
<span v-text="row.guardianName"/>
<div v-text="row.guardianPhone"/>
</div>
</div>
<ai-back/>
</section>
</template>
<script>
import MakeCalls from "./component/makeCalls";
import OpenMap from "./component/openMap";
import AiCell from "../../components/AiCell";
import AiBack from "../../components/AiBack";
export default {
name: "userDetail",
components: {AiBack, AiCell, OpenMap, MakeCalls},
computed: {
batteryIcon() {
return this.cdn(this.detail.electricQuantity == 100 ? 'dcm' : 'dcq')
},
quotas() {
let quota = [
{key: "0", icon: "1"},
{key: "1", icon: "2"},
{key: "2", icon: "3"},
{key: "3", icon: "4"},
]
return quota.map(e => {
let item = this.detail.quota?.find(d => d.item == e.key)
let label = this.$dict.getLabel('intelligentGuardianshipItem', e.key)
return {
label, icon: this.cdn(e.icon), type: e.key,
value: item?.itemValue || "-",
abnormal: item?.abnormalStatus == 1
}
})
},
phoneList() {
let {name: guardianName, phone: guardianPhone} = this.detail
return [{guardianName, guardianPhone}, ...this.detail.guardians]
}
},
data() {
return {
detail: {
guardians: []
}
}
},
methods: {
cdn(icon) {
return `${this.$cdn}guardianship/${icon}.png`
},
getDetail(deviceId) {
this.$http.post("/app/appintelligentguardianshipdevice/queryMonitorList", null, {
params: {type: 1, deviceId}
}).then(res => {
if (res?.data) {
this.$set(this.detail, 'quota', res.data.records)
}
})
this.$http.post("/app/appintelligentguardianshipdevice/queryDetailById", null, {
params: {id: deviceId}
}).then(res => {
if (res?.data) {
this.detail = {...this.detail, ...res.data}
}
})
},
handleShowHistory(item) {
uni.navigateTo({url: `./historyList?type=${item.type}&id=${this.$route.query.id}`})
}
},
created() {
this.$dict.load("intelligentGuardianshipItem", 'sex')
this.getDetail(this.$route.query.id)
}
}
</script>
<style lang="scss" scoped>
.userDetail {
padding-bottom: 60px;
& > * {
margin-bottom: 16px;
}
.card {
background: #fff;
font-size: 32px;
box-shadow: 0 1px 1px 0 rgba(221, 221, 221, 1);
.header {
height: 96px;
background: #FFFFFF;
border-bottom: 1px solid #ddd;
}
& > .spb {
padding: 0 32px;
}
}
::v-deep .selectedInfo {
width: 100%;
background: #fff;
overflow: hidden;
box-sizing: border-box;
.header {
height: 136px;
font-size: 40px;
padding: 0 32px;
border-bottom: 1px solid #D8DDE6;
& > img {
width: 82px;
height: 82px;
margin-right: 16px;
}
.abnormal {
color: #FF4466;
font-size: 24px;
padding: 12px;
background: rgba(#EC4461, .1);
border-radius: 8px;
margin-left: 16px;
}
span {
margin-left: 0;
color: #999;
font-size: 20px;
}
}
& > .content {
padding: 32px;
font-size: 30px;
::v-deep .AiCell {
padding: 0;
min-height: 58px;
}
}
}
.quotas {
margin-top: 24px;
.quota {
cursor: pointer;
width: 318px;
height: 84px;
background: #F4F5F6;
border-radius: 8px;
padding: 0 8px 0 24px;
box-sizing: border-box;
margin-bottom: 36px;
font-size: 28px;
img {
margin-right: 16px;
width: 56px;
height: 56px;
}
.abnormal {
color: #FF4466;
}
}
}
.navigation {
.content {
padding: 10px 40px 32px;
font-size: 26px;
& > .spb {
margin-right: 40px;
}
.fill {
min-width: 100%;
flex-shrink: 0;
margin-bottom: 10px;
}
span {
margin-left: 0;
color: #999;
font-size: 22px;
}
.battery > img {
margin-left: 32px;
margin-right: 14px;
}
}
}
::v-deep .onlineStatus {
font-size: 30px;
color: #5AAD6A;
p {
color: #F5A319;
}
}
.guardian {
height: 96px;
border-bottom: 1px solid #ddd;
font-size: 28px;
color: #222;
&:last-of-type {
border-bottom: none;
}
span {
color: #666666;
}
}
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<section class="wardList">
<ai-top-fixed>
<u-search v-model="search" placeholder="请输入姓名" :show-action="false"
search-icon-color="#ccc" placeholder-color="#999"
@change="page.current=1,getUser()"/>
<area-selector :areaId="areaId" @select="handleSelectArea"/>
</ai-top-fixed>
<div class="userList">
<div v-for="row in list" :key="row.id" flex class="row" @tap="handleShowDetail(row)">
<img :src="top.cdn(row.onlineStatus==1?'zxtx':'lxtx')"/>
<b class="fill" v-text="row.name"/>
<div class="status" :style="{color:$dict.getColor('intelligentGuardianshipAbnormalStatus',row.abnormalStatus)}"
v-text="$dict.getLabel('intelligentGuardianshipAbnormalStatus',row.abnormalStatus)"/>
<u-icon name="arrow-right" color="#ddd"/>
</div>
</div>
</section>
</template>
<script>
import AiTopFixed from "../../components/AiTopFixed";
import {mapState} from "vuex";
import AreaSelector from "./component/areaSelector";
export default {
name: "wardList",
components: {AreaSelector, AiTopFixed},
computed: {
...mapState(['user']),
},
inject: ['top'],
data() {
return {
search: "",
areaId: "",
page: {current: 1, size: 20, total: 0},
list: []
}
},
methods: {
getUser() {
let {areaId, search: name} = this
this.$http.post("/app/appintelligentguardianshipdevice/list", null, {
params: {areaId, name, ...this.page}
}).then(res => {
if (res?.data) {
let data = res.data.records.reverse()
if (this.page.current > 1) {
this.list = [...this.list, ...data]
} else this.list = data
this.page.total = res.data.total
}
})
},
handleSelectArea({id}) {
this.areaId = id
this.getUser()
},
reachBottom() {
if (this.page.total > this.list.length) {
this.page.current++
this.getUser()
}
},
handleShowDetail(user) {
uni.navigateTo({url: `./userDetail?id=${user.id}`})
}
},
created() {
this.areaId = JSON.parse(JSON.stringify(this.user.areaId))
this.getUser()
}
}
</script>
<style lang="scss" scoped>
.wardList {
background: #f5f5f5;
padding-bottom: 130px;
font-size: 30px;
::v-deep .userList {
margin-top: 16px;
background: #fff;
.row {
font-size: 36px;
margin-left: 32px;
padding-right: 32px;
height: 104px;
border-bottom: 1px solid #ddd;
img {
width: 72px;
height: 72px;
margin-right: 38px;
}
.status {
margin-right: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<section class="warningDetail">
<div flex class="header">
<b v-text="detail.name"/>
<div>
{{ $dict.getLabel('intelligentGuardianshipItem2', detail.item) }}
{{ detail.itemValue }}
</div>
</div>
<ai-map class="fill" :map.sync="amap" :lib.sync="mapLib"/>
<div class="navigation">
<div class="content spb" flex>
<div flex class="spb wrap">
<div class="fill" v-text="detail.gpsDesc"/>
<span>最后更新{{ detail.createTime }}</span>
</div>
<open-map :data="detail"/>
</div>
</div>
<make-calls :list="phoneList">
<div class="bottomBtn">拨打电话</div>
</make-calls>
<ai-back/>
</section>
</template>
<script>
import OpenMap from "./component/openMap";
import MakeCalls from "./component/makeCalls";
import AiBack from "../../components/AiBack";
import AiMap from "../../components/AiMap";
export default {
name: "warningDetail",
components: {AiMap, AiBack, MakeCalls, OpenMap},
computed: {
phoneList() {
let {name: guardianName, phone: guardianPhone} = this.detail
return [{guardianName, guardianPhone}, ...this.detail.guardians]
}
},
data() {
return {
detail: {guardians: []},
mapLib: null,
amap: null,
}
},
methods: {
getDetail(id) {
return this.$http.post("/app/appintelligentguardianshipalarm/queryDetailById", null, {
params: {id},
withoutToken: true
}).then(res => {
if (res?.data) {
this.detail = res.data
}
})
},
initMap() {
if (this.mapLib) {
let pos = new this.mapLib.LngLat(this.detail.lng, this.detail.lat)
this.amap.add(new this.mapLib.Marker({
position: pos,
anchor: 'bottom-center',
content: `<div class="marker">${this.detail.name}</div>`,
}))
this.amap.setFitView()
}
}
},
watch: {
mapLib(v) {
this.detail.id && v && this.initMap()
}
},
created() {
this.$dict.load("intelligentGuardianshipItem",'intelligentGuardianshipItem2', 'sex')
},
mounted() {
this.getDetail(this.$route.query.id).then(() => this.initMap())
}
}
</script>
<style lang="scss" scoped>
.warningDetail {
padding: 48px 48px 112px;
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: absolute;
.header {
font-size: 40px;
font-weight: bold;
color: #333333;
justify-content: center;
margin-bottom: 48px;
& > div {
color: #EC4461;
}
}
.navigation {
.content {
padding: 10px 0 32px;
font-size: 28px;
color: #555;
& > .spb {
margin-right: 40px;
}
.fill {
min-width: 100%;
flex-shrink: 0;
margin-bottom: 10px;
}
span {
margin-left: 0;
color: #999;
font-size: 22px;
}
}
}
.bottomBtn {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
background: #1365DD;
line-height: 112px;
}
.AiMap {
margin-bottom: 72px;
}
::v-deep .marker {
border-radius: 52px;
background: #F46159;
color: #fff;
font-size: 22px;
padding: 4px 16px;
white-space: nowrap;
position: relative;
&:before {
position: absolute;
left: calc(50% - 30px);
bottom: -34px;
z-index: -1;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #F46159;
animation: mapWarn 1s ease-out 0s infinite;
content: " ";
}
&:after {
position: absolute;
display: block;
content: " ";
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border: 8px solid transparent;
border-bottom: none;
border-top-color: #F46159;
height: 0;
width: 0;
}
}
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<div class="interviewDetail">
<template v-if="isEdit">
<u-form ref="interviewForm" label-position="top" :rules="rules" :model="form">
<u-form-item label="调查走访事项" prop="title" required>
<u-input v-model="form.title" placeholder="请输入最多30字" maxlength="30"/>
</u-form-item>
<u-form-item label="调查走访内容" prop="content">
<ai-textarea v-model="form.content" placeholder="请输入最多500字" :maxlength="500"/>
</u-form-item>
<u-form-item label="图片最多9张">
<ai-uploader multiple :limit="9" :def.sync="form.fileList" action="/admin/file/add2"/>
</u-form-item>
</u-form>
<div bottom>
<u-button type="primary" @tap="submitForm">保存</u-button>
</div>
</template>
<template v-else>
<div class="headerPane">
<b>{{ form.title }}</b>
<div>记录时间{{ form.createTime }}</div>
</div>
<div class="contentPane">
<div v-html="form.content"/>
<div flex class="wrap">
<ai-image v-for="(op,i) in form.fileList" :src="op.accessUrl" preview :key="i"/>
</div>
</div>
</template>
<ai-back/>
</div>
</template>
<script>
import UButton from "../../uview/components/u-button/u-button";
import AiUploader from "../../components/AiUploader";
import UInput from "../../uview/components/u-input/u-input";
import AiImage from "../../components/AiImage";
import AiTextarea from "../../components/AiTextarea";
import UFormItem from "../../uview/components/u-form-item/u-form-item";
import AiBack from "../../components/AiBack";
export default {
name: 'interviewDetail',
components: {AiBack, UFormItem, AiTextarea, AiImage, UInput, AiUploader, UButton},
computed: {
isEdit() {
let flag = this.$route.query?.detail != 1
!flag && uni.setNavigationBarTitle({title: "走访详情"})
return flag
},
rules() {
return {
title: [{required: true, message: '请输入 调查走访事项'}],
// content: [{required: true, message: '请输入 调查走访内容'}],
}
}
},
data() {
return {
form: {
fileList: []
}
}
},
created() {
this.searchDetail();
},
methods: {
submitForm() {
this.$refs.interviewForm?.validate(v => {
if (v) {
this.$http.post(`/app/appinterview/add-xcx`, {
...this.form
}).then(res => {
if (res?.code == 0) {
this.$u.toast("提交成功!")
uni.navigateBack()
}
})
}
})
},
searchDetail() {
let {id} = this.$route.query
id && this.$http.post(`/app/appinterview/queryDetailById`, null, {
params: {id}
}).then(res => {
if (res?.data) {
this.form = {...res.data};
}
})
},
}
}
</script>
<style lang="scss" scoped>
.interviewDetail {
background: #F3F6F9;
min-height: 100%;
.u-form {
width: 100%;
height: 100%;
overflow-y: auto;
background-color: #f3f6f9;
position: relative;
padding: 0 0 188px;
box-sizing: border-box;
font-size: 30px;
::v-deep textarea {
width: 100%;
}
::v-deep .u-form-item {
margin-bottom: 16px;
.u-form-item--left__content__label {
font-weight: 400;
}
div[flex] {
width: 100%;
}
}
}
div[bottom] {
z-index: 99;
padding: 0;
height: 112px;
.u-btn {
height: 100%;
border-radius: 0;
}
}
::v-deep .headerPane {
width: 100%;
background: #3975C6;
color: #fff;
padding: 24px 32px 32px;
box-sizing: border-box;
font-size: 28px;
b {
display: block;
font-size: 40px;
line-height: 64px;
letter-spacing: 2px;
margin-bottom: 16px;
}
}
::v-deep .contentPane {
padding: 32px;
width: 100%;
box-sizing: border-box;
font-size: 32px;
font-weight: 400;
color: #666;
line-height: 56px;
.wrap {
margin-top: 32px;
}
.AiImage {
width: 31%;
margin-bottom: 16px;
margin-right: 16px;
image {
width: 100%;
height: 218px;
}
}
}
}
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="interview">
<ai-top-fixed>
<div flex>
<ai-date placeholder="日期选择" mode="range" @change="handleDateSearch"/>
<u-search placeholder="请输入标题" :show-action="false" v-model="search.title" @search="current=1,getList()"/>
</div>
</ai-top-fixed>
<template v-if="list.length>0">
<ai-card v-for="(e,index) in list" :key="index" @click.native="goDetail(e.id,1)">
<template #custom>
<div flex>
<b class="fill">{{ e.title }}</b>
</div>
<div flex v-if="!!e.fileList" class="wrap">
<ai-image v-for="(op,i) in e.fileList.slice(0,3)" :src="op.accessUrl" preview :key="i"/>
</div>
<div class="bottom">{{ e.createTime }}</div>
</template>
<template #menu>
<div class="menu" @tap.stop="goDetail(e.id)">编辑</div>
<div class="menu" @tap.stop="handleDelete(e.id)">删除</div>
</template>
</ai-card>
<u-loadmore :status="loadmore" color="#999" font-size="24"
margin-top="32" margin-bottom="80"/>
</template>
<div class="no-message" v-else>
<image src="https://cdn.cunwuyun.cn/wxAdmin/img/message.png"/>
<p>您还未添加过入户调查走访<br>点击<b>新增按钮</b>试试吧~</p>
</div>
<ai-fixed-btn>
<div class="addBtn iconfont iconfont-iconfangda" @tap="gotoAdd()"/>
</ai-fixed-btn>
</div>
</template>
<script>
import AiSelect from "../../components/AiSelect";
import AiTopFixed from "../../components/AiTopFixed";
import AiCard from "../../components/AiCard";
import AiImage from "../../components/AiImage";
import AiDate from "../../components/AiDate";
import AiFixedBtn from "../../components/AiFixedBtn";
export default {
name: "interview",
components: {AiFixedBtn, AiDate, AiImage, AiCard, AiTopFixed, AiSelect},
data() {
return {
search: {title: ""},
list: [],
current: 1,
pages: 0
}
},
computed: {
loadmore() {
return this.pages <= this.current ? 'loading ' : 'nomore'
}
},
onShow() {
this.current = 1;
this.getList()
},
onReachBottom() {
this.current++;
this.getList()
},
methods: {
getList() {
this.$http.post('/app/appinterview/list-xcx', null, {
params: {
current: this.current,
size: 10,
...this.search
}
}).then(res => {
if (res?.data) {
this.list = this.current > 1 ? [...this.list, ...res.data.records] : res.data.records
this.pages = res.data.pages
}
})
},
goDetail(id, readonly) {
let url = `./detail?id=${id}`
readonly && (url += "&detail=1")
uni.navigateTo({url})
},
gotoAdd() {
uni.navigateTo({url: `./detail`})
},
handleDelete(ids) {
this.$confirm("是否要删除该调查走访").then(() => {
this.$http.post("/app/appinterview/delete", null, {
params: {ids}
}).then(res => {
if (res?.code == 0) {
this.$u.toast("删除成功!")
this.getList()
}
})
})
},
handleDateSearch(v) {
this.search.startTime = v.startDate
this.search.endTime = v.endDate || v.startDate
this.current = 1
this.getList()
}
}
}
</script>
<style lang="scss" scoped>
.interview {
width: 100%;
min-height: 100%;
box-sizing: border-box;
position: absolute;
background: #fff;
.no-message {
margin-top: 140px;
text-align: center;
color: #888;
font-size: 30px;
b {
font-size: 32px;
color: $uni-color-primary;
padding: 0 8px;
}
image {
width: 320px;
height: 240px;
}
}
::v-deep .AiCard {
width: 100%;
min-height: 160px;
background: #FFFFFF;
padding: 32px 32px 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
b {
display: block;
width: 100%;
font-size: 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #333;
margin-right: 60px;
margin-bottom: 20px;
}
.iconfont-iconMore {
color: #666;
position: absolute;
right: 32px;
top: 32px;
}
.bottom {
font-size: 24px;
color: #999999;
padding: 24px 0;
border-bottom: 1px solid rgba(221, 221, 221, .4);
}
.AiImage {
width: 30%;
margin-bottom: 8px;
margin-right: 8px;
image {
width: 100%;
height: 218px;
}
}
}
.addBtn {
width: 96px;
height: 96px;
flex-shrink: 0;
background: $uni-color-primary;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
font-size: 48px;
color: #fff;
border-radius: 50%;
justify-content: center;
align-items: center;
display: flex;
}
.menu {
text-align: center;
line-height: 80px;
width: 192px;
height: 80px;
font-size: 28px;
font-weight: 400;
color: #333333;
}
::v-deep .u-search {
margin-bottom: 0 !important;
padding-left: 146px;
box-shadow: none;
.u-content {
padding-left: 50px;
box-sizing: border-box;
}
}
}
</style>

174
src/pages/loading.vue Normal file
View File

@@ -0,0 +1,174 @@
<template>
<section class="loading">
<!-- <div class="iconfont iconfont-iconWeChat"/>-->
<!-- <div class="iconfont iconfont-iconjuminxinxi"/>-->
<!-- <div class="iconfont iconfont-iconLogo"/>-->
<ai-result v-if="result.tips" v-bind="result"/>
<template v-if="isDev">
<input v-if="!!$route.query.code" class="codeText" :value="$route.query.code"/>
<div class="codeBtn" @click="devGetCode">获取code</div>
<div flex class="appsPane wrap">
<b v-for="app in apps" :key="app.key" @tap="gotoApp(app.key)">{{ app.name }}</b>
</div>
</template>
</section>
</template>
<script>
import {mapActions, mapState} from 'vuex'
import AiResult from "../components/AiResult";
import UTag from "../uview/components/u-tag/u-tag";
export default {
name: 'loading',
components: {UTag, AiResult},
inject: ['root'],
computed: {
...mapState(['token', 'apps', 'openUser', 'user']),
currentApp() {
return this.apps.find(e => e.key == this.$route.query.app) || {}
},
isDev() {
return this.$route.hash == "#dev"
}
},
data() {
return {
result: {}
}
},
methods: {
...mapActions(['getToken', 'getAccount', 'agentSign', 'getUserInfo', 'getCode', 'closeAgent']),
initAccess() {
if (this.$route.hash == "#error" || this.isDev) {
return Promise.resolve()
} else if (this.$route.hash == "#form") {
if (this.openUser?.openId || !!this.$route.query.preview) {
this.openForm()
} else if (this.$route.query?.code) {
this.getToken(this.$route.query?.code)
.then(() => this.getUserInfo())
.then(() => this.openForm())
} else this.getCode(location.href)
} else if (this.token) {//获取账号信息
return this.getAccount()
} else if (this.$route.query?.code) {//获取token
return this.getToken(this.$route.query?.code)
} else {//获取应用配置
this.getCode(location.href)
}
},
openForm() {
this.redirectTo("/askForm/askForm")
},
redirectTo(path) {
let {query, hash} = this.$route
delete query.app
uni.redirectTo({
url: `/pages${path}`, success: () => {
this.$router.push({query, hash})
}
})
},
gotoApp(app) {
uni.reLaunch({url: '/pages/loading?app=' + app})
},
devGetCode() {
this.getCode(location.origin + '/pages/loading#dev')
}
},
created() {
uni.showLoading({
title: "加载中"
})
this.initAccess()?.then(() => {
uni.hideLoading()
if (this.token) {
if (this.currentApp.name) {
this.redirectTo(this.currentApp.path)
} else if (this.$route.query.url) {
this.redirectTo(this.$route.query.url)
} else {
this.result = {
status: "error",
tips: "应用加载失败",
btn: "重新加载",
btnTap() {
location.href = location.href?.replace("#error", '')
}
}
}
} else if (this.isDev) {
this.result = {
tips: "欢迎进入开发应用",
}
} else {
this.result = {
status: "error",
tips: "应用加载失败",
btn: "重新加载",
btnTap() {
location.href = location.href?.replace("#error", '')
}
}
}
})?.catch(() => {
uni.navigateTo({url: './login'})
})
}
}
</script>
<style lang="scss" scoped>
.loading {
height: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
& > span {
font-size: 36px;
}
.codeText {
font-size: 24px;
margin-top: 16px;
padding: 4px 0;
width: 100%;
text-align: center;
background: #ccc;
}
.codeBtn {
width: 180px;
text-align: center;
cursor: pointer;
font-size: 24px;
background: $uni-color-warning;
color: #fff;
padding: 8px;
margin-top: 16px;
border-radius: 8px;
font-weight: normal;
}
.appsPane {
justify-content: center;
margin-top: 16px;
padding: 0 16px;
b {
cursor: pointer;
font-size: 24px;
background: $uni-color-primary;
color: #fff;
padding: 8px;
margin: 4px;
border-radius: 8px;
font-weight: normal;
}
}
}
</style>

96
src/pages/login.vue Normal file
View File

@@ -0,0 +1,96 @@
<template>
<section class="login">
<u-form :model="form" ref="loginForm" label-width="140">
<u-form-item label="账号" prop="phone" :errory-type="['message']">
<u-input v-model="form.phone" placeholder="请输入手机号"/>
</u-form-item>
<u-form-item label="验证码" prop="vcode">
<u-input v-model="form.vcode" placeholder="请输入短信验证码"/>
<u-verification-code ref="vcode" secords="60" @change="v=>tips=v"/>
<div class="vcode" @tap="$u.debounce(getVCode)">{{ tips }}</div>
</u-form-item>
</u-form>
<div bottom>
<u-button type="primary" @tap="handleLogin">绑定并登录</u-button>
</div>
</section>
</template>
<script>
import {mapActions, mapState} from "vuex";
export default {
name: "login",
inject: ['root'],
computed: {
...mapState(['lastPage']),
rules() {
return {
phone: [{required: true, message: "请选择分组"}],
vcode: [{required: true, message: "请选择快捷回复类型"}],
}
}
},
data() {
return {
form: {},
tips: ''
}
},
methods: {
...mapActions(['getCode']),
getVCode() {
if (this.form.phone) {
this.$http.post("/admin/user/sendCode", null, {
withoutToken: 1,
params: {phone: this.form.phone}
}).then(() => {
this.$u.toast("验证已发送!")
this.$refs.vcode?.start()
})
} else {
this.$u.toast("请先填写手机号!")
}
},
handleLogin() {
this.$refs.loginForm.validate(v => {
if (v) {
let params = {
...this.form, code: this.$route.query?.code, then: res => {
let last = uni.getStorageSync("lastApp")
if (last) {
this.$store.commit("login", [res?.token_type, res?.access_token].join(" ").trim())
uni.removeStorageSync("lastApp")
// this.root.getCode(location.origin + last)
uni.reLaunch({url: "./loading?app=" + last})
} else this.$u.toast("绑定成功,请重新打开应用页面!")
}
}
this.$store.commit("bindAccount", params)
}
})
}
},
created() {
!this.$route.query?.code && this.getCode()
},
mounted() {
this.$nextTick(() => this.$refs.loginForm?.setRules(this.rules))
}
}
</script>
<style lang="scss" scoped>
.login {
border-top: 1px solid #D4D4D4;
padding: 0 0 208px;
background: #F5F5F5;
box-sizing: border-box;
height: 100%;
.vcode {
color: $uni-color-primary;
cursor: pointer;
}
}
</style>

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