Commit fbc59c076058c34e76e8e03cfef60badb0c8b1c0

Authored by xp.Huang
2 parents fe27c91c 57391bbf

Merge branch 'main_dev' into 'main'

Main dev

See merge request yunteng/thingskit-app!159
... ... @@ -4,37 +4,47 @@
4 4 * data ((deviceProfileIds))
5 5 */
6 6 const getDeviceApi = (urlParams, data) => {
7   - const {
8   - page,
9   - pageSize,
10   - } = urlParams
11   - return uni.$u.http.post(`/yt/device?page=${page}&pageSize=${pageSize}`, data);
12   -};
  7 + const { page, pageSize } = urlParams
  8 + return uni.$u.http.post(`/yt/device?page=${page}&pageSize=${pageSize}`, data)
  9 +}
13 10
14 11 // 设备详情
15 12 const getDeviceDetail = (id) => {
16   - return uni.$u.http.get(`/yt/device/${id}`);
17   -};
  13 + return uni.$u.http.get(`/yt/device/${id}`)
  14 +}
18 15
19 16 //设备属性
20 17 const getAttribute = (deviceProfileId) => {
21   - return uni.$u.http.get(`/yt/device/attributes/${deviceProfileId}`);
22   -};
  18 + return uni.$u.http.get(`/yt/device/attributes/${deviceProfileId}`)
  19 +}
23 20
24 21 //命令下发
25 22 const issueCommand = (type, tbDeviceId, data) => {
26   - return uni.$u.http.post(`/rpc/${type==='OneWay'?'oneway':'twoway'}/${tbDeviceId}`, data)
  23 + return uni.$u.http.post(`/rpc/${type === 'OneWay' ? 'oneway' : 'twoway'}/${tbDeviceId}`, data)
27 24 }
28 25
29 26 //获取命令下发记录
30 27 const getRpcRecord = (params) => {
31   - return uni.$u.http.get('/yt/rpc', params);
32   -};
  28 + return uni.$u.http.get('/yt/rpc', params)
  29 +}
  30 +
  31 +// 获取设备状态 在线or离线
  32 +const getDeviceActiveTime = (entityId) => {
  33 + return uni.$u.http.get(`/plugins/telemetry/DEVICE/${entityId}/values/attributes?keys=active`)
  34 +}
  35 +
  36 +// 获取服务调用
  37 +const getModelServices = (params) => {
  38 + const { deviceProfileId } = params
  39 + return uni.$u.http.get(`/yt/things_model/get_services/${deviceProfileId}`)
  40 +}
33 41
34 42 export default {
35   - getDeviceApi,
36   - getDeviceDetail,
37   - getAttribute,
38   - issueCommand,
39   - getRpcRecord
40   -}
\ No newline at end of file
  43 + getDeviceApi,
  44 + getDeviceDetail,
  45 + getAttribute,
  46 + issueCommand,
  47 + getRpcRecord,
  48 + getModelServices,
  49 + getDeviceActiveTime,
  50 +}
... ...
... ... @@ -17,15 +17,11 @@
17 17 </view>
18 18 </view>
19 19 <!-- 命令下发 设备在线并且不是网关子设备 -->
20   - <view class="mr-2" v-if="deviceDetail.deviceState === 'ONLINE' && deviceDetail.transportType!==deviceTypeNum.GBT">
21   - <!-- #ifdef MP -->
22   - <u-button type="primary" shape="circle" size="mini" text="下发命令" @click="handleMpShowModal" />
23   - <!-- #endif -->
24   - <!-- #ifdef APP-PLUS -->
  20 + <view class="mr-2"
  21 + v-if="deviceDetail.deviceState === 'ONLINE' && deviceDetail.transportType !== deviceTypeNum.GBT">
25 22 <view class="cu-item" @tap="handleAppShowModal" data-target="Modal">
26   - <text>下发命令</text>
  23 + <text>命令下发</text>
27 24 </view>
28   - <!-- #endif -->
29 25 </view>
30 26 </view>
31 27 <!-- 设备详情 -->
... ... @@ -61,66 +57,37 @@
61 57 </view>
62 58 </view>
63 59 <!-- 命令下发 -->
64   - <!-- #ifdef APP-PLUS -->
65   - <!-- 原生弹窗 封装成子组件无效 -->
66   - <view v-show="showNativeModal" class="cu-modal" :class="modalName == 'Modal' ? 'show' : ''">
67   - <view class="cu-dialog">
68   - <view class="app-command-content">
69   - <view class="app-command-text">
70   - <text>命令下发</text>
71   - </view>
72   - <view class="app-command-type">
73   - <text>下发类型</text>
74   - <view class="mr-2">
75   - <radio-group @change="radioChange" class="flex mr-1">
76   - <label v-for="(item, index) in commandTypeList" :key="item.value">
77   - <view class="flex">
78   - <view class="ml-1">
79   - <radio :value="item.value" :checked="index === current" />
80   - </view>
81   - <view style="width:10rpx"></view>
82   - <view class="ml-1">{{item.name}}</view>
83   - </view>
84   - </label>
85   - </radio-group>
86   - </view>
87   - </view>
88   - <view class="app-command-body">
89   - <textarea class="app-command-textarea" v-model="inputCommandContent"
90   - :placeholder="`请输入下发内容${isShowTCP?'(字符串格式)':'(json格式)'}`" />
91   - <u-icon @click="handleCopy(copyTextValue)" v-if="!isShowTCP" name="question-circle"
92   - color="#2979ff" size="28" class="ml-10">
93   - </u-icon>
94   - </view>
95   - <view class="app-command-buttons">
96   - <view class="cancel-button" @click="cancelCommand"><text class="cancel-text">取消</text></view>
97   - <view @click="handleAppCommand" class="confrim-button"><text class="confrim-text">确认</text>
98   - </view>
99   - </view>
100   - </view>
101   - </view>
102   - </view>
103   - <!-- #endif -->
104   - <!-- #ifdef MP -->
105   - <!-- u-modal在app端弹窗层级无法覆盖背景色 -->
106   - <mp-command-issuance ref="mpCommandIssuanceRef" :isShowTCP="isShowTCP" :showModal="mpShowModal"
107   - @hideModal="hideMpModal" @cancelCommand="cancelCommand"
108   - @confirmCommand="confirmCommand"></mp-command-issuance>
109   - <!-- #endif -->
  60 + <!-- 引入三方库,进行mp端和app端同步弹窗 -->
  61 + <tn-popup v-model="showNativeModal">
  62 + <commandIssuanceVue ref="mpCommandIssuanceRef" :isShowTCP="isShowTCP"
  63 + :deviceDetail="deviceDetail" @hideModal="hideMpModal" @cancelCommand="cancelCommand"
  64 + @confirmCommand="confirmCommand" />
  65 + </tn-popup>
110 66 </view>
111 67 </template>
112 68
113 69 <script>
114   - import {formatToDate} from '@/plugins/utils.js';
  70 + import {
  71 + formatToDate
  72 + } from '@/plugins/utils.js';
115 73 import api from '@/api/index.js';
116 74 import mpCommandIssuance from './mp-command-issuance.vue';
117   - import {commandTypeList} from '../config/data.js'
118   - import {useShowModal} from '@/plugins/utils.js'
119   - import {deviceTypeNum} from '../config/data'
120   -
  75 + import {
  76 + commandTypeList
  77 + } from '../config/data.js'
  78 + import {
  79 + useShowModal
  80 + } from '@/plugins/utils.js'
  81 + import {
  82 + deviceTypeNum
  83 + } from '../config/data'
  84 + import deviceDetail from '../device-detail.vue';
  85 + import commandIssuanceVue from './command-issuance.vue';
  86 +
