Commit d15d96df62d87dcb0c6ce643765e2813da771d31

Authored by xp.Huang
2 parents 521f917a 9a4fb61f

Merge branch 'local_dev_ft_wip_configuation' into 'main_dev'

模板组态的修改

See merge request yunteng/thingskit-front!972
... ... @@ -8,10 +8,10 @@ VITE_GLOB_PUBLIC_PATH = /
8 8 # Please note that no line breaks
9 9
10 10 # 本地
11   -VITE_PROXY = [["/api","http://localhost:8080/api"],["/thingskit-scada","http://localhost:5173/thingskit-scada"],["/large-designer", "http://localhost:5555/large-designer/"]]
  11 +VITE_PROXY = [["/api","http://10.0.152.126:8080/api"],["/thingskit-scada","http://localhost:5173/thingskit-scada"],["/large-designer", "http://localhost:5555/large-designer/"]]
12 12
13 13 # 实时数据的ws地址
14   -VITE_GLOB_WEB_SOCKET = ws://localhost:8080/api/ws/plugins/telemetry?token=
  14 +VITE_GLOB_WEB_SOCKET = ws://10.152.126:8080/api/ws/plugins/telemetry?token=
15 15
16 16 # Delete console
17 17 VITE_GLOB_DROP_CONSOLE = true
... ...
... ... @@ -14,6 +14,7 @@ export interface ConfigurationCenterItemsModal {
14 14 export type queryPageParams = BasicPageParams & {
15 15 name?: Nullable<string>;
16 16 organizationId?: Nullable<number>;
  17 + isTemplate?: number;
17 18 };
18 19
19 20 export interface ConfigurationModal {
... ...
... ... @@ -41,6 +41,7 @@ import ApiSelectScrollLoad from './components/ApiSelectScrollLoad.vue';
41 41 import InputGroup from './components/InputGroup.vue';
42 42 import RegisterAddressInput from '/@/views/task/center/components/PollCommandInput/RegisterAddressInput.vue';
43 43 import ExtendDesc from '/@/components/Form/src/externalCompns/components/ExtendDesc/index.vue';
  44 +import DeviceProfileForm from '/@/components/Form/src/externalCompns/components/DeviceProfileForm/index.vue';
44 45
45 46 const componentMap = new Map<ComponentType, Component>();
46 47
... ... @@ -89,6 +90,7 @@ componentMap.set('ApiSelectScrollLoad', ApiSelectScrollLoad);
89 90 componentMap.set('InputGroup', InputGroup);
90 91 componentMap.set('RegisterAddressInput', RegisterAddressInput);
91 92 componentMap.set('ExtendDesc', ExtendDesc);
  93 +componentMap.set('DeviceProfileForm', DeviceProfileForm);
92 94
93 95 export function add(compName: ComponentType, component: Component) {
94 96 componentMap.set(compName, component);
... ...
  1 +<template>
  2 + <div v-for="(param, index) in dynamicInput.params" :key="index" style="display: flex">
  3 + <a-input placeholder="产品" v-model:value="param.label" :disabled="true" />
  4 + <Select
  5 + placeholder="请选择设备"
  6 + v-model:value="param.value"
  7 + style="width: 100%"
  8 + v-bind="createPickerSearch()"
  9 + mode="multiple"
  10 + labelInValue
  11 + />
  12 + <MinusCircleOutlined
  13 + v-if="dynamicInput.params.length > min && !disabled"
  14 + class="dynamic-delete-button"
  15 + @click="remove(param)"
  16 + style="width: 50px"
  17 + />
  18 + </div>
  19 +</template>
  20 +<script lang="ts" name="DeviceProfileForm">
  21 + import { MinusCircleOutlined } from '@ant-design/icons-vue';
  22 + import { defineComponent, reactive, UnwrapRef, watchEffect } from 'vue';
  23 + import { propTypes } from '/@/utils/propTypes';
  24 + import { isEmpty } from '/@/utils/is';
  25 + import { Select } from 'ant-design-vue';
  26 + import { createPickerSearch } from '/@/utils/pickerSearch';
  27 +
  28 + interface Params {
  29 + label: string;
  30 + value: string;
  31 + }
  32 +
  33 + export default defineComponent({
  34 + name: 'DeviceProfileForm',
  35 + components: {
  36 + MinusCircleOutlined,
  37 + Select,
  38 + },
  39 + //--------------不继承Antd Design Vue Input的所有属性 否则控制台报大片警告--------------
  40 + inheritAttrs: false,
  41 + props: {
  42 + value: propTypes.object.def({}),
  43 + //自定义删除按钮多少才会显示
  44 + min: propTypes.integer.def(0),
  45 + disabled: {
  46 + type: Boolean,
  47 + default: false,
  48 + },
  49 + },
  50 + emits: ['change', 'update:value'],
  51 + setup(props, { emit }) {
  52 + //input动态数据
  53 + const dynamicInput: UnwrapRef<{ params: Params[] }> = reactive({ params: [] });
  54 +
  55 + //删除Input
  56 + const remove = (item: Params) => {
  57 + let index = dynamicInput.params.indexOf(item);
  58 + if (index !== -1) {
  59 + dynamicInput.params.splice(index, 1);
  60 + }
  61 + emitChange();
  62 + };
  63 +
  64 + //监听传入数据value
  65 + watchEffect(() => {
  66 + initVal();
  67 + });
  68 +
  69 + /**
  70 + * 初始化数值
  71 + */
  72 + function initVal() {
  73 + dynamicInput.params = [];
  74 + if (props.value) {
  75 + let jsonObj = props.value;
  76 + Object.keys(jsonObj).forEach((key) => {
  77 + dynamicInput.params.push({ label: key, value: jsonObj[key] });
  78 + });
  79 + }
  80 + }
  81 +
  82 + /**
  83 + * 数值改变
  84 + */
  85 + function emitChange() {
  86 + let obj = {};
  87 + if (dynamicInput.params.length > 0) {
  88 + dynamicInput.params.forEach((item) => {
  89 + obj[item.label] = item.value;
  90 + });
  91 + }
  92 + emit('change', isEmpty(obj) ? '' : obj);
  93 + emit('update:value', isEmpty(obj) ? '' : obj);
  94 + }
  95 +
  96 + return {
  97 + dynamicInput,
  98 + emitChange,
  99 + remove,
  100 + createPickerSearch,
  101 + };
  102 + },
  103 + });
  104 +</script>
  105 +<style scoped>
  106 + .dynamic-delete-button {
  107 + cursor: pointer;
  108 + position: relative;
  109 + top: 4px;
  110 + font-size: 24px;
  111 + color: #999;
  112 + transition: all 0.3s;
  113 + }
  114 +
  115 + .dynamic-delete-button:hover {
  116 + color: #777;
  117 + }
  118 +
  119 + .dynamic-delete-button[disabled] {
  120 + cursor: not-allowed;
  121 + opacity: 0.5;
  122 + }
  123 +</style>
... ...
... ... @@ -137,4 +137,5 @@ export type ComponentType =
137 137 | 'CorrelationFilters'
138 138 | 'RelationsQuery'
139 139 | 'CredentialsCard'
140   - | 'ApiComplete';
  140 + | 'ApiComplete'
  141 + | 'DeviceProfileForm';
... ...
... ... @@ -7,36 +7,88 @@
7 7 width="30%"
8 8 @ok="handleSubmit"
9 9 >
10   - <BasicForm @register="registerForm" />
  10 + <BasicForm @register="registerForm">
  11 + <!-- 模板选择 -->
  12 + <template #templateId="{ model }">
  13 + <Select
  14 + v-model:value="model['templateId']"
  15 + placeholder="请选择模板"
  16 + style="width: 100%"
  17 + :options="selectTemplateOptions"
  18 + @change="handleTemplateChange"
  19 + v-bind="createPickerSearch()"
  20 + :disabled="templateDisabled"
  21 + />
  22 + </template>
  23 + <!-- 产品选择 -->
  24 + <template #productIds="{ model }">
  25 + <SelectDeviceProfile
  26 + v-if="model['templateId']"
  27 + ref="selectDeviceProfileRef"
  28 + :selectOptions="selectOptions"
  29 + :organizationId="model['organizationId']"
  30 + />
  31 + </template>
  32 + </BasicForm>
11 33 </BasicDrawer>
12 34 </template>
13 35 <script lang="ts">
14   - import { defineComponent, ref, computed, unref } from 'vue';
  36 + import { defineComponent, ref, computed, unref, Ref, onMounted, nextTick } from 'vue';
15 37 import { BasicForm, useForm } from '/@/components/Form';
  38 + import { Select } from 'ant-design-vue';
16 39 import { formSchema, PC_DEFAULT_CONTENT, PHONE_DEFAULT_CONTENT, Platform } from './center.data';
17 40 import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
18 41 import { useMessage } from '/@/hooks/web/useMessage';
19   - import { saveOrUpdateConfigurationCenter } from '/@/api/configuration/center/configurationCenter';
  42 + import {
  43 + saveOrUpdateConfigurationCenter,
  44 + getPage,
  45 + } from '/@/api/configuration/center/configurationCenter';
20 46 import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
21 47 import { buildUUID } from '/@/utils/uuid';
  48 + import SelectDeviceProfile from './components/SelectDeviceProfile.vue';
  49 + import { createPickerSearch } from '/@/utils/pickerSearch';
  50 + import type { queryPageParams } from '/@/api/configuration/center/model/configurationCenterModal';
  51 + import { getDeviceProfile } from '/@/api/alarm/position';
22 52
23 53 export default defineComponent({
24 54 name: 'ConfigurationDrawer',
25   - components: { BasicDrawer, BasicForm },
  55 + components: { BasicDrawer, BasicForm, SelectDeviceProfile, Select },
26 56 emits: ['success', 'register'],
27 57 setup(_, { emit }) {
28 58 const isUpdate = ref(true);
29 59
30   - const [registerForm, { validate, setFieldsValue, resetFields }] = useForm({
  60 + const selectDeviceProfileRef = ref<InstanceType<typeof SelectDeviceProfile>>();
  61 +
  62 + const [registerForm, { validate, setFieldsValue, resetFields, updateSchema }] = useForm({
31 63 labelWidth: 120,
32 64 schemas: formSchema,
33 65 showActionButtonGroup: false,
34 66 });
35 67
  68 + const updateEnableTemplate = async (enable: number, disabled: boolean) => {
  69 + await setFieldsValue({
  70 + enableTemplate: enable,
  71 + });
  72 + await updateSchema({
  73 + field: 'enableTemplate',
  74 + componentProps: {
  75 + disabled,
  76 + checkedValue: 1,
  77 + unCheckedValue: 0,
  78 + checkedChildren: '开',
  79 + unCheckedChildren: '关',
  80 + },
  81 + });
  82 + };
  83 +
36 84 const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  85 + await nextTick();
37 86 await resetFields();
38 87 setDrawerProps({ confirmLoading: false });
39 88 isUpdate.value = !!data?.isUpdate;
  89 + selectDeviceProfileRef.value?.retValue();
  90 + await updateEnableTemplate(0, false);
  91 + templateDisabled.value = false;
40 92 if (unref(isUpdate)) {
41 93 if (data.record.thumbnail) {
42 94 data.record.thumbnail = [
... ... @@ -49,9 +101,62 @@
49 101 Reflect.deleteProperty(data.record, 'organizationId');
50 102 await setFieldsValue(data.record);
51 103 }
  104 + // 业务 编辑如果有templateId 则启用模板开关禁用,模板不能选择,产品和设备可以选择
  105 + if (Reflect.get(data.record, 'templateId')) {
  106 + await updateEnableTemplate(1, true);
  107 + templateDisabled.value = true;
  108 + //回显产品和设备
  109 + const { productAndDevice } = data.record;
  110 + selectOptions.value = productAndDevice?.map((item) => ({
  111 + label: item.name,
  112 + value: item.profileId,
  113 + }));
  114 + selectDeviceProfileRef.value?.setValue(productAndDevice);
  115 + } else {
  116 + const productAndDevice = data.record['productAndDevice'];
  117 + setFieldsValue({
  118 + productId: productAndDevice?.map((item) => item.profileId),
  119 + });
  120 + }
52 121 }
53 122 });
54 123
  124 + //新增修改
  125 + const templateDisabled = ref(false);
  126 + onMounted(() => {
  127 + getTemplate({
  128 + page: 1,
  129 + pageSize: 30,
  130 + });
  131 + });
  132 +
  133 + const selectTemplateOptions: Ref<any[]> = ref([]);
  134 + const getTemplate = async (params: queryPageParams) => {
  135 + const { items } = await getPage({ ...params, isTemplate: 1 });
  136 + selectTemplateOptions.value = items.map((item) => ({
  137 + ...item,
  138 + label: item.name,
  139 + value: item.id,
  140 + }));
  141 + };
  142 +
  143 + const selectOptions: Ref<any[]> = ref([]);
  144 +
  145 + const handleTemplateChange = async (_, option) => {
  146 + const { productAndDevice } = option;
  147 + if (!productAndDevice) return;
  148 + selectOptions.value = productAndDevice.map((item) => ({
  149 + label: item.name,
  150 + value: item.profileId,
  151 + }));
  152 + await nextTick();
  153 + // 赋值
  154 + selectDeviceProfileRef.value?.setFieldsValue(
  155 + productAndDevice.map((item) => ({ label: item.name, value: item.profileId }))
  156 + );
  157 + };
  158 + //
  159 +
55 160 const getTitle = computed(() => (!unref(isUpdate) ? '新增组态中心' : '编辑组态中心'));
56 161
57 162 const getDefaultContent = (platform: Platform) => {
... ... @@ -61,10 +166,44 @@
61 166 return PHONE_DEFAULT_CONTENT;
62 167 };
63 168
  169 + // 获取产品
  170 + const getCurrentAllProduct = async (products: string[]) => {
  171 + const resp = (await getDeviceProfile()) as any;
  172 + if (!resp) return;
  173 + const values = resp?.map((item) => ({ name: item.name, profileId: item.id }));
  174 + return values.filter((item) => products?.includes(item.profileId));
  175 + };
  176 +
64 177 async function handleSubmit() {
65 178 try {
66 179 const { createMessage } = useMessage();
67 180 const values = await validate();
  181 + if (!values) return;
  182 + const selectDevice = selectDeviceProfileRef.value?.getSelectDevice();
  183 + if (values['enableTemplate'] === 1) {
  184 + if (!values['templateId']) return createMessage.error('未选择模板');
  185 + // 业务 启用模板,产品和设备必填
  186 + if (values['templateId']) {
  187 + values.templateId = values['templateId'];
  188 + if (Array.isArray(selectDevice) && selectDevice.length == 0)
  189 + return createMessage.error('您已经选择了模板,但产品或者设备未选择');
  190 + if (
  191 + selectDevice?.some(
  192 + (item) =>
  193 + !item.name ||
  194 + item.deviceList.includes('' || undefined) ||
  195 + item.deviceList.length == 0
  196 + )
  197 + ) {
  198 + return createMessage.error('您已经选择了模板,但产品或者设备未选择');
  199 + }
  200 + values.productAndDevice = selectDevice;
  201 + }
  202 + } else {
  203 + const { productId } = values;
  204 + values.productAndDevice = await getCurrentAllProduct(productId);
  205 + Reflect.deleteProperty(values, 'productId');
  206 + }
68 207 if (Reflect.has(values, 'thumbnail')) {
69 208 const file = (values.thumbnail || []).at(0) || {};
70 209 values.thumbnail = file.url || null;
... ... @@ -87,6 +226,12 @@
87 226 registerDrawer,
88 227 registerForm,
89 228 handleSubmit,
  229 + selectOptions,
  230 + selectTemplateOptions,
  231 + handleTemplateChange,
  232 + createPickerSearch,
  233 + selectDeviceProfileRef,
  234 + templateDisabled,
90 235 };
91 236 },
92 237 });
... ...
... ... @@ -83,6 +83,7 @@ export const searchFormSchema: FormSchema[] = [
83 83 },
84 84 ];
85 85
  86 +// 表单
86 87 export const formSchema: FormSchema[] = [
87 88 {
88 89 field: 'thumbnail',
... ... @@ -112,14 +113,9 @@ export const formSchema: FormSchema[] = [
112 113 onPreview: (fileList: FileItem) => {
113 114 createImgPreview({ imageList: [fileList.url!] });
114 115 },
115   - // showUploadList: {
116   - // showDownloadIcon: true,
117   - // showRemoveIcon: true,
118   - // },
119 116 };
120 117 },
121 118 },
122   -
123 119 {
124 120 field: 'name',
125 121 label: '组态名称',
... ... @@ -137,17 +133,76 @@ export const formSchema: FormSchema[] = [
137 133 component: 'OrgTreeSelect',
138 134 },
139 135 {
  136 + field: 'platform',
  137 + label: '平台',
  138 + required: true,
  139 + component: 'RadioGroup',
  140 + defaultValue: Platform.PC,
  141 + componentProps: {
  142 + defaultValue: Platform.PC,
  143 + options: [
  144 + { label: 'PC端', value: Platform.PC },
  145 + { label: '移动端', value: Platform.PHONE },
  146 + ],
  147 + },
  148 + },
  149 + {
  150 + field: 'enableTemplate', //前端控制
  151 + label: '启用模版',
  152 + component: 'Switch',
  153 + defaultValue: 0,
  154 + componentProps: ({ formActionType }) => {
  155 + const { setFieldsValue } = formActionType;
  156 + return {
  157 + checkedValue: 1,
  158 + unCheckedValue: 0,
  159 + checkedChildren: '开',
  160 + unCheckedChildren: '关',
  161 + onChange() {
  162 + setFieldsValue({
  163 + productIds: [],
  164 + });
  165 + },
  166 + };
  167 + },
  168 + },
  169 + {
140 170 field: 'isTemplate',
141   - label: '模版',
  171 + label: '默认启用模版',
142 172 component: 'Switch',
143 173 defaultValue: 0,
144 174 componentProps: {
145 175 checkedValue: 1,
146 176 unCheckedValue: 0,
147 177 },
  178 + show: false,
148 179 },
149 180 {
150   - field: 'productIds',
  181 + field: 'templateId', //暂且使用插槽形式
  182 + label: '模板',
  183 + component: 'Input',
  184 + slot: 'templateId',
  185 + colProps: { span: 24 },
  186 + ifShow: ({ values }) => values['enableTemplate'] === 1,
  187 + },
  188 + {
  189 + field: 'productIds', //暂且使用插槽形式
  190 + label: '产品',
  191 + component: 'Input',
  192 + slot: 'productIds',
  193 + colProps: { span: 24 },
  194 + ifShow: ({ values }) => values['enableTemplate'] === 1 && values['templateId'],
  195 + },
  196 + {
  197 + field: 'deviceIds', //暂且使用插槽形式
  198 + label: '设备',
  199 + component: 'Input',
  200 + slot: 'deviceIds',
  201 + colProps: { span: 24 },
  202 + ifShow: ({ values }) => values['enableTemplate'] === 1 && values['templateId'],
  203 + },
  204 + {
  205 + field: 'productId',
151 206 label: '产品',
152 207 component: 'ApiSelect',
153 208 required: true,
... ... @@ -155,22 +210,18 @@ export const formSchema: FormSchema[] = [
155 210 api: getDeviceProfile,
156 211 mode: 'multiple',
157 212 labelField: 'name',
158   - valueField: 'tbProfileId',
159   - },
160   - },
161   - {
162   - field: 'platform',
163   - label: '平台',
164   - required: true,
165   - component: 'RadioGroup',
166   - defaultValue: Platform.PC,
167   - componentProps: {
168   - defaultValue: Platform.PC,
169   - options: [
170   - { label: 'PC端', value: Platform.PC },
171   - { label: '移动端', value: Platform.PHONE },
172   - ],
  213 + valueField: 'id',
  214 + placeholder: '请选择产品',
  215 + getPopupContainer: (triggerNode) => triggerNode.parentNode,
  216 + filterOption: (inputValue: string, option: Record<'label' | 'value', string>) => {
  217 + let { label, value } = option;
  218 + label = label.toLowerCase();
  219 + value = value.toLowerCase();
  220 + inputValue = inputValue.toLowerCase();
  221 + return label.includes(inputValue) || value.includes(inputValue);
  222 + },
173 223 },
  224 + ifShow: ({ values }) => values['enableTemplate'] === 0,
174 225 },
175 226 {
176 227 field: 'remark',
... ...
  1 +<template>
  2 + <div v-for="param in dynamicInput.params" :key="param.name" class="mt-4 flex gap-2">
  3 + <a-input :disabled="true" v-model:value="param.name" class="w-1/2 flex-1" />
  4 + <Select
  5 + placeholder="请选择设备"
  6 + v-model:value="param.deviceList"
  7 + class="!w-1/2"
  8 + :options="selectOptions"
  9 + v-bind="createPickerSearch()"
  10 + @change="emitChange"
  11 + mode="multiple"
  12 + allowClear
  13 + />
  14 + </div>
  15 +</template>
  16 +<script lang="ts">
  17 + export default {
  18 + inheritAttrs: false,
  19 + };
  20 +</script>
  21 +<script lang="ts" setup name="SelectAttributes">
  22 + import { reactive, UnwrapRef, watchEffect, ref } from 'vue';
  23 + import { propTypes } from '/@/utils/propTypes';
  24 + import { Select } from 'ant-design-vue';
  25 + import { createPickerSearch } from '/@/utils/pickerSearch';
  26 + import { byOrganizationIdGetMasterDevice } from '/@/api/ruleengine/ruleengineApi';
  27 +
  28 + const props = defineProps({
  29 + value: propTypes.object.def({}),
  30 + organizationId: {
  31 + type: String,
  32 + required: true,
  33 + },
  34 + });
  35 +
  36 + const selectOptions: any = ref([]);
  37 +
  38 + //动态数据
  39 + const dynamicInput: UnwrapRef<{ params: any[] }> = reactive({ params: [] });
  40 +
  41 + const initVal = async () => {
  42 + if (props.value) {
  43 + if (props.organizationId) {
  44 + const resp = await byOrganizationIdGetMasterDevice({
  45 + organizationId: props.organizationId,
  46 + deviceProfileId: props.value.value,
  47 + });
  48 + selectOptions.value = resp.map((item) => ({
  49 + label: item.alias || item.name,
  50 + value: item.tbDeviceId,
  51 + }));
  52 + }
  53 + dynamicInput.params.push({
  54 + name: props.value.label,
  55 + profileId: props.value.value,
  56 + deviceList: props.value.deviceList?.filter(Boolean)?.map((item) => item.deviceId),
  57 + });
  58 + }
  59 + };
  60 +
  61 + //数值改变
  62 + const valEffect = watchEffect(() => {
  63 + initVal();
  64 + });
  65 +
  66 + valEffect();
  67 +
  68 + //chang改变
  69 + const emitChange = () => {
  70 + const findDeviceDict = selectOptions.value.map((item) => {
  71 + if (dynamicInput.params[0].deviceList?.includes(item.value)) {
  72 + return {
  73 + name: item.label,
  74 + deviceId: item.value,
  75 + };
  76 + }
  77 + });
  78 + return {
  79 + ...dynamicInput.params[0],
  80 + deviceList: findDeviceDict.filter(Boolean),
  81 + };
  82 + };
  83 + defineExpose({
  84 + emitChange,
  85 + });
  86 +</script>
  87 +<style scoped lang="css">
  88 + .dynamic-delete-button {
  89 + cursor: pointer;
  90 + position: relative;
  91 + top: 4px;
  92 + font-size: 24px;
  93 + color: #999;
  94 + transition: all 0.3s;
  95 + }
  96 +
  97 + .dynamic-delete-button:hover {
  98 + color: #777;
  99 + }
  100 +
  101 + .dynamic-delete-button[disabled] {
  102 + cursor: not-allowed;
  103 + opacity: 0.5;
  104 + }
  105 +</style>
... ...
  1 +<template>
  2 + <Select
  3 + placeholder="请选择产品"
  4 + v-model:value="selectValue"
  5 + style="width: 100%"
  6 + :options="selectOptions"
  7 + v-bind="createPickerSearch()"
  8 + @change="handleDeviceProfileChange"
  9 + mode="multiple"
  10 + labelInValue
  11 + />
  12 + <template v-for="(item, index) in profileList" :key="item.value">
  13 + <SelectDevice
  14 + :ref="bindDeviceRef.deviceAttrRef"
  15 + :value="item"
  16 + :index="index"
  17 + :organizationId="organizationId"
  18 + />
  19 + </template>
  20 +</template>
  21 +<script lang="ts" setup name="SelectDevice">
  22 + import { ref, Ref, PropType, unref, nextTick, onMounted } from 'vue';
  23 + import { Select } from 'ant-design-vue';
  24 + import SelectDevice from './SelectDevice.vue';
  25 + import { createPickerSearch } from '/@/utils/pickerSearch';
  26 +
  27 + import { getDeviceProfile } from '/@/api/alarm/position';
  28 +
  29 + defineProps({
  30 + selectOptions: {
  31 + type: Array as PropType<any[]>,
  32 + required: true,
  33 + },
  34 + organizationId: {
  35 + type: String,
  36 + required: true,
  37 + },
  38 + });
  39 +
  40 + const selectValue = ref([]);
  41 + const selectOptions = ref<any>([]);
  42 +
  43 + const bindDeviceRef = {
  44 + deviceAttrRef: ref([]),
  45 + };
  46 +
  47 + const profileList: Ref<any[]> = ref([]);
  48 +
  49 + const handleDeviceProfileChange = (_, options) => {
  50 + profileList.value = options;
  51 + };
  52 +
  53 + const getSelectDevice = () => {
  54 + return unref(bindDeviceRef.deviceAttrRef)?.map((item: any) => item.emitChange());
  55 + };
  56 +
  57 + const setFieldsValue = async (productIds) => {
  58 + await nextTick();
  59 + if (!productIds && !productIds.length) {
  60 + return;
  61 + }
  62 + selectValue.value = productIds;
  63 +
  64 + profileList.value = productIds;
  65 + };
  66 +
  67 + const setValue = (value: any) => {
  68 + selectValue.value = value.map((item) => ({
  69 + label: item.name,
  70 + key: item.profileId,
  71 + }));
  72 + profileList.value = value.map((item) => {
  73 + return {
  74 + label: item.name,
  75 + value: item.profileId,
  76 + deviceList: item.deviceList,
  77 + };
  78 + });
  79 + };
  80 + const retValue = () => {
  81 + selectValue.value = [];
  82 + profileList.value = [];
  83 + };
  84 +
  85 + onMounted(async () => {
  86 + const values = await getDeviceProfile();
  87 + selectOptions.value = values.map((item) => ({
  88 + label: item.name,
  89 + value: item.id,
  90 + }));
  91 + });
  92 +
  93 + defineExpose({
  94 + getSelectDevice,
  95 + setValue,
  96 + retValue,
  97 + setFieldsValue,
  98 + });
  99 +</script>
  100 +<style scoped lang="css"></style>
... ...
... ... @@ -77,6 +77,7 @@
77 77 const { items, total } = await getPage({
78 78 organizationId: unref(organizationId),
79 79 ...value,
  80 + isTemplate: 0,
80 81 page: pagination.current!,
81 82 pageSize,
82 83 });
... ...
  1 +<template>
  2 + <BasicDrawer
  3 + v-bind="$attrs"
  4 + @register="registerDrawer"
  5 + showFooter
  6 + :title="getTitle"
  7 + width="30%"
  8 + @ok="handleSubmit"
  9 + >
  10 + <BasicForm @register="registerForm" />
  11 + </BasicDrawer>
  12 +</template>
  13 +<script lang="ts">
  14 + import { defineComponent, ref, computed, unref } from 'vue';
  15 + import { BasicForm, useForm } from '/@/components/Form';
  16 + import { formSchema, PC_DEFAULT_CONTENT, PHONE_DEFAULT_CONTENT, Platform } from './center.data';
  17 + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  18 + import { useMessage } from '/@/hooks/web/useMessage';
  19 + import { saveOrUpdateConfigurationCenter } from '/@/api/configuration/center/configurationCenter';
  20 + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  21 + import { buildUUID } from '/@/utils/uuid';
  22 + import { getDeviceProfile } from '/@/api/alarm/position';
  23 +
  24 + export default defineComponent({
  25 + name: 'ConfigurationDrawer',
  26 + components: { BasicDrawer, BasicForm },
  27 + emits: ['success', 'register'],
  28 + setup(_, { emit }) {
  29 + const isUpdate = ref(true);
  30 +
  31 + const [registerForm, { validate, setFieldsValue, resetFields }] = useForm({
  32 + labelWidth: 120,
  33 + schemas: formSchema,
  34 + showActionButtonGroup: false,
  35 + });
  36 +
  37 + const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  38 + await resetFields();
  39 + setDrawerProps({ confirmLoading: false });
  40 + isUpdate.value = !!data?.isUpdate;
  41 + if (unref(isUpdate)) {
  42 + if (data.record.thumbnail) {
  43 + data.record.thumbnail = [
  44 + { uid: buildUUID(), name: 'name', url: data.record.thumbnail } as FileItem,
  45 + ];
  46 + }
  47 + if (data.record.organizationDTO) {
  48 + await setFieldsValue(data.record);
  49 + } else {
  50 + Reflect.deleteProperty(data.record, 'organizationId');
  51 + await setFieldsValue(data.record);
  52 + }
  53 + if (!data.record?.productAndDevice) return;
  54 + const productAndDevice = data.record['productAndDevice'];
  55 + setFieldsValue({
  56 + productIds: productAndDevice.map((item) => item.profileId),
  57 + });
  58 + }
  59 + });
  60 +
  61 + const getTitle = computed(() => (!unref(isUpdate) ? '新增模板' : '编辑模板'));
  62 +
  63 + const getDefaultContent = (platform: Platform) => {
  64 + if (platform === Platform.PC) {
  65 + return PC_DEFAULT_CONTENT;
  66 + }
  67 + return PHONE_DEFAULT_CONTENT;
  68 + };
  69 +
  70 + // 获取产品
  71 + const getCurrentAllProduct = async (products: string[]) => {
  72 + const resp = (await getDeviceProfile()) as any;
  73 + if (!resp) return;
  74 + const values = resp.map((item) => ({ name: item.name, profileId: item.id }));
  75 + return values.filter((item) => products.includes(item.profileId));
  76 + };
  77 +
  78 + async function handleSubmit() {
  79 + try {
  80 + const { createMessage } = useMessage();
  81 + const values = await validate();
  82 + if (!values) return;
  83 + const reflectProduct = await getCurrentAllProduct(values['productIds']);
  84 + if (Reflect.has(values, 'thumbnail')) {
  85 + const file = (values.thumbnail || []).at(0) || {};
  86 + values.thumbnail = file.url || null;
  87 + }
  88 + setDrawerProps({ confirmLoading: true });
  89 + let saveMessage = '添加成功';
  90 + let updateMessage = '修改成功';
  91 + values.defaultContent = getDefaultContent(values.platform);
  92 + values.productAndDevice = reflectProduct;
  93 + Reflect.deleteProperty(values, 'productIds');
  94 + await saveOrUpdateConfigurationCenter(values, unref(isUpdate));
  95 + closeDrawer();
  96 + emit('success');
  97 + createMessage.success(unref(isUpdate) ? updateMessage : saveMessage);
  98 + } finally {
  99 + setDrawerProps({ confirmLoading: false });
  100 + }
  101 + }
  102 +
  103 + return {
  104 + getTitle,
  105 + registerDrawer,
  106 + registerForm,
  107 + handleSubmit,
  108 + };
  109 + },
  110 + });
  111 +</script>
... ...
  1 +<template>
  2 + <div>
  3 + <PageWrapper dense contentFullHeight contentClass="flex">
  4 + <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
  5 + <BasicTable
  6 + style="flex: auto"
  7 + :clickToRowSelect="false"
  8 + @register="registerTable"
  9 + :searchInfo="searchInfo"
  10 + class="w-3/4 xl:w-4/5"
  11 + >
  12 + <template #platform="{ record }">
  13 + <Tag :color="record.platform === Platform.PHONE ? 'cyan' : 'blue'">
  14 + {{ record.platform === Platform.PHONE ? '移动端' : 'PC端' }}
  15 + </Tag>
  16 + </template>
  17 + <template #toolbar>
  18 + <Authority value="api:yt:configuration:center:post">
  19 + <a-button type="primary" @click="handleCreateOrEdit(null)"> 新增组态 </a-button>
  20 + </Authority>
  21 + <Authority value="api:yt:configuration:center:delete">
  22 + <Popconfirm
  23 + title="您确定要批量删除数据"
  24 + ok-text="确定"
  25 + cancel-text="取消"
  26 + @confirm="handleDeleteOrBatchDelete(null)"
  27 + >
  28 + <a-button type="primary" color="error" :disabled="hasBatchDelete">
  29 + 批量删除
  30 + </a-button>
  31 + </Popconfirm>
  32 + </Authority>
  33 + </template>
  34 + <template #action="{ record }">
  35 + <TableAction
  36 + :actions="[
  37 + {
  38 + label: '设计',
  39 + auth: 'api:yt:configuration:center:get_configuration_info:get',
  40 + icon: 'clarity:note-edit-line',
  41 + onClick: handleDesign.bind(null, record),
  42 + },
  43 + {
  44 + label: '预览',
  45 + auth: 'api:yt:configuration:center:get_configuration_info:get',
  46 + icon: 'ant-design:eye-outlined',
  47 + onClick: handlePreview.bind(null, record),
  48 + },
  49 + {
  50 + label: '编辑',
  51 + auth: 'api:yt:configuration:center:update',
  52 + icon: 'clarity:note-edit-line',
  53 + onClick: handleCreateOrEdit.bind(null, record),
  54 + },
  55 + {
  56 + label: '删除',
  57 + auth: 'api:yt:configuration:center:delete',
  58 + icon: 'ant-design:delete-outlined',
  59 + color: 'error',
  60 + popConfirm: {
  61 + title: '是否确认删除',
  62 + confirm: handleDeleteOrBatchDelete.bind(null, record),
  63 + },
  64 + },
  65 + ]"
  66 + />
  67 + </template>
  68 + </BasicTable>
  69 + </PageWrapper>
  70 + <ContactDrawer @register="registerDrawer" @success="handleSuccess" />
  71 + </div>
  72 +</template>
  73 +
  74 +<script lang="ts">
  75 + import { defineComponent, reactive, nextTick } from 'vue';
  76 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  77 + import { PageWrapper } from '/@/components/Page';
  78 + import { useDrawer } from '/@/components/Drawer';
  79 + import ContactDrawer from './ConfigurationCenterDrawer.vue';
  80 + import { useResetOrganizationTree, OrganizationIdTree } from '/@/views/common/organizationIdTree';
  81 + import { searchFormSchema, columns, Platform } from './center.data';
  82 + import {
  83 + getPage,
  84 + deleteConfigurationCenter,
  85 + } from '/@/api/configuration/center/configurationCenter';
  86 + import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
  87 + import { isDevMode } from '/@/utils/env';
  88 + import { Authority } from '/@/components/Authority';
  89 + import { Popconfirm } from 'ant-design-vue';
  90 + import { Tag } from 'ant-design-vue';
  91 + import { useGlobSetting } from '/@/hooks/setting';
  92 + export default defineComponent({
  93 + components: {
  94 + PageWrapper,
  95 + OrganizationIdTree,
  96 + BasicTable,
  97 + TableAction,
  98 + ContactDrawer,
  99 + Authority,
  100 + Popconfirm,
  101 + Tag,
  102 + },
  103 + setup() {
  104 + const { configurationPrefix } = useGlobSetting();
  105 + const isDev = isDevMode();
  106 + const searchInfo = reactive<Recordable>({});
  107 + const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo);
  108 + // 表格hooks
  109 + const [registerTable, { reload, setProps }] = useTable({
  110 + title: '组态中心列表',
  111 + api: getPage,
  112 + columns,
  113 + clickToRowSelect: false,
  114 + formConfig: {
  115 + labelWidth: 120,
  116 + schemas: searchFormSchema,
  117 + resetFunc: resetFn,
  118 + },
  119 + showIndexColumn: false,
  120 + useSearchForm: true,
  121 + showTableSetting: true,
  122 + bordered: true,
  123 + rowKey: 'id',
  124 + actionColumn: {
  125 + width: 200,
  126 + title: '操作',
  127 + dataIndex: 'action',
  128 + slots: { customRender: 'action' },
  129 + fixed: 'right',
  130 + },
  131 + });
  132 + const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
  133 + deleteConfigurationCenter,
  134 + handleSuccess,
  135 + setProps
  136 + );
  137 + nextTick(() => {
  138 + setProps(selectionOptions);
  139 + });
  140 +
  141 + // 弹框
  142 + const [registerDrawer, { openDrawer }] = useDrawer();
  143 +
  144 + // 刷新
  145 + function handleSuccess() {
  146 + reload();
  147 + }
  148 + // 新增或编辑
  149 + const handleCreateOrEdit = (record: Recordable | null) => {
  150 + if (record) {
  151 + openDrawer(true, {
  152 + isUpdate: true,
  153 + record,
  154 + });
  155 + } else {
  156 + openDrawer(true, {
  157 + isUpdate: false,
  158 + });
  159 + }
  160 + };
  161 + // 树形选择器
  162 + const handleSelect = (organizationId: string) => {
  163 + searchInfo.organizationId = organizationId;
  164 + handleSuccess();
  165 + };
  166 +
  167 + const handlePreview = (record: Recordable | null) => {
  168 + window.open(
  169 + `${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${
  170 + record!.id
  171 + }&lightbox=1`
  172 + );
  173 + };
  174 + const handleDesign = (record: Recordable | null) => {
  175 + window.open(
  176 + `${configurationPrefix}/${isDev ? '?dev=1&' : '?'}configurationId=${record!.id}`
  177 + );
  178 + };
  179 +
  180 + return {
  181 + Platform,
  182 + searchInfo,
  183 + hasBatchDelete,
  184 + handleCreateOrEdit,
  185 + handleDeleteOrBatchDelete,
  186 + handleSelect,
  187 + handleSuccess,
  188 + handlePreview,
  189 + handleDesign,
  190 + registerTable,
  191 + registerDrawer,
  192 + organizationIdTreeRef,
  193 + };
  194 + },
  195 + });
  196 +</script>
... ...
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
  3 +import { createImgPreview } from '/@/components/Preview';
  4 +import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter';
  5 +import { useComponentRegister } from '/@/components/Form';
  6 +import { OrgTreeSelect } from '../../common/OrgTreeSelect';
  7 +import { getDeviceProfile } from '/@/api/alarm/position';
  8 +
  9 +useComponentRegister('OrgTreeSelect', OrgTreeSelect);
  10 +export enum Platform {
  11 + PHONE = 'phone',
  12 + PC = 'pc',
  13 +}
  14 +
  15 +export enum ConfigurationPermission {
  16 + CREATE = 'api:yt:configuration:center:post',
  17 + UPDATE = 'api:yt:configuration:center:update',
  18 + DELETE = 'api:yt:configuration:center:delete',
  19 + DESIGN = 'api:yt:configuration:center:get_configuration_info:design',
  20 + PREVIEW = 'api:yt:configuration:center:get_configuration_info:preview',
  21 + SHARE = 'api:yt:configuration:center:share',
  22 + UN_SHARE = 'api:yt:configuration:center:monopoly',
  23 +}
  24 +
  25 +export const PC_DEFAULT_CONTENT =
  26 + '<mxfile><diagram>dZHBDsIgDIafhvuEzOh5Tr142sEzGXWQsHVhmKFP7xbAidMT5fv/UtoSVrTuZHgvLyhAE5oJR9iBUMrybT4dM3l4stnTzJPGKBHYAir1hACj7a4EDInRImqr+hTW2HVQ24RxY3BMbTfUadWeN7ACVc31ml6VsPK7jVk4g2pkLJ3tgtLy6A5gkFzg+IFYSVhhEK2PWleAnscXB+Pzjn/U988MdPZHwhQsb0+XZEesfAE=</diagram></mxfile>';
  27 +
  28 +export const PHONE_DEFAULT_CONTENT =
  29 + '<mxfile><diagram>dZHBEoIgEEC/hru6lXU2q0snD50Z2YQZdB2k0fr6dMCMsU4sb9+ysDDI6uFseCuvJFCzJBIDgyNLkjjdw7hM5OnIYRc5UBklvLSAQr3Qw1l7KIFdIFoibVUbwpKaBksbMG4M9aF2Jx12bXmFK1CUXK/pTQkrHd3E24VfUFXSd04hdYmaz65/SCe5oP4LQc4gM0TWRfWQoZ5mN4/F1Z3+ZD/3MtjYHwVjsJw9boIPgvwN</diagram></mxfile>';
  30 +// 表格列数据
  31 +export const columns: BasicColumn[] = [
  32 + {
  33 + title: '组态名称',
  34 + dataIndex: 'name',
  35 + width: 120,
  36 + },
  37 + {
  38 + title: '所属组织',
  39 + dataIndex: 'organizationDTO.name',
  40 + width: 160,
  41 + },
  42 + {
  43 + title: '平台',
  44 + dataIndex: 'platform',
  45 + width: 100,
  46 + slots: { customRender: 'platform' },
  47 + },
  48 + {
  49 + title: '备注',
  50 + dataIndex: 'remark',
  51 + width: 200,
  52 + },
  53 + {
  54 + title: '创建时间',
  55 + dataIndex: 'createTime',
  56 + width: 120,
  57 + },
  58 + {
  59 + title: '更新时间',
  60 + dataIndex: 'updateTime',
  61 + width: 120,
  62 + },
  63 + {
  64 + title: '操作',
  65 + dataIndex: 'action',
  66 + flag: 'ACTION',
  67 + width: 260,
  68 + slots: { customRender: 'action' },
  69 + },
  70 +];
  71 +
  72 +// 查询字段
  73 +export const searchFormSchema: FormSchema[] = [
  74 + {
  75 + field: 'name',
  76 + label: '组态名称',
  77 + component: 'Input',
  78 + colProps: { span: 8 },
  79 + componentProps: {
  80 + maxLength: 36,
  81 + placeholder: '请输入组态名称',
  82 + },
  83 + },
  84 +];
  85 +
  86 +export const formSchema: FormSchema[] = [
  87 + {
  88 + field: 'thumbnail',
  89 + label: '缩略图',
  90 + component: 'ApiUpload',
  91 + changeEvent: 'update:fileList',
  92 + valueField: 'fileList',
  93 + componentProps: () => {
  94 + return {
  95 + listType: 'picture-card',
  96 + maxFileLimit: 1,
  97 + accept: '.png,.jpg,.jpeg,.gif',
  98 + api: async (file: File) => {
  99 + try {
  100 + const formData = new FormData();
  101 + formData.set('file', file);
  102 + const { fileStaticUri, fileName } = await uploadThumbnail(formData);
  103 + return {
  104 + uid: fileStaticUri,
  105 + name: fileName,
  106 + url: fileStaticUri,
  107 + } as FileItem;
  108 + } catch (error) {
  109 + return {};
  110 + }
  111 + },
  112 + onPreview: (fileList: FileItem) => {
  113 + createImgPreview({ imageList: [fileList.url!] });
  114 + },
  115 + // showUploadList: {
  116 + // showDownloadIcon: true,
  117 + // showRemoveIcon: true,
  118 + // },
  119 + };
  120 + },
  121 + },
  122 +
  123 + {
  124 + field: 'name',
  125 + label: '模板名称',
  126 + required: true,
  127 + component: 'Input',
  128 + componentProps: {
  129 + placeholder: '请输入模板名称',
  130 + maxLength: 36,
  131 + },
  132 + },
  133 + {
  134 + field: 'organizationId',
  135 + label: '所属组织',
  136 + required: true,
  137 + component: 'OrgTreeSelect',
  138 + },
  139 + {
  140 + field: 'productIds',
  141 + label: '产品',
  142 + component: 'ApiSelect',
  143 + required: true,
  144 + componentProps: {
  145 + api: getDeviceProfile,
  146 + mode: 'multiple',
  147 + labelField: 'name',
  148 + valueField: 'id',
  149 + },
  150 + },
  151 + {
  152 + field: 'isTemplate',
  153 + label: '',
  154 + component: 'Switch',
  155 + required: true,
  156 + defaultValue: 1,
  157 + componentProps: {
  158 + disabled: true,
  159 + checkedValue: 1,
  160 + unCheckedValue: 0,
  161 + },
  162 + show: false,
  163 + },
  164 + {
  165 + field: 'platform',
  166 + label: '平台',
  167 + required: true,
  168 + component: 'RadioGroup',
  169 + defaultValue: Platform.PC,
  170 + componentProps: {
  171 + defaultValue: Platform.PC,
  172 + options: [
  173 + { label: 'PC端', value: Platform.PC },
  174 + { label: '移动端', value: Platform.PHONE },
  175 + ],
  176 + },
  177 + },
  178 + {
  179 + field: 'remark',
  180 + label: '备注',
  181 + component: 'InputTextArea',
  182 + componentProps: {
  183 + placeholder: '请输入备注',
  184 + maxLength: 255,
  185 + },
  186 + },
  187 + {
  188 + field: 'id',
  189 + label: '',
  190 + component: 'Input',
  191 + show: false,
  192 + componentProps: {
  193 + maxLength: 36,
  194 + placeholder: 'id',
  195 + },
  196 + },
  197 +];
... ...
  1 +import { Platform } from './center.data';
  2 +import { ConfigurationCenterItemsModal } from '/@/api/configuration/center/model/configurationCenterModal';
  3 +import { useGlobSetting } from '/@/hooks/setting';
  4 +
  5 +export enum ScadaModeEnum {
  6 + LIGHTBOX = 'lightbox',
  7 + DESIGN = 'design',
  8 + SHARE = 'share',
  9 +}
  10 +
  11 +interface ScadaLinkParamsType {
  12 + configurationId: string;
  13 + organizationId: string;
  14 + mode: ScadaModeEnum;
  15 + platform: Platform;
  16 + publicId?: string;
  17 +}
  18 +
  19 +const getRandomString = () => Number(Math.random().toString().substring(2)).toString(36);
  20 +
  21 +export const encode = (record: Recordable) => {
  22 + let hash = JSON.stringify(record);
  23 + const mixinString = getRandomString()
  24 + .slice(0, 10)
  25 + .padEnd(10, getRandomString())
  26 + .split('')
  27 + .map((item) => (Math.random() > 0.5 ? item.toUpperCase() : item))
  28 + .join('');
  29 + hash = window.btoa(hash);
  30 + hash = hash.substring(0, 6) + mixinString + hash.substring(6);
  31 + hash = window.btoa(hash);
  32 + return hash;
  33 +};
  34 +
  35 +export const createScadaPageLink = (
  36 + record: ConfigurationCenterItemsModal,
  37 + mode: ScadaModeEnum = ScadaModeEnum.DESIGN,
  38 + open = true
  39 +) => {
  40 + const { configurationPrefix } = useGlobSetting();
  41 + const params: ScadaLinkParamsType = {
  42 + configurationId: record.id,
  43 + organizationId: record.organizationId!,
  44 + mode: mode,
  45 + platform: record.platform as Platform,
  46 + };
  47 +
  48 + if (mode === ScadaModeEnum.SHARE) {
  49 + params.publicId = record.publicId;
  50 + }
  51 +
  52 + const href = new URL(location.origin);
  53 + href.pathname = configurationPrefix;
  54 + href.hash = encode(params);
  55 + open && window.open(href.href);
  56 + return href.href;
  57 +};
... ...
  1 +<script setup lang="ts">
  2 + import { List, Card, Button, PaginationProps, Tooltip } from 'ant-design-vue';
  3 + import { ReloadOutlined } from '@ant-design/icons-vue';
  4 + import { computed, onMounted, reactive, ref, unref } from 'vue';
  5 + import { OrganizationIdTree, useResetOrganizationTree } from '../../common/organizationIdTree';
  6 + import {
  7 + deleteConfigurationCenter,
  8 + getPage,
  9 + shareConfiguration,
  10 + } from '/@/api/configuration/center/configurationCenter';
  11 + import { ConfigurationCenterItemsModal } from '/@/api/configuration/center/model/configurationCenterModal';
  12 + import { PageWrapper } from '/@/components/Page';
  13 + import { BasicForm, useForm } from '/@/components/Form';
  14 + import { ConfigurationPermission, Platform, searchFormSchema } from './center.data';
  15 + import { useMessage } from '/@/hooks/web/useMessage';
  16 + import { Authority } from '/@/components/Authority';
  17 + import ConfigurationCenterDrawer from './ConfigurationCenterDrawer.vue';
  18 + import { useDrawer } from '/@/components/Drawer';
  19 + import { getBoundingClientRect } from '/@/utils/domUtils';
  20 + import configurationSrc from '/@/assets/icons/configuration.svg';
  21 + import { cloneDeep } from 'lodash';
  22 + import { usePermission } from '/@/hooks/web/usePermission';
  23 + import { AuthIcon, CardLayoutButton } from '/@/components/Widget';
  24 + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue';
  25 + import { ShareModal } from '/@/views/common/ShareModal';
  26 + import { ViewTypeNameEnum } from '../../common/ShareModal/config';
  27 + import { useModal } from '/@/components/Modal';
  28 + import { ViewType } from '../../visual/board/config/panelDetail';
  29 + import { useRole } from '/@/hooks/business/useRole';
  30 + import { useClipboard } from '@vueuse/core';
  31 + import { Icon } from '/@/components/Icon';
  32 + import { createScadaPageLink, ScadaModeEnum } from './help';
  33 +
  34 + const listColumn = ref(5);
  35 +
  36 + const { createMessage } = useMessage();
  37 +
  38 + const { isCustomerUser } = useRole();
  39 +
  40 + const organizationId = ref<Nullable<number>>(null);
  41 +
  42 + const pagination = reactive<PaginationProps>({
  43 + size: 'small',
  44 + showTotal: (total: number) => `共 ${total} 条数据`,
  45 + current: 1,
  46 + pageSize: unref(listColumn) * 2,
  47 + onChange: (page: number) => {
  48 + pagination.current = page;
  49 + getListData();
  50 + },
  51 + });
  52 +
  53 + const loading = ref(false);
  54 +
  55 + const dataSource = ref<ConfigurationCenterItemsModal[]>([]);
  56 +
  57 + const [registerForm, { getFieldsValue }] = useForm({
  58 + schemas: searchFormSchema,
  59 + showAdvancedButton: true,
  60 + labelWidth: 100,
  61 + compact: true,
  62 + resetFunc: () => {
  63 + resetFn();
  64 + organizationId.value = null;
  65 + return getListData();
  66 + },
  67 + submitFunc: async () => {
  68 + const value = getFieldsValue();
  69 + getListData(value);
  70 + },
  71 + });
  72 +
  73 + async function getListData(value: Recordable = {}) {
  74 + try {
  75 + loading.value = true;
  76 + const pageSize = unref(listColumn) * 2;
  77 + const { items, total } = await getPage({
  78 + organizationId: unref(organizationId),
  79 + ...value,
  80 + page: pagination.current!,
  81 + pageSize,
  82 + isTemplate: 1,
  83 + });
  84 +
  85 + dataSource.value = items;
  86 + Object.assign(pagination, { total, pageSize });
  87 + } catch (error) {
  88 + } finally {
  89 + loading.value = false;
  90 + }
  91 + }
  92 +
  93 + onMounted(() => {
  94 + getListData();
  95 + });
  96 +
  97 + const searchInfo = reactive<Recordable>({});
  98 + const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo);
  99 + const handleSelect = (orgId: number) => {
  100 + organizationId.value = orgId;
  101 + getListData();
  102 + };
  103 +
  104 + const [registerDrawer, { openDrawer }] = useDrawer();
  105 +
  106 + const { hasPermission } = usePermission();
  107 +
  108 + const getPreviewFlag = computed(() => {
  109 + return hasPermission(ConfigurationPermission.PREVIEW);
  110 + });
  111 +
  112 + const getDesignFlag = computed(() => {
  113 + return hasPermission(ConfigurationPermission.DESIGN);
  114 + });
  115 +
  116 + const getShareFlag = computed(() => {
  117 + return hasPermission(ConfigurationPermission.SHARE);
  118 + });
  119 +
  120 + const handleCreateOrUpdate = (record?: ConfigurationCenterItemsModal) => {
  121 + if (record) {
  122 + openDrawer(true, {
  123 + isUpdate: true,
  124 + record: cloneDeep(record),
  125 + });
  126 + } else {
  127 + openDrawer(true, {
  128 + isUpdate: false,
  129 + });
  130 + }
  131 + };
  132 +
  133 + const handlePreview = (record: ConfigurationCenterItemsModal) => {
  134 + if (!unref(getPreviewFlag)) return;
  135 + createScadaPageLink(record, ScadaModeEnum.LIGHTBOX);
  136 + };
  137 +
  138 + const handleDesign = (record: ConfigurationCenterItemsModal) => {
  139 + if (!unref(getDesignFlag)) return;
  140 +
  141 + createScadaPageLink(record, ScadaModeEnum.DESIGN);
  142 + };
  143 +
  144 + const handleDelete = async (record: ConfigurationCenterItemsModal) => {
  145 + try {
  146 + await deleteConfigurationCenter([record.id]);
  147 + createMessage.success('删除成功');
  148 + await getListData();
  149 + } catch (error) {}
  150 + };
  151 +
  152 + const handleCardLayoutChange = () => {
  153 + pagination.current = 1;
  154 + getListData();
  155 + };
  156 +
  157 + const createShareUrl = (record: ConfigurationCenterItemsModal) => {
  158 + return createScadaPageLink(record, ScadaModeEnum.SHARE, false);
  159 + };
  160 +
  161 + const { copied, copy } = useClipboard({ legacy: true });
  162 + const handleCreateShareUrl = async (record: ConfigurationCenterItemsModal) => {
  163 + if (!unref(getShareFlag)) return;
  164 + const url = createShareUrl(record);
  165 + await copy(url);
  166 + if (unref(copied)) {
  167 + createMessage.success('复制成功~');
  168 + }
  169 + };
  170 +
  171 + const [registerShareModal, { openModal }] = useModal();
  172 +
  173 + const handleOpenShareModal = (record: ConfigurationCenterItemsModal) => {
  174 + openModal(true, { record, href: createShareUrl(record) });
  175 + };
  176 +
  177 + const listEl = ref<Nullable<ComponentElRef>>(null);
  178 +
  179 + onMounted(() => {
  180 + const clientHeight = document.documentElement.clientHeight;
  181 + const rect = getBoundingClientRect(unref(listEl)!.$el! as HTMLElement) as DOMRect;
  182 + // margin-top 24 height 24
  183 + const paginationHeight = 24 + 24 + 8;
  184 + // list pading top 8 maring-top 8 extra slot 56
  185 + const listContainerMarginBottom = 8 + 8 + 56;
  186 + const listContainerHeight =
  187 + clientHeight - rect.top - paginationHeight - listContainerMarginBottom;
  188 + const listContainerEl = (unref(listEl)!.$el as HTMLElement).querySelector(
  189 + '.ant-spin-container'
  190 + ) as HTMLElement;
  191 + listContainerEl &&
  192 + (listContainerEl.style.height = listContainerHeight + 'px') &&
  193 + (listContainerEl.style.overflowY = 'auto') &&
  194 + (listContainerEl.style.overflowX = 'hidden');
  195 + });
  196 +</script>
  197 +
  198 +<template>
  199 + <PageWrapper dense contentFullHeight contentClass="flex">
  200 + <OrganizationIdTree @select="handleSelect" ref="organizationIdTreeRef" />
  201 + <section class="flex-auto p-4 w-3/4 xl:w-4/5 w-full configuration-list">
  202 + <div class="flex-auto w-full bg-light-50 dark:bg-dark-900 p-4">
  203 + <BasicForm @register="registerForm" />
  204 + </div>
  205 + <List
  206 + ref="listEl"
  207 + :loading="loading"
  208 + class="flex-auto bg-light-50 dark:bg-dark-900 !p-2 !mt-4"
  209 + position="bottom"
  210 + :pagination="pagination"
  211 + :data-source="dataSource"
  212 + :grid="{ gutter: 4, column: listColumn }"
  213 + >
  214 + <template #header>
  215 + <div class="flex gap-3 justify-end">
  216 + <Authority v-if="!isCustomerUser" :value="ConfigurationPermission.CREATE">
  217 + <Button type="primary" @click="handleCreateOrUpdate()">新增模板</Button>
  218 + </Authority>
  219 + <CardLayoutButton v-model:value="listColumn" @change="handleCardLayoutChange" />
  220 + <Tooltip title="刷新">
  221 + <Button type="primary" @click="getListData">
  222 + <ReloadOutlined />
  223 + </Button>
  224 + </Tooltip>
  225 + </div>
  226 + </template>
  227 + <template #renderItem="{ item }">
  228 + <List.Item>
  229 + <Card
  230 + :style="{
  231 + '--viewType': item.viewType === ViewType.PUBLIC_VIEW ? '#1890ff' : '#faad14',
  232 + }"
  233 + hoverable
  234 + class="card-container"
  235 + >
  236 + <template #cover>
  237 + <div
  238 + class="img-container h-full w-full !flex justify-center items-center text-center p-1 relative"
  239 + >
  240 + <img
  241 + class="w-full h-36"
  242 + alt="example"
  243 + :src="item.thumbnail || configurationSrc"
  244 + @click="handlePreview(item)"
  245 + />
  246 + <span
  247 + class="absolute top-0 left-0 text-light-50 transform -rotate-45 translate-y-1"
  248 + >
  249 + {{ ViewTypeNameEnum[item.viewType] || ViewTypeNameEnum.PRIVATE_VIEW }}
  250 + </span>
  251 + </div>
  252 + </template>
  253 + <template class="ant-card-actions" #actions>
  254 + <Tooltip v-if="!isCustomerUser" title="设计">
  255 + <AuthIcon
  256 + :auth="ConfigurationPermission.DESIGN"
  257 + class="!text-lg"
  258 + icon="ant-design:edit-outlined"
  259 + @click="handleDesign(item)"
  260 + />
  261 + </Tooltip>
  262 + <Tooltip title="点击复制分享链接">
  263 + <AuthIcon
  264 + :auth="ConfigurationPermission.SHARE"
  265 + :disabled="!item.publicId"
  266 + class="!text-lg"
  267 + icon="ant-design:share-alt-outlined"
  268 + @click="handleCreateShareUrl(item)"
  269 + />
  270 + </Tooltip>
  271 + <AuthDropDown
  272 + v-if="!isCustomerUser"
  273 + :dropMenuList="[
  274 + {
  275 + text: '分享',
  276 + auth: ConfigurationPermission.SHARE,
  277 + icon: 'ant-design:share-alt-outlined',
  278 + event: '',
  279 + onClick: handleOpenShareModal.bind(null, item),
  280 + },
  281 + {
  282 + text: '编辑',
  283 + auth: ConfigurationPermission.UPDATE,
  284 + icon: 'clarity:note-edit-line',
  285 + event: '',
  286 + onClick: handleCreateOrUpdate.bind(null, item),
  287 + },
  288 + {
  289 + text: '删除',
  290 + auth: ConfigurationPermission.DELETE,
  291 + icon: 'ant-design:delete-outlined',
  292 + event: '',
  293 + popconfirm: {
  294 + title: '是否确认删除操作?',
  295 + onConfirm: handleDelete.bind(null, item),
  296 + },
  297 + },
  298 + ]"
  299 + :trigger="['hover']"
  300 + />
  301 + </template>
  302 + <Card.Meta>
  303 + <template #title>
  304 + <span class="truncate">{{ item.name }}</span>
  305 + </template>
  306 + <template #description>
  307 + <div class="truncate h-11">
  308 + <div class="truncate flex justify-between items-center">
  309 + <div>{{ item.organizationDTO?.name }}</div>
  310 + <Icon
  311 + :icon="
  312 + item.platform === Platform.PC
  313 + ? 'ri:computer-line'
  314 + : 'clarity:mobile-phone-solid'
  315 + "
  316 + />
  317 + </div>
  318 + <div class="truncate">{{ item.remark || '' }} </div>
  319 + </div>
  320 + </template>
  321 + </Card.Meta>
  322 + </Card>
  323 + </List.Item>
  324 + </template>
  325 + </List>
  326 + </section>
  327 + <ConfigurationCenterDrawer @register="registerDrawer" @success="getListData" />
  328 + <ShareModal
  329 + @register="registerShareModal"
  330 + :shareApi="shareConfiguration"
  331 + @success="getListData"
  332 + />
  333 + </PageWrapper>
  334 +</template>
  335 +
  336 +<style lang="less" scoped>
  337 + .configuration-list:deep(.ant-list-header) {
  338 + border-bottom: none !important;
  339 + }
  340 +
  341 + .configuration-list:deep(.ant-list-pagination) {
  342 + height: 24px;
  343 + }
  344 +
  345 + .configuration-list:deep(.ant-card-body) {
  346 + padding: 16px !important;
  347 + }
  348 +
  349 + .configuration-list:deep(.ant-list-empty-text) {
  350 + @apply w-full h-full flex justify-center items-center;
  351 + }
  352 +
  353 + .card-container {
  354 + // background-color: red;
  355 + .img-container {
  356 + border-top-left-radius: 80px;
  357 + background-color: #fff;
  358 +
  359 + img {
  360 + border-top-left-radius: 80px;
  361 + }
  362 + }
  363 + }
  364 +
  365 + .card-container:deep(.ant-card-cover) {
  366 + background-color: var(--viewType);
  367 + }
  368 +</style>
... ...