121 87 export default {
122 88 components: {
123 89 mpCommandIssuance,
  90 + commandIssuanceVue
124 91 },
125 92 props: {
126 93 deviceDetail: {
... ... @@ -133,7 +100,7 @@
133 100 showNativeModal: false,
134 101 current: 0,
135 102 commandTypeList,
136   - deviceTypeNum:deviceTypeNum,
  103 + deviceTypeNum: deviceTypeNum,
137 104 commandTypeStr: 'OneWay',
138 105 inputCommandContent: '',
139 106 mpShowModal: false,
... ... @@ -233,8 +200,8 @@
233 200 this.modalName = null;
234 201 this.showNativeModal = false
235 202 },
236   - confirmCommand(commandType, inputCommandVal) {
237   - this.handleCommand(commandType, inputCommandVal)
  203 + confirmCommand(commandType, callType, values) {
  204 + this.handleCommand(commandType, callType, values)
238 205 },
239 206 cancelCommand() {
240 207 this.hideMpModal();
... ... @@ -246,31 +213,41 @@
246 213 })
247 214 },
248 215 handleAppCommand() {
249   - this.handleCommand(this.commandTypeStr, this.inputCommandContent)
  216 + this.handleCommand(this.commandTypeStr, this.commandTypeStr, this.inputCommandContent)
250 217 },
251   - async handleCommand(commandType, inputCommandVal) {
252   - if (!inputCommandVal) return uni.$u.toast('请输入下发内容~');
253   - if (this.isShowTCP) {
254   - //TCP的格式只能是字符串
255   - const zg = /^[0-9a-zA-Z]*$/;
256   - if (!zg.test(inputCommandVal)) {
257   - uni.$u.toast('输入的内容只能是字母和数字的组合');
258   - return;
  218 + async handleCommand(commandType, callType, values) {
  219 + if (!values) return uni.$u.toast('请输入下发内容~');
  220 + if (callType == 'TwoWay') {
  221 + const result = await api.deviceApi.getDeviceActiveTime(this.deviceDetail.tbDeviceId)
  222 + const [firsetItem] = result || []
  223 + if (!firsetItem.value) {
  224 + return uni.$u.toast('当前设备不在线~')
259 225 }
260   - this.commandValue.params = inputCommandVal;
261   - } else {
262   - this.commandValue.params = JSON.parse(inputCommandVal);
263 226 }
  227 +
264 228 this.commandValue.persistent = true;
265 229 this.commandValue.additionalInfo = {
266   - cmdType: 'API'
  230 + cmdType: commandType == 0 ? 0 : 1
267 231 };
268 232 this.commandValue.method = 'methodThingskit';
269   - this.commandValue.params = inputCommandVal;
270   - await api.deviceApi.issueCommand(commandType, this.deviceDetail.tbDeviceId, this.commandValue);
  233 + this.commandValue.params = values;
  234 + if (commandType == 0) { //下发类型是自定义时
  235 + if (this.isShowTCP) {
  236 + //TCP的格式只能是字符串
  237 + const zg = /^[0-9a-zA-Z]*$/;
  238 + if (!zg.test(values)) {
  239 + uni.$u.toast('输入的内容只能是字母和数字的组合');
  240 + return;
  241 + }
  242 + this.commandValue.params = values;
  243 + } else {
  244 + this.commandValue.params = JSON.parse(values);
  245 + }
  246 + }
  247 +
  248 + await api.deviceApi.issueCommand(callType, this.deviceDetail.tbDeviceId, this.commandValue);
271 249 this.cancelCommand();
272 250 uni.$u.toast('下发成功~');
273   -
274 251 },
275 252
276 253 }
... ... @@ -415,4 +392,4 @@
415 392 /deep/ .u-modal__content {
416 393 padding: 30rpx 0 !important;
417 394 }
418   -</style>
  395 +</style>
\ No newline at end of file
... ...
  1 +<template>
  2 + <view class="w-100 modal-content">
  3 + <view>
  4 + <view style="max-height: 560rpx; overflow-y: scroll">
  5 + <view class="header-title">命令下发</view>
  6 + <view class="u-flex">
  7 + <text class="type-text">下发类型:</text>
  8 + <u-radio-group v-model="commandType" placement="row" @change="handleCommand">
  9 + <u-radio :customStyle="{ marginRight: '20rpx' }" v-for="item in commandTypeList"
  10 + activeColor="#3388FF" :label="item.label" :name="item.value" :key="item.value"></u-radio>
  11 + </u-radio-group>
  12 + </view>
  13 + <view class="u-flex" style="margin-top: 28rpx" v-if="commandType == 0">
  14 + <text class="type-text">单向/双向:</text>
  15 + <u-radio-group v-model="callType" placement="row">
  16 + <u-radio activeColor="#3388FF" label="单向" name="OneWay"></u-radio>
  17 + <view style="margin: 0 20rpx"></view>
  18 + <u-radio activeColor="#3388FF" label="双向" name="TwoWay"></u-radio>
  19 + </u-radio-group>
  20 + </view>
  21 + <view class="u-flex" style="margin-top: 28rpx" v-else>
  22 + <text class="type-text">服务:</text>
  23 + <view @click="openService">
  24 + <u-input shape="circle" v-model="serviceName" placeholder="请选择服务" disabled disabledColor="#fff"
  25 + suffixIcon="arrow-down" />
  26 + </view>
  27 + </view>
  28 + <view class="u-flex" style="
  29 + margin-top: 28rpx;
  30 + flex-direction: column;
  31 + align-items: flex-start;
  32 + " v-if="isShowServiceFunctionName && commandType == 1">
  33 + <text class="type-text">输入参数:</text>
  34 + <seriesForm ref="seriesFormRef" :seriesInputData="seriesInputData" :isTCPTransport="isTCPTransport">
  35 + </seriesForm>
  36 + </view>
  37 + <view class="content-body" v-if="commandType == 0">
  38 + <div class="u-flex u-row-between">
  39 + <u--textarea :placeholder="`请输入下发内容${
  40 + isShowTCP ? '(字符串格式)' : '(json格式)'
  41 + }`" v-model="inputCommandVal" />
  42 + <u-icon v-if="!isShowTCP" @click="handleCopy(copyTextValue)" name="question-circle"
  43 + color="#2979ff" size="28" class="ml-10">
  44 + </u-icon>
  45 + </div>
  46 + </view>
  47 + </view>
  48 + <view class="button-group">
  49 + <view>
  50 + <u-button :customStyle="{ color: '#333' }" color="#e3e3e5" shape="circle" text="取消"
  51 + @click="cancelCommand"></u-button>
  52 + </view>
  53 + <view>
  54 + <u-button color="#3388ff" shape="circle" text="确认" @click="confirmCommand"></u-button>
  55 + </view>
  56 + </view>
  57 + </view>
  58 + <u-picker :show="isShowService" :columns="[
  59 + seriesList.map((item) => ({
  60 + label: item.functionName,
  61 + value: item.identifier,
  62 + callType: item.callType,
  63 + })),
  64 + ]" keyName="label" closeOnClickOverlay @cancel="cancelTypeGap" @close="cancelTypeGap"
  65 + @confirm="handleSelect"></u-picker>
  66 + </view>
  67 +</template>
  68 +
  69 +<script>
  70 + import {
  71 + useShowModal
  72 + } from "@/plugins/utils.js";
  73 + import seriesForm from "./seriesForm.vue";
  74 + import api from "@/api/index.js";
  75 +
  76 + export default {
  77 + components: {
  78 + seriesForm,
  79 + },
  80 + props: {
  81 + showModal: Boolean,
  82 + isShowTCP: Boolean,
  83 + deviceDetail: Object,
  84 + },
  85 + data() {
  86 + return {
  87 + current: 0,
  88 + commandType: 0, //下发类型
  89 + callType: "OneWay", //单双向
  90 + serviceName: "", //服务
  91 + service: null, //服务
  92 + inputCommandVal: "", //自定义命令
  93 + isShowService: false, //服务下拉框
  94 + isShowServiceFunctionName: false, //选择服务过后的输入参数
  95 + copyTextValue: {
  96 + method: "methodThingskit",
  97 + params: {
  98 + pin: 7,
  99 + value: 1,
  100 + },
  101 + },
  102 + commandTypeList: [{
  103 + label: "自定义",
  104 + value: 0
  105 + }],
  106 + seriesList: [], //服务下拉框的数据
  107 + boolList: [],
  108 + enumList: [],
  109 + seriesInputData: [],
  110 + isTCPTransport: false,
  111 + };
  112 + },
  113 + mounted() {
  114 + this.getFormInfo();
  115 + },
  116 + watch: {
  117 + showModal: {
  118 + deep: true,
  119 + handler() {
  120 + this.commandType = 0;
  121 + this.serviceName = "";
  122 + this.isShowServiceFunctionName = false;
  123 + },
  124 + },
  125 + },
  126 + methods: {
  127 + cancelCommand() {
  128 + this.commandType = 0;
  129 + this.$emit("cancelCommand");
  130 + },
  131 + async confirmCommand() {
  132 + if (this.commandType == 0) {
  133 + this.$emit(
  134 + "confirmCommand",
  135 + this.commandType,
  136 + this.callType,
  137 + this.inputCommandVal
  138 + );
  139 + } else {
  140 + const result = this.$refs.seriesFormRef.handleValidate();
  141 + if (!result) {
  142 + return;
  143 + }
  144 + const value = this.$refs.seriesFormRef.getFormField();
  145 + const values = this.isTCPTransport ?
  146 + value.serviceCommand : {
  147 + [this.service]: value,
  148 + };
  149 + this.$emit("confirmCommand", this.commandType, this.callType, values);
  150 + }
  151 + },
  152 + handleCopy(value) {
  153 + useShowModal(JSON.stringify(value), "命令下发", "复制内容").then(
  154 + (res) => {
  155 + uni.setClipboardData({
  156 + data: JSON.stringify(value),
  157 + success: () => {
  158 + uni.showToast({
  159 + title: "复制成功",
  160 + });
  161 + },
  162 + });
  163 + }
  164 + );
  165 + },
  166 +
  167 + async getFormInfo() {
  168 + const {
  169 + transportType,
  170 + deviceType,
  171 + deviceProfile
  172 + } =
  173 + this.deviceDetail || {};
  174 + const {
  175 + profileData: {
  176 + transportConfiguration: {
  177 + protocol
  178 + },
  179 + },
  180 + } = deviceProfile || {};
  181 + this.isTCPTransport = transportType === "TCP";
  182 + const isTCPModbus = this.isTCPTransport && protocol === "MODBUS_RTU";
  183 + if (isTCPModbus || (this.isTCPTransport && deviceType === "SENSOR")) {
  184 + this.commandTypeList =
  185 + this.commandTypeList.length == 2 ?
  186 + this.commandTypeList.pop() :
  187 + this.commandTypeList;
  188 + } else {
  189 + this.commandTypeList.push({
  190 + label: "服务",
  191 + value: 1
  192 + });
  193 + this.seriesList = await api.deviceApi.getModelServices(
  194 + this.deviceDetail
  195 + );
  196 + }
  197 + },
  198 +
  199 + openService() {
  200 + this.isShowService = true;
  201 + },
  202 + handleSelect(e) {
  203 + this.isShowService = false;
  204 + const {
  205 + value
  206 + } = e || {};
  207 + this.serviceName = value[0].label;
  208 + this.service = value[0].value;
  209 + if (this.service) {
  210 + this.isShowServiceFunctionName = true;
  211 + const {
  212 + functionJson
  213 + } =
  214 + this.seriesList.filter(
  215 + (item) => item.identifier === this.service
  216 + )[0] || {};
  217 + const {
  218 + inputData
  219 + } = functionJson || {};
  220 + this.seriesInputData = inputData || [];
  221 + this.callType = value[0].callType === "ASYNC" ? "OneWay" : "TwoWay";
  222 + }
  223 + },
  224 +
  225 + cancelModel() {
  226 + this.isShowService = false;
  227 + this.seriesInputData = [];
  228 + this.seriesList = [];
  229 + },
  230 +
  231 + handleCommand(name) {
  232 + this.seriesInputData = [];
  233 + this.serviceName = "";
  234 + this.isShowServiceFunctionName = false;
  235 + if (this.commandType == 0) {
  236 + this.callType = "OneWay";
  237 + }
  238 + },
  239 +
  240 + cancelTypeGap() {
  241 + this.isShowService = false;
  242 + },
  243 +
  244 + reset() {
  245 + this.callType = "OneWay";
  246 + this.inputCommandVal = "";
  247 + },
  248 + },
  249 + };
  250 +</script>
  251 +
  252 +<style lang="scss" scoped>
  253 + .modal-content {
  254 + width: 720rpx;
  255 + padding: 0 30rpx;
  256 + background-color: white;
  257 +
  258 + .header-title {
  259 + text-align: center;
  260 + font-weight: 700;
  261 + margin-bottom: 40rpx;
  262 + }
  263 +
  264 + .type-text {
  265 + color: #333;
  266 + font-size: 14px;
  267 + font-weight: 700;
  268 + margin-right: 30rpx;
  269 + }
  270 +
  271 + .content-body {
  272 + margin-top: 28rpx;
  273 + width: 100%;
  274 + }
  275 +
  276 + .button-group {
  277 + display: flex;
  278 + margin-top: 40rpx;
  279 + justify-content: space-between;
  280 +
  281 + view {
  282 + width: 300rpx;
  283 + }
  284 + }
  285 + }
  286 +</style>
\ No newline at end of file
... ...
1 1 <template>
2 2 <view class="mp-u-modal">
3 3 <u-modal :mask-close-able="true" :show="showModal" closeOnClickOverlay :showConfirmButton="false"
4   - @close="$emit('hideModal')" @touchmove.stop.prevent="disabledScroll" z-index="99999">
5   - <view class="w-100 modal-content">
6   - <view class="header-title">命令下发</view>
7   - <view class="u-flex">
8   - <text class="type-text">下发类型:</text>
9   - <u-radio-group v-model="commandType" placement="row">
10   - <u-radio activeColor="#3388FF" label="单向" name="OneWay"></u-radio>
11   - <view style="margin: 0 20rpx;"></view>
12   - <u-radio activeColor="#3388FF" label="双向" name="TwoWay"></u-radio>
13   - </u-radio-group>
14   - </view>
15   - <view class="content-body">
16   - <div class="u-flex u-row-between">
17   - <u--textarea :placeholder="`请输入下发内容${isShowTCP?'(字符串格式)':'(json格式)'}`"
18   - v-model="inputCommandVal" />
19   - <u-icon v-if="!isShowTCP" @click="handleCopy(copyTextValue)" name="question-circle"
20   - color="#2979ff" size="28" class="ml-10">
21   - </u-icon>
22   - </div>
23   - </view>
24   - <view class="button-group">
25   - <view>
26   - <u-button :customStyle="{ color: '#333' }" color="#e3e3e5" shape="circle" text="取消"
27   - @click="cancelCommand"></u-button>
28   - </view>
29   - <view>
30   - <u-button color="#3388ff" shape="circle" text="确认" @click="confirmCommand"></u-button>
31   - </view>
32   - </view>
33   - </view>
  4 + @close="$emit('hideModal')" z-index="99999">
  5 + <commandIssuance :isShowTCP="isShowTCP" :deviceDetail="deviceDetail" />
34 6 </u-modal>
35 7 </view>
36 8 </template>
37 9
38 10 <script>
39   - import {
40   - useShowModal
41   - } from '@/plugins/utils.js'
  11 + import commandIssuance from "./command-issuance.vue";
42 12
43 13 export default {
  14 + components: {
  15 + commandIssuance,
  16 + },
44 17 props: {
45 18 showModal: Boolean,
46   - isShowTCP: Boolean
47   - },
48   - data() {
49   - return {
50   - current: 0,
51   - commandType: 'OneWay',
52   - inputCommandVal: '',
53   - copyTextValue: {
54   - "method": "methodThingskit",
55   - "params": {
56   - "pin": 7,
57   - "value": 1
58   - }
59   - }
60   - }
  19 + isShowTCP: Boolean,
  20 + deviceDetail: Object,
61 21 },
62   - methods: {
63   - cancelCommand() {
64   - this.$emit('cancelCommand')
65   - },
66   - confirmCommand() {
67   - this.$emit('confirmCommand', this.commandType, this.inputCommandVal)
68   - },
69   - handleCopy(value) {
70   - useShowModal(JSON.stringify(value), '命令下发', '复制内容').then(res => {
71   - uni.setClipboardData({
72   - data: JSON.stringify(value),
73   - success: () => {
74   - uni.showToast({
75   - title: '复制成功'
76   - })
77   - }
78   - });
79   - })
80   - },
81   - reset() {
82   - this.commandType = 'OneWay'
83   - this.inputCommandVal = ''
84   - }
85   - }
86   - }
  22 + };
87 23 </script>
88 24
89   -<style lang="scss" scoped>
90   - .modal-content {
91   - width: 720rpx;
92   - padding: 0 30rpx;
93   - background-color: white;
94   -
95   - .header-title {
96   - text-align: center;
97   - font-weight: 700;
98   - margin-bottom: 40rpx;
99   - }
100   -
101   - .type-text {
102   - color: #333;
103   - font-size: 14px;
104   - font-weight: 700;
105   - margin-right: 30rpx;
106   - }
107   -
108   - .content-body {
109   - margin-top: 28rpx;
110   - width: 100%;
111   - }
112   -
113   - .button-group {
114   - display: flex;
115   - margin-top: 40rpx;
116   - justify-content: space-between;
117   -
118   - view {
119   - width: 300rpx;
120   - }
121   - }
122   - }
123   -</style>
\ No newline at end of file
  25 +<style lang="scss" scoped></style>
\ No newline at end of file
... ...
... ... @@ -5,7 +5,7 @@
5 5 <view>{{ item.name }}</view>
6 6 <view class="item-value">{{ item.value || '' }}</view>
7 7 </view>
8   - <view class="item-time">{{ item.time }}</view>
  8 + <view class="item-time">{{ item.value? item.time:'' }}</view>
9 9 </view>
10 10 <mescroll-empty v-if="!recordList.length" />
11 11 </view>
... ...
  1 +<template>
  2 + <view>
  3 + <!-- :label-style="{'width':'120rpx','overflow':'hidden','text-overflow':'ellipsis','white-space':'nowrap',display:'block'}" label-width="120rpx" -->
  4 + <u-form :model="seriesForm" ref="seriesRef">
  5 + <u-form-item style="display: flex" v-for="(item, index) in seriesFunctionList" label-position="left" :key="item.identifier" :prop="item.identifier">
  6 + <view v-if="!isTCPTransport"" :class="Array.isArray(item.dataType.specs)?'positionTop':'positionLeft'">
  7 + <view class="text">{{ item.functionName }}</view>
  8 + <u-input
  9 + v-if="item.dataType.type == 'INT' || item.dataType.type == 'DOUBLE'"
  10 + shape="circle"
  11 + type="number"
  12 + v-model.number="seriesForm[item.identifier]"
  13 + :placeholder="`请输入${item.functionName}`"
  14 + @blur="(e)=>handleBlur(e,item.identifier)"
  15 + />
  16 + <u-input v-if="item.dataType.type == 'TEXT'" shape="circle" type="text" v-model="seriesForm[item.identifier]" :placeholder="`请输入${item.functionName}`" />
  17 + <view v-if="item.dataType.type == 'BOOL'" @click="handleBool(item.functionName, item.dataType, index)">
  18 + <u-input
  19 + shape="circle"
  20 + v-model="seriesForm[item.identifier]"
  21 + :placeholder="`请选择${item.functionName}`"
  22 + disabled
  23 + disabledColor="#fff"
  24 + suffixIcon="arrow-down"
  25 + />
  26 + <u-picker
  27 + :show="showPickerView"
  28 + :columns="[boolList]"
  29 + keyName="label"
  30 + @cancel="handleCancelBool(index)"
  31 + @close="handleCancelBool(index)"
  32 + @confirm="(e) => handleSelectBool(e, item.identifier, index)"
  33 + ></u-picker>
  34 + </view>
  35 + <view v-if="item.dataType.type == 'ENUM'" @click="handleEnum(item.functionName, item.dataType, index)">
  36 + <u-input
  37 + shape="circle"
  38 + v-model="seriesForm[item.identifier]"
  39 + :placeholder="`请选择${item.functionName}`"
  40 + disabled
  41 + disabledColor="#fff"
  42 + suffixIcon="arrow-down"
  43 + />
  44 + <u-picker
  45 + :show="showPickerView"
  46 + :columns="[enumList.map((item) => ({ label: item.name, value: item.value }))]"
  47 + keyName="label"
  48 + @cancel="handleCancelEnum(index)"
  49 + @close="handleCancelEnum(index)"
  50 + @confirm="(e) => handleSelectEnum(e, item.identifier, index)"
  51 + ></u-picker>
  52 + </view>
  53 + <template v-if="Array.isArray(item.dataType.specs)">
  54 + <structuralForm class="seriesForm" :ref="item.identifier" :seriesInputData="item.dataType.specs || []"></structuralForm>
  55 + </template>
  56 + </view>
  57 + <view v-else class="positionLeft" style=" border: 1px dashed #f0f0f0; padding: 20rpx;">
  58 + <view class="text">服务命令</view>
  59 + <u-input v-model="seriesForm.serviceCommand" type="text" shape="circle" :disabled="true"></u-input>
  60 + </view>
  61 + </u-form-item>
  62 + </u-form>
  63 + </view>
  64 +</template>
  65 +
  66 +<script>
  67 +import structuralForm from './structuralForm.vue'
  68 +export default {
  69 + name: 'StructForm',
  70 + props: {
  71 + seriesInputData: {
  72 + type: Array,
  73 + default: () => [],
  74 + },
  75 + isTCPTransport:{
  76 + type:Boolean,
  77 + default:false
  78 + }
  79 + },
  80 + components: {
  81 + structuralForm,
  82 + },
  83 + data() {
  84 + return {
  85 + showPickerView:false,
  86 + boolInfo: {},
  87 + enumInfo: {},
  88 + seriesForm: {},
  89 + seriesRules: {},
  90 + seriesFunctionList: [],
  91 + boolList: [],
  92 + enumList: [],
  93 + }
  94 + },
  95 + async mounted() {
  96 + await this.$nextTick(() => {
  97 + this.createInit()
  98 + })
  99 + },
  100 + watch: {
  101 + seriesInputData: {
  102 + deep: true,
  103 + handler(newVal, oldVal) {
  104 + this.createInit()
  105 + },
  106 + },
  107 + },
  108 +
  109 + methods: {
  110 + createInit() {
  111 + this.seriesForm = {}
  112 + this.boolInfo = {}
  113 + this.seriesFunctionList = this.seriesInputData
  114 + for(const item of this.seriesInputData){
  115 + this.$set(item, 'isShowModel', false)//动态添加picker的控制变量
  116 + const {
  117 + dataType,
  118 + identifier,
  119 + functionName,
  120 + serviceCommand
  121 + } = item || {}
  122 + const {specs,type} = dataType || {}
  123 + if(this.isTCPTransport){
  124 + this.$set(this.seriesForm,'serviceCommand',serviceCommand)
  125 + break;
  126 + }
  127 + if (Array.isArray(specs)) {
  128 + specs.forEach((itemSpecs) => {
  129 + this.$set(this.seriesForm, identifier, {
  130 + [itemSpecs.identifier]: '',
  131 + })
  132 + })
  133 + } else {
  134 + this.$set(this.seriesForm, identifier, '')
  135 + }
  136 +
  137 + //设置验证规则
  138 + const { valueRange, length = 10240 } = specs || {}
  139 + const { max = 2147483647, min = -2147483647 } = valueRange || {}
  140 + if (type !== 'STRUCT') {
  141 + this.seriesRules[identifier] = [
  142 + { required: true, message: type == 'BOOL' || type == 'ENUM' ? '请选择' : '请输入' + functionName },
  143 + type == 'INT' || type == 'DOUBLE'
  144 + ? {
  145 + type: 'number',
  146 + trigger: 'change',
  147 + validator: (_rule, value) => {
  148 + const reg = /^[0-9]*$/
  149 + if (!reg.test(value)) return Promise.reject(new Error(`${functionName}不是一个有效的数字`))
  150 + if (value < min || value > max) return Promise.reject(new Error(`${functionName}取值范围在${min}~${max}之间`))
  151 +
  152 + return Promise.resolve(value)
  153 + },
  154 + }
  155 + : type == 'TEXT'
  156 + ? {
  157 + type: 'string',
  158 + trigger: 'change',
  159 + validator: (_rule, value) => {
  160 + if ((value?.length || 0) > length) return Promise.reject(new Error(`${functionName}数据长度应该小于${length}`))
  161 +
  162 + return Promise.resolve(value)
  163 + },
  164 + }
  165 + : {},
  166 + ]
  167 + }
  168 + }
  169 + //设置验证规则
  170 + this.$nextTick(() => {
  171 + !this.isTCPTransport && this.$refs.seriesRef.setRules(this.seriesRules)
  172 + })
  173 + },
  174 + isEmptyObject(obj) {
  175 + return Object.keys(obj).length === 0 && obj.constructor === Object
  176 + },
  177 +
  178 + //打开Bool的picker
  179 + handleBool(name, dataType, num) {
  180 + this.seriesFunctionList.forEach((item, index) => {
  181 + if (index == num) {
  182 + this.showPickerView = true
  183 + item.isShowModel = true
  184 + }
  185 + })
  186 + const { specs } = dataType || {}
  187 + const { boolClose, boolOpen } = specs || {}
  188 + if(!boolClose&&!boolOpen){
  189 + uni.$u.toast(`暂无可选的${name}`)
  190 + this.boolList = []
  191 + return
  192 + }
  193 + this.boolList = [
  194 + { label: boolClose + '-0', value: 0 },
  195 + { label: boolOpen + '-1', value: 1 },
  196 + ]
  197 + },
  198 + handleSelectBool(e, name, num) {
  199 + const { value } = e || {}
  200 + this.boolInfo[name] = value[0].value
  201 + this.$set(this.seriesForm, name, value[0].label)
  202 + this.seriesFunctionList.forEach((item, index) => {
  203 + if (index == num) {
  204 + item.isShowModel = false
  205 + this.showPickerView = false
  206 + }
  207 + })
  208 + },
  209 + handleBlur(value,name){
  210 + if(!value) return
  211 + this.$set(this.seriesForm,name,Number(value))
  212 + },
  213 +
  214 + //打开Enum的picker
  215 + handleEnum(name, dataType, num) {
  216 + const { specsList } = dataType || {}
  217 + this.enumList = specsList || []
  218 + console.log(this.enumList,'enumInfo')
  219 + if(!this.enumList.length){
  220 + return uni.$u.toast(`暂无可选的${name}`)
  221 + }
  222 + this.seriesFunctionList.forEach((item, index) => {
  223 + if (index == num) {
  224 + item.isShowModel = true
  225 + this.showPickerView = true
  226 + }
  227 + })
  228 + },
  229 + //确定选中
  230 + handleSelectEnum(e, name, num) {
  231 + const { value } = e || {}
  232 + this.enumInfo[name] = value[0].value
  233 + this.$set(this.seriesForm, name, value[0].label)
  234 + this.seriesFunctionList.forEach((item, index) => {
  235 + if (index == num) {
  236 + item.isShowModel = false
  237 + this.showPickerView = false
  238 + }
  239 + })
  240 + },
  241 +
  242 +
  243 + //关闭Enum和Bool的picker弹框
  244 + handleCancelEnum(num){
  245 + this.seriesFunctionList.forEach((item, index) => {
  246 + if (index == num) {
  247 + item.isShowModel = false
  248 + this.showPickerView = false
  249 + }
  250 + })
  251 + },
  252 + handleCancelBool(num){
  253 + this.seriesFunctionList.forEach((item, index) => {
  254 + if (index == num) {
  255 + item.isShowModel = false
  256 + this.showPickerView = false
  257 + }
  258 + })
  259 + },
  260 +
  261 + // 获取表单数据
  262 + getFormField() {
  263 + const keys = Object.keys(this.seriesForm)
  264 + for (let i = 0; i < keys.length; i++) {
  265 + const key = keys[i]
  266 + if (Array.isArray(this.$refs[key])) {
  267 + const values = this.$refs[key][0]?.getFormField()
  268 + if (!this.isEmptyObject(values)) {
  269 + this.seriesForm[key] = values
  270 + }
  271 + }
  272 + }
  273 + return {
  274 + ...this.seriesForm,
  275 + ...this.boolInfo,
  276 + ...this.enumInfo,
  277 + }
  278 + },
  279 +
  280 +
  281 + handleValidate() {
  282 + const keys = Object.keys(this.seriesForm)
  283 + for (let i = 0; i < keys.length; i++) {
  284 + const key = keys[i]
  285 + if (Array.isArray(this.$refs[key])) {
  286 + this.$refs[key][0]?.handleValidate(valid=>{
  287 + if(!valid) return false
  288 + })
  289 + } else {
  290 + this.$refs.seriesRef.validate(valid=>{
  291 + if(!valid) return false
  292 + })
  293 + }
  294 + return true
  295 + }
  296 + },
  297 + },
  298 +}
  299 +</script>
  300 +
  301 +<style lang="scss" scoped>
  302 +.positionLeft {
  303 + display: flex;
  304 + align-items: center;
  305 + .text{
  306 +
  307 + max-width: 190rpx;
  308 + overflow: hidden;
  309 + text-overflow: ellipsis;
  310 + white-space: nowrap;
  311 + }
  312 +}
  313 +
  314 +.positionTop {
  315 + display: flex;
  316 + flex-direction: column;
  317 + border: 1px dashed #f0f0f0;
  318 + padding: 20rpx;
  319 + .seriesForm{
  320 + margin-left:30rpx
  321 + }
  322 +}
  323 +</style>
... ...
  1 +<template>
  2 + <view>
  3 + <u-form :model="seriesForm" ref="seriesRef" :label-style="{ width: '160rpx', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap', display: 'block' }" label-width="160rpx">
  4 + <u-form-item v-for="(item, index) in seriesFunctionList" :key="item.identifier" :label="item.functionName" :prop="item.identifier">
  5 + <u-input v-if="item.dataType.type == 'INT' || item.dataType.type == 'DOUBLE'" shape="circle" type="number" v-model="seriesForm[item.identifier]" :placeholder="`请输入${item.functionName}`" />
  6 + <u-input v-if="item.dataType.type == 'TEXT'" shape="circle" type="text" v-model="seriesForm[item.identifier]" :placeholder="`请输入${item.functionName}`" />
  7 + <view @click="handleBool(item.identifier, item.dataType, index)">
  8 + <u-input
  9 + v-if="item.dataType.type == 'BOOL'"
  10 + shape="circle"
  11 + v-model="seriesForm[item.identifier]"
  12 + :placeholder="`请选择${item.functionName}`"
  13 + disabled
  14 + disabledColor="#fff"
  15 + suffixIcon="arrow-down"
  16 + />
  17 + <u-picker
  18 + :show="showPickerView"
  19 + :columns="[boolList]"
  20 + keyName="label"
  21 + closeOnClickOverlay
  22 + @cancel="handleCancel(index)"
  23 + @close="handleCancel(index)"
  24 + @confirm="(e) => handleSelectBool(e, item.identifier, index)"
  25 + ></u-picker>
  26 + </view>
  27 +
  28 + <view @click="handleEnum(item.identifier, item.dataType, index)">
  29 + <u-input
  30 + v-if="item.dataType.type == 'ENUM'"
  31 + shape="circle"
  32 + v-model="seriesForm[item.identifier]"
  33 + :placeholder="`请选择${item.functionName}`"
  34 + disabled
  35 + disabledColor="#fff"
  36 + suffixIcon="arrow-down"
  37 + />
  38 + <u-picker
  39 + :show="showPickerView"
  40 + :columns="[enumList.map((item) => ({ label: item.name, value: item.value }))]"
  41 + keyName="label"
  42 + closeOnClickOverlay
  43 + @cancel="handleCancel(index)"
  44 + @close="handleCancel(index)"
  45 + @confirm="(e) => handleSelectEnum(e, item.identifier, index)"
  46 + ></u-picker>
  47 + </view>
  48 + </u-form-item>
  49 + </u-form>
  50 + </view>
  51 +</template>
  52 +
  53 +<script>
  54 +export default {
  55 + name: 'StructForm',
  56 + props: {
  57 + seriesInputData: {
  58 + type: Array,
  59 + default: () => [],
  60 + },
  61 + },
  62 + data() {
  63 + return {
  64 + showPickerView: false,
  65 + boolInfo: {},
  66 + enumInfo: {},
  67 + seriesForm: {},
  68 + seriesRules: {},
  69 + seriesFunctionList: [],
  70 + boolList: [],
  71 + enumList: [],
  72 + }
  73 + },
  74 + async mounted() {
  75 + await this.$nextTick(() => {
  76 + this.createInit()
  77 + })
  78 + },
  79 + watch: {
  80 + seriesInputData: {
  81 + deep: true,
  82 + handler(newVal, oldVal) {
  83 + this.createInit()
  84 + },
  85 + },
  86 + },
  87 +
  88 + methods: {
  89 + createInit() {
  90 + this.seriesFunctionList = this.seriesInputData
  91 + this.seriesInputData.forEach((item) => {
  92 + this.$set(item, 'isShowModel', false)
  93 + this.showPickerView=false
  94 + const {
  95 + dataType: { specs, type },
  96 + identifier,
  97 + functionName,
  98 + } = item || {}
  99 +
  100 + this.$set(this.seriesForm, identifier, '')
  101 + const { valueRange, length = 10240 } = specs || {}
  102 +
  103 + const { max = 2147483647, min = -2147483647 } = valueRange || {}
  104 +
  105 + this.seriesRules[identifier] = [
  106 + { required: true, message: type == 'BOOL' || type == 'ENUM' ? '请选择' : '请输入' + functionName },
  107 + type == 'INT' || type == 'DOUBLE'
  108 + ? {
  109 + type: 'number',
  110 + trigger: 'change',
  111 + validator: (_rule, value) => {
  112 + const reg = /^[0-9]*$/
  113 + if (!reg.test(value)) return Promise.reject(new Error(`${functionName}不是一个有效的数字`))
  114 + if (value < min || value > max) return Promise.reject(new Error(`${functionName}取值范围在${min}~${max}之间`))
  115 +
  116 + return Promise.resolve(value)
  117 + },
  118 + }
  119 + : type == 'TEXT'
  120 + ? {
  121 + type: 'string',
  122 + trigger: 'change',
  123 + validator: (_rule, value) => {
  124 + if ((value?.length || 0) > length) return Promise.reject(new Error(`${functionName}数据长度应该小于${length}`))
  125 +
  126 + return Promise.resolve(value)
  127 + },
  128 + }
  129 + : {},
  130 + ]
  131 + })
  132 + this.$nextTick(() => {
  133 + this.$refs.seriesRef.setRules(this.seriesRules)
  134 + })
  135 + },
  136 +
  137 + handleBool(name, dataType, num) {
  138 + this.seriesFunctionList.forEach((item, index) => {
  139 + if (index == num) {
  140 + item.isShowModel = true
  141 + this.showPickerView=true
  142 + }
  143 + })
  144 + const { specs } = dataType || {}
  145 + const { boolClose, boolOpen } = specs || {}
  146 + this.boolList = [
  147 + { label: boolClose + '0', value: 0 },
  148 + { label: boolOpen + '0', value: 1 },
  149 + ]
  150 + this.isShowBool = true
  151 + },
  152 + handleSelectBool(e, name, num) {
  153 + this.isShowBool = false
  154 + const { value } = e || {}
  155 + this.boolInfo[name] = value[0].value
  156 + this.$set(this.seriesForm, name, value[0].label)
  157 + this.seriesFunctionList.forEach((item, index) => {
  158 + if (index == num) {
  159 + item.isShowModel = false
  160 + this.showPickerView=false
  161 + }
  162 + })
  163 + },
  164 +
  165 + getFormField() {
  166 + return {
  167 + ...this.seriesForm,
  168 + ...this.boolInfo,
  169 + ...this.enumInfo,
  170 + }
  171 + },
  172 +
  173 + handleEnum(name, dataType, num) {
  174 + const { specsList } = dataType || {}
  175 + this.isShowEnum = true
  176 + this.enumList = specsList || []
  177 + if(!this.enumList.length){
  178 + return uni.$u.toast(`暂无可选的${name}`)
  179 + }
  180 + this.seriesFunctionList.forEach((item, index) => {
  181 + if (index == num) {
  182 + item.isShowModel = true
  183 + this.showPickerView=true
  184 + }
  185 + })
  186 + },
  187 + handleSelectEnum(e, name, num) {
  188 + const { value } = e || {}
  189 + this.enumInfo[name] = value[0].value
  190 + this.$set(this.seriesForm, name, value[0].label)
  191 + this.seriesFunctionList.forEach((item, index) => {
  192 + if (index == num) {
  193 + item.isShowModel = false
  194 + this.showPickerView=false
  195 + }
  196 + })
  197 + },
  198 + handleCancel(num){
  199 + this.seriesFunctionList.forEach((item, index) => {
  200 + if (index == num) {
  201 + item.isShowModel = false
  202 + this.showPickerView=false
  203 + }
  204 + })
  205 + },
  206 +
  207 + handleValidate() {
  208 + return this.$refs.seriesRef?.validate((valid) => {
  209 + if (!valid) return
  210 + })
  211 + },
  212 + },
  213 +}
  214 +</script>
  215 +
  216 +<style lang="scss" scoped></style>
... ...
... ... @@ -35,15 +35,12 @@
35 35 <u-form-item labelWidth="80px" label="用户账号" prop="username" borderBottom>
36 36 <u--input disabled placeholder="请输入用户账号 " v-model="myInfoModel.username" border="none"></u--input>
37 37 </u-form-item>
  38 + <u-form-item labelWidth="80px" label="有效期" prop="accountExpireTime" borderBottom>
  39 + <u--input disabled v-model="myInfoModel.accountExpireTime" border="none"></u--input>
  40 + </u-form-item>
38 41 <u-form-item labelWidth="80px" label="邮箱地址" prop="email" borderBottom>
39 42 <u--input placeholder="请输入邮箱地址" v-model="myInfoModel.email" border="none"></u--input>
40 43 </u-form-item>
41   - <u-form-item @click="hideKeyboard" labelWidth="80px" label="有效期" prop="accountExpireTime">
42   - <u--input v-model="myInfoModel.accountExpireTime" placeholder="请选择有效期" border="none"></u--input>
43   - <u-datetime-picker :formatter="formatter" :show="showDate" :value="datetime" mode="dateTime"
44   - closeOnClickOverlay @confirm="dateConfirm" @cancel="dateClose" @close="dateClose">
45   - </u-datetime-picker>
46   - </u-form-item>
47 44 </u--form>
48 45 </view>
49 46 <view class="basic-bottom u-flex">
... ... @@ -403,4 +400,10 @@
403 400 width: 663rpx !important;
404 401 }
405 402 }
  403 + /deep/ .u-form-item:nth-child(4) {
  404 + .u-form-item__body {
  405 + background: #f5f7fa !important;
  406 + width: 663rpx !important;
  407 + }
  408 + }
406 409 </style>
... ...
... ... @@ -69,15 +69,17 @@
69 69 this.conditions = {
70 70 deviceState: params
71 71 }
72   - this.loadData(1, {
73   - deviceState: params
74   - });
  72 + // this.loadData(1, {
  73 + // deviceState: params
  74 + // });
75 75 }
76 76 },
77 77 onShow() {
  78 + console.log(2)
78 79 if (getApp().getBindNot()) {
79 80 return
80 81 }
  82 + this.page.num=1
81 83 if (this.ordId) {
82 84 this.loadData(1, {
83 85 organizationId: this.ordId
... ... @@ -87,7 +89,7 @@
87 89 }
88 90 return
89 91 }
90   - this.loadData(1);
  92 + this.loadData(this.page.num,this.conditions);
91 93 },
92 94 methods: {
93 95 inputChanged(e) {
... ...
  1 +## 1.0.0(2024-02-17)
  2 +1. 剥离图鸟框架,可通过 uni_modules 进行导入 节省开发成本
... ...
  1 +let color = [
  2 + 'red',
  3 + 'purplered',
  4 + 'purple',
  5 + 'bluepurple',
  6 + 'aquablue',
  7 + 'blue',
  8 + 'indigo',
  9 + 'cyan',
  10 + 'teal',
  11 + 'green',
  12 + 'yellowgreen',
  13 + 'lime',
  14 + 'yellow',
  15 + 'orangeyellow',
  16 + 'orange',
  17 + 'orangered',
  18 + 'brown',
  19 + 'grey',
  20 + 'gray'
  21 +]
  22 +
  23 +// 酷炫颜色的数量
  24 +const COOL_BG_COLOR_COUNT = 16
  25 +
  26 +/**
  27 + * 获取图鸟配色颜色列表
  28 + */
  29 +function getTuniaoColorList() {
  30 + return color
  31 +}
  32 +
  33 +/**
  34 + * 获取指定类型的随机颜色对应的类
  35 + * @param {String} type 颜色类型
  36 + */
  37 +function getRandomColorClass(type = 'bg') {
  38 + const index = Math.floor(Math.random() * color.length)
  39 + const colorValue = color[index]
  40 +
  41 + return 'tn-' + type + '-' + colorValue
  42 +}
  43 +
  44 +/**
  45 + * 随机获取酷炫背景对应的类
  46 + */
  47 +function getRandomCoolBgClass() {
  48 + const index = (Math.random() * COOL_BG_COLOR_COUNT) + 1
  49 + return 'tn-cool-bg-color-' + Math.floor(index)
  50 +}
  51 +
  52 +/**
  53 + * 根据传入的值获取内部背景颜色类
  54 + *
  55 + * @param {String} backgroundColor 背景颜色信息
  56 + */
  57 +function getBackgroundColorInternalClass(backgroundColor = '') {
  58 + if (!backgroundColor) return ''
  59 +
  60 + if (['tn-bg', 'tn-dynamic-bg', 'tn-main-gradient', 'tn-cool-bg'].some(item => {
  61 + return backgroundColor.includes(item)
  62 + })) {
  63 + return backgroundColor
  64 + }
  65 + return ''
  66 +}
  67 +
  68 +/**
  69 + * 根据传入的值获取背景颜色样式
  70 + *
  71 + * @param {String} backgroundColor 背景颜色信息
  72 + */
  73 +function getBackgroundColorStyle(backgroundColor = '') {
  74 + if (!backgroundColor) return ''
  75 +
  76 + if (!backgroundColor.startsWith('tn-') || ['#', 'rgb', 'rgba'].some(item => {
  77 + return backgroundColor.includes(item)
  78 + })) {
  79 + return backgroundColor
  80 + }
  81 + return ''
  82 +}
  83 +
  84 +/**
  85 + * 根据传入的值获取内部字体颜色类
  86 + *
  87 + * @param {String} fontColor 背景颜色信息
  88 + */
  89 +function getFontColorInternalClass(fontColor = '') {
  90 + if (!fontColor) return ''
  91 +
  92 + if (['tn-color'].some(item => {
  93 + return fontColor.includes(item)
  94 + })) {
  95 + return fontColor
  96 + }
  97 + return ''
  98 +}
  99 +
  100 +/**
  101 + * 根据传入的值获取字体颜色样式
  102 + *
  103 + * @param {String} fontColor 背景颜色信息
  104 + */
  105 +function getFontColorStyle(fontColor = '') {
  106 + if (!fontColor) return ''
  107 +
  108 + if (!fontColor.startsWith('tn-') || ['#', 'rgb', 'rgba'].some(item => {
  109 + return fontColor.includes(item)
  110 + })) {
  111 + return fontColor
  112 + }
  113 + return ''
  114 +}
  115 +
  116 +/**
  117 + * 求两个颜色之间的渐变值
  118 + *
  119 + * @param {String} startColor 开始颜色
  120 + * @param {String} endColor 结束颜色
  121 + * @param {Number} step 颜色等分的份额
  122 + */
  123 +function colorGradient(startColor = 'rgb(0, 0, 0)', endColor='rgb(255, 255, 255)', step = 10) {
  124 + let startRGB = hexToRGB(startColor, false)
  125 + let startR = startRGB[0]
  126 + let startG = startRGB[1]
  127 + let startB = startRGB[2]
  128 +
  129 + let endRGB = hexToRGB(endColor, false)
  130 + let endR = endRGB[0]
  131 + let endG = endRGB[1]
  132 + let endB = endRGB[2]
  133 +
  134 + // 求差值
  135 + let R = (endR - startR) / step
  136 + let G = (endG - startG) / step
  137 + let B = (endB - startB) / step
  138 +
  139 + let colorArr = []
  140 + for (let i = 0; i < step; i++) {
  141 + // 计算每一步的hex值
  142 + let hex = rgbToHex(`rgb(${Math.round(R * i + startR)}, ${Math.round(G * i + startG)}, ${Math.round(B * i + startB)})`)
  143 + colorArr.push(hex)
  144 + }
  145 + return colorArr
  146 +}
  147 +
  148 +/**
  149 + * 将hex的颜色表示方式转换为rgb表示方式
  150 + *
  151 + * @param {String} color 颜色
  152 + * @param {Boolean} str 是否返回字符串
  153 + * @return {Array|String} rgb的值
  154 + */
  155 +function hexToRGB(color, str = true) {
  156 + let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
  157 +
  158 + color = color.toLowerCase()
  159 + if (color && reg.test(color)) {
  160 + // #000 => #000000
  161 + if (color.length === 4) {
  162 + let colorNew = '#'
  163 + for (let i = 1; i < 4; i++) {
  164 + colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1))
  165 + }
  166 + color = colorNew
  167 + }
  168 + // 处理六位的颜色值
  169 + let colorChange = []
  170 + for (let i = 1; i < 7; i += 2) {
  171 + colorChange.push(parseInt("0x" + color.slice(i, i + 2)))
  172 + }
  173 + if (!str) {
  174 + return colorChange
  175 + } else {
  176 + return `rgb(${colorChange[0]}, ${colorChange[1]}, ${colorChange[2]})`
  177 + }
  178 + } else if (/^(rgb|RGB)/.test(color)) {
  179 + let arr = color.replace(/(?:\(|\)|rgb|RGB)*/g, "").split(',')
  180 + return arr.map(item => Number(item))
  181 + } else {
  182 + return color
  183 + }
  184 +}
  185 +
  186 +/**
  187 + * 将rgb的颜色表示方式转换成hex表示方式
  188 + *
  189 + * @param {Object} rgb rgb颜色值
  190 + */
  191 +function rgbToHex(rgb) {
  192 + let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
  193 + if (/^(rgb|RGB)/.test(rgb)) {
  194 + let color = rgb.replace(/(?:\(|\)|rgb|GRB)*/g, "").split(',')
  195 + let strHex = '#'
  196 + for (let i = 0; i < color.length; i++) {
  197 + let hex = Number(color[i]).toString(16)
  198 + // 保证每个值否是两位数
  199 + hex = String(hex).length === 1 ? 0 + '' + hex: hex
  200 + if (hex === '0') {
  201 + hex += hex
  202 + }
  203 + strHex += hex
  204 + }
  205 + if (strHex.length !== 7) {
  206 + strHex = rgb
  207 + }
  208 + return strHex
  209 + } else if (reg.test(rgb)) {
  210 + let num = rgb.replace(/#/, '').split('')
  211 + if (num.length === 6) {
  212 + return rgb
  213 + } else if (num.length === 3) {
  214 + let numHex = '#'
  215 + for (let i = 0; i < num.length; i++) {
  216 + numHex += (num[i] + num[i])
  217 + }
  218 + return numHex
  219 + }
  220 + } else {
  221 + return rgb
  222 + }
  223 +}
  224 +
  225 +/**
  226 + * 将传入的颜色值转换为rgba字符串
  227 + *
  228 + * @param {String} color 颜色
  229 + * @param {Number} alpha 透明度
  230 + */
  231 +function colorToRGBA(color, alpha = 0.3) {
  232 + color = rgbToHex(color)
  233 + // 十六进制颜色值的正则表达式
  234 + let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
  235 +
  236 + color = color.toLowerCase()
  237 + if (color && reg.test(color)) {
  238 + // #000 => #000000
  239 + if (color.length === 4) {
  240 + let colorNew = '#'
  241 + for (let i = 1; i < 4; i++) {
  242 + colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1))
  243 + }
  244 + color = colorNew
  245 + }
  246 + // 处理六位的颜色值
  247 + let colorChange = []
  248 + for (let i = 1; i < 7; i += 2) {
  249 + colorChange.push(parseInt("0x" + color.slice(i, i + 2)))
  250 + }
  251 + return `rgba(${colorChange[0]}, ${colorChange[1]}, ${colorChange[2]}, ${alpha})`
  252 + } else {
  253 + return color
  254 + }
  255 +}
  256 +
  257 +export default {
  258 + COOL_BG_COLOR_COUNT: COOL_BG_COLOR_COUNT,
  259 + getTuniaoColorList,
  260 + getRandomColorClass,
  261 + getRandomCoolBgClass,
  262 + getBackgroundColorInternalClass,
  263 + getBackgroundColorStyle,
  264 + getFontColorInternalClass,
  265 + getFontColorStyle,
  266 + colorGradient,
  267 + hexToRGB,
  268 + rgbToHex,
  269 + colorToRGBA
  270 +}
... ...
  1 +<template>
  2 + <view
  3 + v-if="visibleSync"
  4 + class="tn-popup-class tn-popup"
  5 + :style="[customStyle, popupStyle, { zIndex: elZIndex - 1}]"
  6 + hover-stop-propagation
  7 + >
  8 + <!-- mask -->
  9 + <view
  10 + class="tn-popup__mask"
  11 + :class="[{'tn-popup__mask--show': showPopup && mask}]"
  12 + :style="{zIndex: elZIndex - 2}"
  13 + @tap="maskClick"
  14 + @touchmove.stop.prevent = "() => {}"
  15 + hover-stop-propagation
  16 + ></view>
  17 + <!-- 弹框内容 -->
  18 + <view
  19 + class="tn-popup__content"
  20 + :class="[
  21 + mode !== 'center' ? backgroundColorClass : '',
  22 + safeAreaInsetBottom ? 'tn-safe-area-inset-bottom' : '',
  23 + 'tn-popup--' + mode,
  24 + showPopup ? 'tn-popup__content--visible' : '',
  25 + zoom && mode === 'center' ? 'tn-popup__content__center--animation-zoom' : ''
  26 + ]"
  27 + :style="[contentStyle]"
  28 + @tap="modeCenterClose"
  29 + @touchmove.stop.prevent
  30 + @tap.stop.prevent
  31 + >
  32 + <!-- 居中时候的内容 -->
  33 + <view
  34 + v-if="mode === 'center'"
  35 + class="tn-popup__content__center_box"
  36 + :class="[backgroundColorClass]"
  37 + :style="[centerStyle]"
  38 + @touchmove.stop.prevent
  39 + @tap.stop.prevent
  40 + >
  41 + <!-- 关闭按钮 -->
  42 + <view
  43 + v-if="closeBtn"
  44 + class="tn-popup__close"
  45 + :class="[`tn-icon-${closeBtnIcon}`, `tn-popup__close--${closeBtnPosition}`]"
  46 + :style="[closeBtnStyle, {zIndex: elZIndex}]"
  47 + @tap="close"
  48 + ></view>
  49 + <scroll-view scroll-y class="tn-popup__content__scroll-view">
  50 + <slot></slot>
  51 + </scroll-view>
  52 + </view>
  53 +
  54 + <!-- 除居中外的其他情况 -->
  55 + <scroll-view scroll-y v-else class="tn-popup__content__scroll-view">
  56 + <slot></slot>
  57 + </scroll-view>
  58 + <!-- 关闭按钮 -->
  59 + <view
  60 + v-if="mode !== 'center' && closeBtn"
  61 + class="tn-popup__close"
  62 + :class="[`tn-popup__close--${closeBtnPosition}`]"
  63 + :style="{zIndex: elZIndex}"
  64 + @tap="close"
  65 + >
  66 + <view :class="[`tn-icon-${closeBtnIcon}`]" :style="[closeBtnStyle]"></view>
  67 + </view>
  68 + </view>
  69 + </view>
  70 +</template>
  71 +
  72 +<script>
  73 + import componentsColorMixin from '../../components_color.js'
  74 + // 格式化字符串工具
  75 + import string from '../../string.js'
  76 + export default {
  77 + mixins: [componentsColorMixin],
  78 + name: 'tn-popup',
  79 + props: {
  80 + value: {
  81 + type: Boolean,
  82 + default: false
  83 + },
  84 + // 弹出方向
  85 + // left/right/top/bottom/center
  86 + mode: {
  87 + type: String,
  88 + default: 'left'
  89 + },
  90 + // 是否显示遮罩
  91 + mask: {
  92 + type: Boolean,
  93 + default: true
  94 + },
  95 + // 抽屉的宽度(mode=left/right),高度(mode=top/bottom)
  96 + length: {
  97 + type: [Number, String],
  98 + default: 'auto'
  99 + },
  100 + // 宽度,只对左,右,中部弹出时起作用,单位rpx,或者"auto"
  101 + // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
  102 + width: {
  103 + type: String,
  104 + default: ''
  105 + },
  106 + // 高度,只对上,下,中部弹出时起作用,单位rpx,或者"auto"
  107 + // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
  108 + height: {
  109 + type: String,
  110 + default: ''
  111 + },
  112 + // 是否开启动画,只在mode=center有效
  113 + zoom: {
  114 + type: Boolean,
  115 + default: true
  116 + },
  117 + // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
  118 + safeAreaInsetBottom: {
  119 + type: Boolean,
  120 + default: false
  121 + },
  122 + // 是否可以通过点击遮罩进行关闭
  123 + maskCloseable: {
  124 + type: Boolean,
  125 + default: true
  126 + },
  127 + // 用户自定义样式
  128 + customStyle: {
  129 + type: Object,
  130 + default() {
  131 + return {}
  132 + }
  133 + },
  134 + // 显示圆角的大小
  135 + borderRadius: {
  136 + type: Number,
  137 + default: 0
  138 + },
  139 + // zIndex
  140 + zIndex: {
  141 + type: Number,
  142 + default: 0
  143 + },
  144 + // 是否显示关闭按钮
  145 + closeBtn: {
  146 + type: Boolean,
  147 + default: false
  148 + },
  149 + // 关闭按钮的图标
  150 + closeBtnIcon: {
  151 + type: String,
  152 + default: 'close'
  153 + },
  154 + // 关闭按钮显示的位置
  155 + // top-left/top-right/bottom-left/bottom-right
  156 + closeBtnPosition: {
  157 + type: String,
  158 + default: 'top-right'
  159 + },
  160 + // 关闭按钮图标颜色
  161 + closeIconColor: {
  162 + type: String,
  163 + default: '#AAAAAA'
  164 + },
  165 + // 关闭按钮图标的大小
  166 + closeIconSize: {
  167 + type: Number,
  168 + default: 30
  169 + },
  170 + // 给一个负的margin-top,往上偏移,避免和键盘重合的情况,仅在mode=center时有效
  171 + negativeTop: {
  172 + type: Number,
  173 + default: 0
  174 + },
  175 + // marginTop,在mode = top,left,right时生效,避免用户使用了自定义导航栏,组件把导航栏遮挡了
  176 + marginTop: {
  177 + type: Number,
  178 + default: 0
  179 + },
  180 + // 此为内部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件
  181 + // 对v-model双向绑定多层调用造成报错不能修改props值的问题
  182 + popup: {
  183 + type: Boolean,
  184 + default: true
  185 + },
  186 + },
  187 + computed: {
  188 + // 处理使用了自定义导航栏时被遮挡的问题
  189 + popupStyle() {
  190 + let style = {}
  191 + if ((this.mode === 'top' || this.mode === 'left' || this.mode === 'right') && this.marginTop) {
  192 + style.marginTop = string.getLengthUnitValue(this.marginTop, 'px')
  193 + }
  194 +
  195 + return style
  196 + },
  197 + // 根据mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
  198 + contentStyle() {
  199 + let style = {}
  200 + // 如果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏
  201 + if (this.mode === 'left' || this.mode === 'right') {
  202 + style = {
  203 + width: this.width ? string.getLengthUnitValue(this.width) : string.getLengthUnitValue(this.length),
  204 + height: '100%',
  205 + transform: `translate3D(${this.mode === 'left' ? '-100%' : '100%'}, 0px, 0px)`
  206 + }
  207 + } else if (this.mode === 'top' || this.mode === 'bottom') {
  208 + style = {
  209 + width: '100%',
  210 + height: this.height ? string.getLengthUnitValue(this.height) : string.getLengthUnitValue(this.length),
  211 + transform: `translate3D(0px, ${this.mode === 'top' ? '-100%': '100%'}, 0px)`
  212 + }
  213 + }
  214 + style.zIndex = this.elZIndex
  215 + // 如果设置了圆角的值,添加弹窗的圆角
  216 + if (this.borderRadius) {
  217 + switch(this.mode) {
  218 + case 'left':
  219 + style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0`
  220 + break
  221 + case 'top':
  222 + style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx`
  223 + break
  224 + case 'right':
  225 + style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx`
  226 + break
  227 + case 'bottom':
  228 + style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0`
  229 + break
  230 + }
  231 + style.overflow = 'hidden'
  232 + }
  233 +
  234 + if (this.backgroundColorStyle && this.mode !== 'center') {
  235 + style.backgroundColor = this.backgroundColorStyle
  236 + }
  237 +
  238 + return style
  239 + },
  240 + // 中部弹窗的样式
  241 + centerStyle() {
  242 + let style = {}
  243 + style.width = this.width ? string.getLengthUnitValue(this.width) : string.getLengthUnitValue(this.length)
  244 + // 中部弹出的模式,如果没有设置高度,就用auto值,由内容撑开
  245 + style.height = this.height ? string.getLengthUnitValue(this.height) : 'auto'
  246 + style.zIndex = this.elZIndex
  247 + if (this.negativeTop) {
  248 + style.marginTop = `-${string.getLengthUnitValue(this.negativeTop)}`
  249 + }
  250 + if (this.borderRadius) {
  251 + style.borderRadius = `${this.borderRadius}rpx`
  252 + style.overflow='hidden'
  253 + }
  254 + if (this.backgroundColorStyle) {
  255 + style.backgroundColor = this.backgroundColorStyle
  256 + }
  257 + return style
  258 + },
  259 + // 关闭按钮样式
  260 + closeBtnStyle() {
  261 + let style = {}
  262 + if (this.closeIconColor) {
  263 + style.color = this.closeIconColor
  264 + }
  265 + if (this.closeIconSize) {
  266 + style.fontSize = this.closeIconSize + 'rpx'
  267 + }
  268 +
  269 + return style
  270 + },
  271 + elZIndex() {
  272 + return this.zIndex ? this.zIndex : 20075
  273 + }
  274 + },
  275 + data() {
  276 + return {
  277 + timer: null,
  278 + visibleSync: false,
  279 + showPopup: false,
  280 + closeFromInner: false
  281 + }
  282 + },
  283 + watch: {
  284 + value(val) {
  285 + if (val) {
  286 + // console.log(this.visibleSync);
  287 + if (this.visibleSync) {
  288 + this.visibleSync = false
  289 + return
  290 + }
  291 + this.open()
  292 + } else if (!this.closeFromInner) {
  293 + this.close()
  294 + }
  295 + this.closeFromInner = false
  296 + }
  297 + },
  298 + mounted() {
  299 + // 组件渲染完成时,检查value是否为true,如果是,弹出popup
  300 + this.value && this.open()
  301 + },
  302 + methods: {
  303 + // 点击遮罩
  304 + maskClick() {
  305 + if (!this.maskCloseable) return
  306 + this.close()
  307 + },
  308 + open() {
  309 + this.change('visibleSync', 'showPopup', true)
  310 + },
  311 + // 关闭弹框
  312 + close() {
  313 + // 标记关闭是内部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close
  314 + // 造成@close事件触发两次
  315 + this.closeFromInner = true
  316 + this.change('showPopup', 'visibleSync', false)
  317 + },
  318 + // 中部弹出时,需要.tn-drawer-content将内容居中,此元素会铺满屏幕,点击需要关闭弹窗
  319 + // 让其只在mode=center时起作用
  320 + modeCenterClose() {
  321 + if (this.mode != 'center' || !this.maskCloseable) return
  322 + this.close()
  323 + },
  324 + // 关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
  325 + // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
  326 + change(param1, param2, status) {
  327 + // 如果this.popup为false,意味着为picker,actionsheet等组件调用了popup组件
  328 + if (this.popup === true) {
  329 + this.$emit('input', status)
  330 + }
  331 + this[param1] = status
  332 + if (status) {
  333 + // #ifdef H5 || MP
  334 + this.timer = setTimeout(() => {
  335 + this[param2] = status
  336 + this.$emit(status ? 'open' : 'close')
  337 + clearTimeout(this.timer)
  338 + }, 10)
  339 + // #endif
  340 + // #ifndef H5 || MP
  341 + this.$nextTick(() => {
  342 + this[param2] = status
  343 + this.$emit(status ? 'open' : 'close')
  344 + })
  345 + // #endif
  346 + } else {
  347 + this.timer = setTimeout(() => {
  348 + this[param2] = status
  349 + this.$emit(status ? 'open' : 'close')
  350 + clearTimeout(this.timer)
  351 + }, 250)
  352 + }
  353 + }
  354 + }
  355 + }
  356 +</script>
  357 +
  358 +<style lang="scss" scoped>
  359 + @import '../../theme.scss';
  360 + .tn-popup {
  361 + /* #ifndef APP-NVUE */
  362 + display: block;
  363 + /* #endif */
  364 + position: fixed;
  365 + top: 0;
  366 + left: 0;
  367 + right: 0;
  368 + bottom: 0;
  369 + overflow: hidden;
  370 + z-index: 29091 !important;
  371 +
  372 + &__content {
  373 + /* #ifndef APP-NVUE */
  374 + display: block;
  375 + /* #endif */
  376 + position: absolute;
  377 + transition: all 0.25s linear;
  378 +
  379 + &--visible {
  380 + transform: translate3D(0px, 0px, 0px) !important;
  381 + &.tn-popup--center {
  382 + transform: scale(1);
  383 + opacity: 1;
  384 + }
  385 + }
  386 +
  387 + &__center_box {
  388 + min-width: 100rpx;
  389 + min-height: 100rpx;
  390 + /* #ifndef APP-NVUE */
  391 + display: block;
  392 + /* #endif */
  393 + position: relative;
  394 + background-color: #FFFFFF;
  395 + }
  396 +
  397 + &__scroll-view {
  398 + width: 100%;
  399 + height: 100%;
  400 + }
  401 +
  402 + &__center--animation-zoom {
  403 + transform: scale(1.15);
  404 + }
  405 + }
  406 +
  407 + &__scroll_view {
  408 + width: 100%;
  409 + height: 100%;
  410 + }
  411 +
  412 + &--left {
  413 + top: 0;
  414 + bottom: 0;
  415 + left: 0;
  416 + background-color: #FFFFFF;
  417 + }
  418 +
  419 + &--right {
  420 + top: 0;
  421 + bottom: 0;
  422 + right: 0;
  423 + background-color: #FFFFFF;
  424 + }
  425 +
  426 + &--top {
  427 + left: 0;
  428 + right: 0;
  429 + top: 0;
  430 + background-color: #FFFFFF;
  431 + }
  432 +
  433 + &--bottom {
  434 + left: 0;
  435 + right: 0;
  436 + bottom: 0;
  437 + background-color: #FFFFFF;
  438 + }
  439 +
  440 + &--center {
  441 + display: flex;
  442 + flex-direction: column;
  443 + bottom: 0;
  444 + top: 0;
  445 + left: 0;
  446 + right: 0;
  447 + justify-content: center;
  448 + align-items: center;
  449 + opacity: 0;
  450 + }
  451 +
  452 + &__close {
  453 + position: absolute;
  454 +
  455 + &--top-left {
  456 + top: 30rpx;
  457 + left: 30rpx;
  458 + }
  459 +
  460 + &--top-right {
  461 + top: 30rpx;
  462 + right: 30rpx;
  463 + }
  464 +
  465 + &--bottom-left {
  466 + bottom: 30rpx;
  467 + left: 30rpx;
  468 + }
  469 +
  470 + &--bottom-right {
  471 + bottom: 30rpx;
  472 + right: 30rpx;
  473 + }
  474 + }
  475 +
  476 + &__mask {
  477 + width: 100%;
  478 + height: 100%;
  479 + position: fixed;
  480 + top: 0;
  481 + left: 0;
  482 + right: 0;
  483 + border: 0;
  484 + background-color: $tn-mask-bg-color;
  485 + transition: 0.25s linear;
  486 + transition-property: opacity;
  487 + opacity: 0;
  488 +
  489 + &--show {
  490 + opacity: 1;
  491 + }
  492 + }
  493 + }
  494 +</style>
... ...
  1 +// 格式化字符串工具
  2 +import string from './string.js'
  3 +// 获取颜色工具
  4 +import color from './color.js'
  5 +module.exports = {
  6 + data() {
  7 +
  8 + },
  9 + props: {
  10 + // 背景颜色
  11 + backgroundColor: {
  12 + type: String,
  13 + default: ''
  14 + },
  15 + // 字体颜色
  16 + fontColor: {
  17 + type: String,
  18 + default: ''
  19 + },
  20 + // 字体大小
  21 + fontSize: {
  22 + type: Number,
  23 + default: 0
  24 + },
  25 + // 字体大小单位
  26 + fontUnit: {
  27 + type: String,
  28 + default: 'rpx'
  29 + }
  30 + },
  31 + computed: {
  32 + backgroundColorStyle() {
  33 + return color.getBackgroundColorStyle(this.backgroundColor)
  34 + },
  35 + backgroundColorClass() {
  36 + return color.getBackgroundColorInternalClass(this.backgroundColor)
  37 + },
  38 + fontColorStyle() {
  39 + return color.getFontColorStyle(this.fontColor)
  40 + },
  41 + fontColorClass() {
  42 + return color.getFontColorInternalClass(this.fontColor)
  43 + },
  44 + fontSizeStyle() {
  45 + string.getLengthUnitValue(this.fontSize, this.fontUnit)
  46 + }
  47 + },
  48 + methods: {
  49 +
  50 + }
  51 +}
\ No newline at end of file
... ...
  1 +{
  2 + "id": "tn-popup",
  3 + "displayName": "tn-popup 弹出层 全面兼容vue2、app、h5、wx小程序",
  4 + "version": "1.0.0",
  5 + "description": "弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义",
  6 + "keywords": [
  7 + "tuniaoui",
  8 + "tuniao",
  9 + "tn-popup",
  10 + "popup",
  11 + "弹出层"
  12 +],
  13 + "repository": "",
  14 + "engines": {
  15 + "HBuilderX": "^3.1.0"
  16 + },
  17 + "dcloudext": {
  18 + "type": "component-vue",
  19 + "sale": {
  20 + "regular": {
  21 + "price": "0.00"
  22 + },
  23 + "sourcecode": {
  24 + "price": "0.00"
  25 + }
  26 + },
  27 + "contact": {
  28 + "qq": ""
  29 + },
  30 + "declaration": {
  31 + "ads": "无",
  32 + "data": "无",
  33 + "permissions": "无"
  34 + },
  35 + "npmurl": ""
  36 + },
  37 + "uni_modules": {
  38 + "dependencies": [],
  39 + "encrypt": [],
  40 + "platforms": {
  41 + "cloud": {
  42 + "tcb": "y",
  43 + "aliyun": "y",
  44 + "alipay": "y"
  45 + },
  46 + "client": {
  47 + "Vue": {
  48 + "vue2": "y",
  49 + "vue3": "u"
  50 + },
  51 + "App": {
  52 + "app-vue": "y",
  53 + "app-nvue": "u",
  54 + "app-uvue": "u"
  55 + },
  56 + "H5-mobile": {
  57 + "Safari": "y",
  58 + "Android Browser": "y",
  59 + "微信浏览器(Android)": "y",
  60 + "QQ浏览器(Android)": "y"
  61 + },
  62 + "H5-pc": {
  63 + "Chrome": "y",
  64 + "IE": "y",
  65 + "Edge": "y",
  66 + "Firefox": "y",
  67 + "Safari": "y"
  68 + },
  69 + "小程序": {
  70 + "微信": "y",
  71 + "阿里": "u",
  72 + "百度": "u",
  73 + "字节跳动": "u",
  74 + "QQ": "u",
  75 + "钉钉": "u",
  76 + "快手": "u",
  77 + "飞书": "u",
  78 + "京东": "u"
  79 + },
  80 + "快应用": {
  81 + "华为": "u",
  82 + "联盟": "u"
  83 + }
  84 + }
  85 + }
  86 + }
  87 +}
\ No newline at end of file
... ...
  1 +# Popup 弹出层
  2 +> 组件名: tn-popup
  3 +> 弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义。
  4 +# 提示
  5 +> popup弹出层中对应自定义内容的容器中已经内置了scroll-view元素,内如内容超出容器的高度,将会自动获得垂直滚动的特性,如果您因为在slot内容做了滚动的处理,而造成了 冲突的话,请移除自定义关于滚动部分的逻辑。
  6 +# [查看文档](https://vue2.tuniaokj.com/components/popup.html)
  7 +## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=tuniao-tuniaoui)
  8 +## [问题反馈/提出建议](https://vue2.tuniaokj.com/cooperation/about.html)
  9 +![](https://vue2.tuniaokj.com/index/index.jpg)
\ No newline at end of file
... ...
  1 +/**
  2 + * 去掉字符串中空格
  3 + *
  4 + * @param {String} str 待处理的字符串
  5 + * @param {String} type 处理类型
  6 + */
  7 +function trim(str, type = 'both') {
  8 + if (type === 'both') {
  9 + return str.replace(/^\s+|\s+$/g, "")
  10 + } else if (type === 'left') {
  11 + return str.replace(/^\s*/g, "")
  12 + } else if (type === 'right') {
  13 + return str.replace(/(\s*$)/g, "")
  14 + } else if (type === 'all') {
  15 + return str.replace(/\s+/g, "")
  16 + } else {
  17 + return str
  18 + }
  19 +}
  20 +
  21 +/**
  22 + * 获取带单位的长度值
  23 + *
  24 + * @param {String} value 待处理的值
  25 + * @param {String} unit 单位
  26 + */
  27 +function getLengthUnitValue(value, unit = 'rpx') {
  28 + if (!value) {
  29 + return ''
  30 + }
  31 + if (/(%|px|rpx|auto)$/.test(value)) return value
  32 + else return value + unit
  33 +}
  34 +
  35 +/**
  36 + * 将驼峰命名的字符串转换为指定连接符来进行连接
  37 + *
  38 + * @param {Object} string 待转换的字符串
  39 + * @param {Object} replace 进行连接的字符
  40 + */
  41 +function humpConvertChar(string, replace = '_') {
  42 + if (!string || !replace) {
  43 + return ''
  44 + }
  45 + return string.replace(/([A-Z])/g, `${replace}$1`).toLowerCase()
  46 +}
  47 +
  48 +/**
  49 + * 将用指定连接符来进行连接的字符串转为驼峰命名的字符串
  50 + *
  51 + * @param {Object} string 待转换的字符串
  52 + * @param {Object} replace 进行连接的字符
  53 + */
  54 +function charConvertHump(string, replace = '_') {
  55 + if (!string || !replace) {
  56 + return ''
  57 + }
  58 + let reg = RegExp(replace + "(\\w)", "g")
  59 + return string.replace(reg, function(all, letter) {
  60 + return letter.toUpperCase()
  61 + })
  62 +}
  63 +
  64 +export default {
  65 + trim,
  66 + getLengthUnitValue,
  67 + humpConvertChar,
  68 + charConvertHump
  69 +}
\ No newline at end of file
... ...
  1 +// 此文件为TuniaoUI的主题变量,这些变量目前只能通过uni.scss引入才有效,另外由于
  2 +// uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大,
  3 +// 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入
  4 +
  5 +// 组件配置
  6 +$tn-form-item-height: 70rpx;
  7 +
  8 +
  9 +// 主颜色
  10 +$tn-main-color: #01BEFF;
  11 +$tn-main-orange: #FBBD12;
  12 +$tn-embellished-green: #00FFC6;
  13 +$tn-embellished-yellow: #FFF00D;
  14 +$tn-auxiliary-powder: #FF71D2;
  15 +$tn-auxiliary-blue: #82B2FF;
  16 +$tn-bg-color: #FFFFFF;
  17 +$tn-bg-gray-color: #F4F4F4;
  18 +$tn-space-color: #F8F7F8;
  19 +
  20 +// 边框颜色
  21 +$tn-border-solid-color: rgba(0, 0, 0, 0.1);
  22 +$tn-border-solids-color: #eee;
  23 +$tn-border-dashed-color: #ddd;
  24 +
  25 +// 阴影颜色
  26 +$tn-shadow-color: rgba(0, 0, 0, 0.1);
  27 +$tn-box-shadow-color: rgba(0, 0, 0, 0.2);
  28 +
  29 +// 字体颜色
  30 +$tn-font-color: #080808;
  31 +$tn-font-sub-color: #AAAAAA;
  32 +$tn-content-color: #838383;
  33 +$tn-font-holder-color: #E6E6E6;
  34 +
  35 +$tn-mask-bg-color: rgba(0, 0, 0, 0.4);
  36 +
  37 +$tn-progress-bg-color: #f0f0f0;
  38 +
  39 +
  40 +$tn-color-red: #E83A30;
  41 +$tn-color-red-dark: #BA2E26;
  42 +$tn-color-red-disabled: #F39C97;
  43 +$tn-color-red-light: #FAD8D6;
  44 +
  45 +$tn-color-purplered: #E72F8C;
  46 +$tn-color-purplered-dark: #B9266F;
  47 +$tn-color-purplered-disabled: #F397C5;
  48 +$tn-color-purplered-light: #FAD5E8;
  49 +
  50 +$tn-color-purple: #892FE8;
  51 +$tn-color-purple-dark: #6E26BA;
  52 +$tn-color-purple-disabled: #C497F3;
  53 +$tn-color-purple-light: #E7D5FA;
  54 +
  55 +$tn-color-bluepurple: #5F4FD9;
  56 +$tn-color-bluepurple-dark: #4C3FAE;
  57 +$tn-color-bluepurple-disabled: #AFA7EC;
  58 +$tn-color-bluepurple-light: #DFDCF7;
  59 +
  60 +$tn-color-aquablue: #3646FF;
  61 +$tn-color-aquablue-dark: #2B38CC;
  62 +$tn-color-aquablue-disabled: #9AA2FF;
  63 +$tn-color-aquablue-light: #D7DAFF;
  64 +
  65 +$tn-color-blue: #3D7EFF;
  66 +$tn-color-blue-dark: #3165CC;
  67 +$tn-color-blue-disabled: #9EBEFF;
  68 +$tn-color-blue-light: #D8E5FF;
  69 +
  70 +$tn-color-indigo: #31C9E8;
  71 +$tn-color-indigo-dark: #27A1BA;
  72 +$tn-color-indigo-disabled: #98E4F3;
  73 +$tn-color-indigo-light: #D6F4FA;
  74 +
  75 +$tn-color-cyan: #2DE8BD;
  76 +$tn-color-cyan-dark: #24BA97;
  77 +$tn-color-cyan-disabled: #96F3DE;
  78 +$tn-color-cyan-light: #D5FAF2;
  79 +
  80 +$tn-color-teal: #24F083;
  81 +$tn-color-teal-dark: #1DC069;
  82 +$tn-color-teal-disabled: #91F7C1;
  83 +$tn-color-teal-light: #D3FCE6;
  84 +
  85 +$tn-color-green: #31E749;
  86 +$tn-color-green-dark: #27B93A;
  87 +$tn-color-green-disabled: #98F3A4;
  88 +$tn-color-green-light: #D6FADB;
  89 +
  90 +$tn-color-yellowgreen: #A4E82F;
  91 +$tn-color-yellowgreen-dark: #82BA26;
  92 +$tn-color-yellowgreen-disabled: #D1F397;
  93 +$tn-color-yellowgreen-light: #EDFAD5;
  94 +
  95 +$tn-color-lime: #D5EB00;
  96 +$tn-color-lime-dark: #AABC00;
  97 +$tn-color-lime-disabled: #E9F57F;
  98 +$tn-color-lime-light: #F7FBCC;
  99 +
  100 +$tn-color-yellow: #FFF420;
  101 +$tn-color-yellow-dark: #CCC21A;
  102 +$tn-color-yellow-disabled: #FFF88F;
  103 +$tn-color-yellow-light: #FFFDD2;
  104 +
  105 +$tn-color-orangeyellow: #FFCA28;
  106 +$tn-color-orangeyellow-dark: #CCA220;
  107 +$tn-color-orangeyellow-disabled: #FFE493;
  108 +$tn-color-orangeyellow-light: #FFF4D4;
  109 +
  110 +$tn-color-orange: #FFA726;
  111 +$tn-color-orange-dark: #CC851E;
  112 +$tn-color-orange-disabled: #FFD392;
  113 +$tn-color-orange-light: #FFEDD4 ;
  114 +
  115 +$tn-color-orangered: #FF7043;
  116 +$tn-color-orangered-dark: #CC5A36;
  117 +$tn-color-orangered-disabled: #FFB7A1;
  118 +$tn-color-orangered-light: #FFE2D9;
  119 +
  120 +$tn-color-brown: #914F2C;
  121 +$tn-color-brown-dark: #743F23;
  122 +$tn-color-brown-disabled: #C8A795;
  123 +$tn-color-brown-light: #E9DCD5;
  124 +
  125 +$tn-color-grey: #78909C;
  126 +$tn-color-grey-dark: #5F7E8B;
  127 +$tn-color-grey-disabled: #C6D1D8;
  128 +$tn-color-grey-light: #E4E9EC;
  129 +
  130 +$tn-color-gray: #AAAAAA;
  131 +$tn-color-gray-dark: #838383;
  132 +$tn-color-gray-disabled: #E6E6E6;
  133 +$tn-color-gray-light: #F8F7F8;
  134 +
  135 +$tn-cool-bg-color-1-start: #F5317F;
  136 +$tn-cool-bg-color-1-end: #FF7C6E;
  137 +
  138 +$tn-cool-bg-color-2-start: #CA26FF;
  139 +$tn-cool-bg-color-2-end: #F360A7;
  140 +
  141 +$tn-cool-bg-color-3-start: #A26FFC;
  142 +$tn-cool-bg-color-3-end: #9D12FF;
  143 +
  144 +$tn-cool-bg-color-4-start: #AA77F0;
  145 +$tn-cool-bg-color-4-end: #E871E5;
  146 +
  147 +$tn-cool-bg-color-5-start: #40A0F7;
  148 +$tn-cool-bg-color-5-end: #4866E6;
  149 +
  150 +$tn-cool-bg-color-6-start: #209CFF;
  151 +$tn-cool-bg-color-6-end: #68E0CF;
  152 +
  153 +$tn-cool-bg-color-7-start: #00C3FF;
  154 +$tn-cool-bg-color-7-end: #58FFF5;
  155 +
  156 +$tn-cool-bg-color-8-start: #00d1FF;
  157 +$tn-cool-bg-color-8-end: #69FF97;
  158 +
  159 +$tn-cool-bg-color-9-start: #0FD893;
  160 +$tn-cool-bg-color-9-end: #29ECBF;
  161 +
  162 +$tn-cool-bg-color-10-start: #0FD850;
  163 +$tn-cool-bg-color-10-end: #F9F047;
  164 +
  165 +$tn-cool-bg-color-11-start: #24FE41;
  166 +$tn-cool-bg-color-11-end: #F7FD47;
  167 +
  168 +$tn-cool-bg-color-12-start: #D6FF7F;
  169 +$tn-cool-bg-color-12-end: #00F657;
  170 +
  171 +$tn-cool-bg-color-13-start: #FA709A;
  172 +$tn-cool-bg-color-13-end: #FEE140;
  173 +
  174 +$tn-cool-bg-color-14-start: #FE5E9C;
  175 +$tn-cool-bg-color-14-end: #F1AA76;
  176 +
  177 +$tn-cool-bg-color-15-start: #FF3181;
  178 +$tn-cool-bg-color-15-end: #FF8331;
  179 +
  180 +$tn-cool-bg-color-16-start: #ED1C24;
  181 +$tn-cool-bg-color-16-end: #FECE12;
  182 +
  183 +
... ...