Commit 81906bdb60ce220da9894a7faea8b550bd5e12ac
Merge branch 'f-dev' into 'main'
feat:新增意见反馈功能 See merge request huang/yun-teng-iot-front!203
Showing
5 changed files
with
446 additions
and
0 deletions
src/api/feedback/feedbackManager.ts
0 → 100644
1 | +import { defHttp } from '/@/utils/http/axios'; | |
2 | +import { CameraModel, CameraQueryParam } from './model/feedbackModel'; | |
3 | + | |
4 | +enum CameraManagerApi { | |
5 | + CAMERA_POST_URL = '/opinion', | |
6 | + CAMERA_GET_URL = '/opinion', | |
7 | + CAMERA_DELETE_URL = '/opinion', | |
8 | + CAMERA_GET_DETAIL_URL = '/opinion', | |
9 | +} | |
10 | + | |
11 | +export const feedbackPage = (params: CameraQueryParam) => { | |
12 | + return defHttp.get<CameraQueryParam>({ | |
13 | + url: CameraManagerApi.CAMERA_GET_URL, | |
14 | + params, | |
15 | + }); | |
16 | +}; | |
17 | + | |
18 | +/** | |
19 | + * 删除视频 | |
20 | + * @param ids 删除的ids | |
21 | + */ | |
22 | +export const deleteFeedbackManage = (ids: string[]) => { | |
23 | + return defHttp.delete({ | |
24 | + url: CameraManagerApi.CAMERA_DELETE_URL, | |
25 | + data: { | |
26 | + ids: ids, | |
27 | + }, | |
28 | + }); | |
29 | +}; | |
30 | + | |
31 | +// 创建或编辑视频 | |
32 | +export const createOrEditFeedBackManage = (data) => { | |
33 | + return defHttp.post<CameraModel>({ | |
34 | + url: CameraManagerApi.CAMERA_POST_URL, | |
35 | + data, | |
36 | + }); | |
37 | +}; | |
38 | + | |
39 | +// 查询视频详情 | |
40 | +export const getCameraManageDetail = (id: string) => { | |
41 | + return defHttp.get({ | |
42 | + url: CameraManagerApi.CAMERA_GET_DETAIL_URL + `/${id}`, | |
43 | + }); | |
44 | +}; | ... | ... |
src/api/feedback/model/feedbackModel.ts
0 → 100644
1 | +import { BasicPageParams } from '/@/api/model/baseModel'; | |
2 | +export type CameraQueryParam = BasicPageParams & CameraParam; | |
3 | + | |
4 | +export type CameraParam = { | |
5 | + status?: true; | |
6 | + name?: string; | |
7 | + organizationId?: string; | |
8 | + orderFiled?: string; | |
9 | + orderType?: string; | |
10 | +}; | |
11 | + | |
12 | +export interface CameraModel { | |
13 | + category?: string; | |
14 | + contact?: string; | |
15 | + createTime?: string; | |
16 | + creator?: string; | |
17 | + defaultConfig?: string; | |
18 | + description?: string; | |
19 | + enabled?: true; | |
20 | + icon?: string; | |
21 | + id?: string; | |
22 | + images?: string; | |
23 | + message?: string; | |
24 | + name?: string; | |
25 | + remark?: string; | |
26 | + roleIds?: [string]; | |
27 | + status?: string; | |
28 | + tenantExpireTime?: string; | |
29 | + tenantId?: string; | |
30 | + tenantProfileId?: string; | |
31 | + tenantStatus?: string; | |
32 | + title?: string; | |
33 | + updateTime?: string; | |
34 | + updater?: string; | |
35 | +} | ... | ... |
src/views/system/feedback/FeedbackDrawer.vue
0 → 100644
1 | +<template> | |
2 | + <BasicDrawer v-bind="$attrs" @register="registerDrawer" title="意见反馈预览" width="30%"> | |
3 | + <BasicForm @register="registerForm"> | |
4 | + <template #iconSelect> | |
5 | + <div style="width: 22vw; display: flex; flex-wrap: wrap"> | |
6 | + <template v-for="(item, index) in feedBackImgUrl" :key="index"> | |
7 | + <span style="display: none">{{ index }}</span> | |
8 | + <div> | |
9 | + <Upload list-type="picture-card" :openFileDialogOnClick="false"> | |
10 | + <img @click="handlePreview(item)" :src="item" alt="avatar" /> | |
11 | + </Upload> | |
12 | + </div> | |
13 | + </template> | |
14 | + <Modal | |
15 | + :visible="previewVisible" | |
16 | + :title="previewTitle" | |
17 | + :footer="null" | |
18 | + @cancel="handleCancel" | |
19 | + > | |
20 | + <img alt="example" style="width: 100%" :src="previewImage" /> | |
21 | + </Modal> | |
22 | + </div> | |
23 | + </template> | |
24 | + </BasicForm> | |
25 | + </BasicDrawer> | |
26 | +</template> | |
27 | +<script lang="ts"> | |
28 | + import { defineComponent, ref } from 'vue'; | |
29 | + import { BasicForm, useForm } from '/@/components/Form'; | |
30 | + import { formSchema } from './config.data'; | |
31 | + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; | |
32 | + import { Upload } from 'ant-design-vue'; | |
33 | + import { Modal } from 'ant-design-vue'; | |
34 | + | |
35 | + export default defineComponent({ | |
36 | + name: 'ContactDrawer', | |
37 | + components: { BasicDrawer, BasicForm, Upload, Modal }, | |
38 | + emits: ['success', 'register'], | |
39 | + setup(_) { | |
40 | + const feedBackImgUrl = ref([]); | |
41 | + const previewVisible = ref(false); | |
42 | + const previewImage = ref(''); | |
43 | + const previewTitle = ref(''); | |
44 | + const [registerForm, { setFieldsValue, resetFields }] = useForm({ | |
45 | + labelWidth: 120, | |
46 | + schemas: formSchema, | |
47 | + showActionButtonGroup: false, | |
48 | + }); | |
49 | + | |
50 | + const [registerDrawer, { setDrawerProps }] = useDrawerInner(async (data) => { | |
51 | + await resetFields(); | |
52 | + feedBackImgUrl.value = []; | |
53 | + setDrawerProps({ confirmLoading: false }); | |
54 | + await setFieldsValue(data.record); | |
55 | + try { | |
56 | + const contactFormJson = JSON.parse(data.record.contact); | |
57 | + const imageFormJson = JSON.parse(data.record.images); | |
58 | + feedBackImgUrl.value = imageFormJson; | |
59 | + await setFieldsValue({ | |
60 | + qq: contactFormJson.qq, | |
61 | + email: contactFormJson.email, | |
62 | + phone: contactFormJson.phone, | |
63 | + }); | |
64 | + } catch (e) { | |
65 | + console.log('意见反馈', e); | |
66 | + } | |
67 | + }); | |
68 | + const handlePreview = async (item) => { | |
69 | + previewVisible.value = true; | |
70 | + previewImage.value = item; | |
71 | + previewTitle.value = '预览图片'; | |
72 | + }; | |
73 | + const handleCancel = () => { | |
74 | + previewVisible.value = false; | |
75 | + }; | |
76 | + return { | |
77 | + registerDrawer, | |
78 | + registerForm, | |
79 | + feedBackImgUrl, | |
80 | + handlePreview, | |
81 | + previewVisible, | |
82 | + handleCancel, | |
83 | + previewImage, | |
84 | + previewTitle, | |
85 | + }; | |
86 | + }, | |
87 | + }); | |
88 | +</script> | ... | ... |
src/views/system/feedback/config.data.ts
0 → 100644
1 | +import { BasicColumn, FormSchema } from '/@/components/Table'; | |
2 | + | |
3 | +// 表格列数据 | |
4 | +export const columns: BasicColumn[] = [ | |
5 | + { | |
6 | + title: '主题', | |
7 | + dataIndex: 'title', | |
8 | + width: 80, | |
9 | + }, | |
10 | + { | |
11 | + title: '姓名', | |
12 | + dataIndex: 'name', | |
13 | + width: 120, | |
14 | + }, | |
15 | + { | |
16 | + title: '反馈信息', | |
17 | + dataIndex: 'message', | |
18 | + width: 120, | |
19 | + }, | |
20 | + { | |
21 | + title: '反馈方式', | |
22 | + dataIndex: 'config', | |
23 | + width: 180, | |
24 | + slots: { customRender: 'config' }, | |
25 | + }, | |
26 | + { | |
27 | + title: '添加时间', | |
28 | + dataIndex: 'createTime', | |
29 | + width: 180, | |
30 | + }, | |
31 | +]; | |
32 | + | |
33 | +// 查询字段 | |
34 | +export const searchFormSchema: FormSchema[] = [ | |
35 | + { | |
36 | + field: 'name', | |
37 | + label: '姓名', | |
38 | + component: 'Input', | |
39 | + colProps: { span: 8 }, | |
40 | + componentProps: { | |
41 | + maxLength: 36, | |
42 | + placeholder: '请输入姓名', | |
43 | + }, | |
44 | + }, | |
45 | +]; | |
46 | + | |
47 | +// 弹框配置项 | |
48 | +export const formSchema: FormSchema[] = [ | |
49 | + { | |
50 | + field: 'title', | |
51 | + label: '主题', | |
52 | + component: 'Input', | |
53 | + componentProps: { | |
54 | + disabled: true, | |
55 | + }, | |
56 | + }, | |
57 | + { | |
58 | + field: 'name', | |
59 | + label: '姓名', | |
60 | + component: 'Input', | |
61 | + componentProps: { | |
62 | + disabled: true, | |
63 | + }, | |
64 | + }, | |
65 | + { | |
66 | + field: 'phone', | |
67 | + label: '手机', | |
68 | + component: 'Input', | |
69 | + componentProps: { | |
70 | + disabled: true, | |
71 | + }, | |
72 | + }, | |
73 | + { | |
74 | + field: 'qq', | |
75 | + label: 'QQ', | |
76 | + component: 'Input', | |
77 | + componentProps: { | |
78 | + disabled: true, | |
79 | + }, | |
80 | + }, | |
81 | + { | |
82 | + field: 'email', | |
83 | + label: '邮箱', | |
84 | + component: 'Input', | |
85 | + componentProps: { | |
86 | + disabled: true, | |
87 | + }, | |
88 | + }, | |
89 | + { | |
90 | + field: 'message', | |
91 | + label: '反馈信息', | |
92 | + colProps: { span: 24 }, | |
93 | + component: 'InputTextArea', | |
94 | + componentProps: { | |
95 | + disabled: true, | |
96 | + }, | |
97 | + }, | |
98 | + { | |
99 | + field: 'images', | |
100 | + label: '预览图片', | |
101 | + slot: 'iconSelect', | |
102 | + component: 'Input', | |
103 | + }, | |
104 | +]; | ... | ... |
src/views/system/feedback/index.vue
0 → 100644
1 | +<template> | |
2 | + <div> | |
3 | + <BasicTable :clickToRowSelect="false" @register="registerTable" :searchInfo="searchInfo"> | |
4 | + <template #toolbar> | |
5 | + <a-button | |
6 | + type="primary" | |
7 | + color="error" | |
8 | + @click="handleDeleteOrBatchDelete(null)" | |
9 | + :disabled="hasBatchDelete" | |
10 | + > | |
11 | + 批量删除 | |
12 | + </a-button> | |
13 | + </template> | |
14 | + <template #config="{ record }"> | |
15 | + <a-button type="link" class="ml-2" @click="showData(record)"> 查看反馈方式 </a-button> | |
16 | + </template> | |
17 | + <template #action="{ record }"> | |
18 | + <TableAction | |
19 | + :actions="[ | |
20 | + { | |
21 | + label: '预览', | |
22 | + icon: 'clarity:note-edit-line', | |
23 | + onClick: handleViewVideo.bind(null, record), | |
24 | + }, | |
25 | + { | |
26 | + label: '删除', | |
27 | + icon: 'ant-design:delete-outlined', | |
28 | + color: 'error', | |
29 | + popConfirm: { | |
30 | + title: '是否确认删除', | |
31 | + confirm: handleDeleteOrBatchDelete.bind(null, record), | |
32 | + }, | |
33 | + }, | |
34 | + ]" | |
35 | + /> | |
36 | + </template> | |
37 | + </BasicTable> | |
38 | + <FeedbackDrawer @register="registerDrawer" @success="handleSuccess" /> | |
39 | + </div> | |
40 | +</template> | |
41 | + | |
42 | +<script lang="ts"> | |
43 | + import { defineComponent, reactive, ref, computed, h } from 'vue'; | |
44 | + import { BasicTable, useTable, TableAction } from '/@/components/Table'; | |
45 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
46 | + import { useDrawer } from '/@/components/Drawer'; | |
47 | + import FeedbackDrawer from './FeedbackDrawer.vue'; | |
48 | + import { feedbackPage, deleteFeedbackManage } from '/@/api/feedback/feedbackManager'; | |
49 | + import { searchFormSchema, columns } from './config.data'; | |
50 | + import { useModal } from '/@/components/Modal'; | |
51 | + import { Modal } from 'ant-design-vue'; | |
52 | + import { JsonPreview } from '/@/components/CodeEditor'; | |
53 | + | |
54 | + export default defineComponent({ | |
55 | + components: { | |
56 | + BasicTable, | |
57 | + TableAction, | |
58 | + FeedbackDrawer, | |
59 | + }, | |
60 | + setup() { | |
61 | + let selectedRowIds = ref<string[]>([]); | |
62 | + const hasBatchDelete = computed(() => selectedRowIds.value.length <= 0); | |
63 | + // 复选框事件 | |
64 | + const onSelectRowChange = (selectedRowKeys: string[]) => { | |
65 | + selectedRowIds.value = selectedRowKeys; | |
66 | + }; | |
67 | + const searchInfo = reactive<Recordable>({}); | |
68 | + const [registerModal] = useModal(); | |
69 | + // 表格hooks | |
70 | + const [registerTable, { reload }] = useTable({ | |
71 | + title: '意见反馈列表', | |
72 | + api: feedbackPage, | |
73 | + columns, | |
74 | + showIndexColumn: false, | |
75 | + clickToRowSelect: false, | |
76 | + formConfig: { | |
77 | + labelWidth: 120, | |
78 | + schemas: searchFormSchema, | |
79 | + }, | |
80 | + useSearchForm: true, | |
81 | + showTableSetting: true, | |
82 | + bordered: true, | |
83 | + rowSelection: { | |
84 | + onChange: onSelectRowChange, | |
85 | + type: 'checkbox', | |
86 | + }, | |
87 | + rowKey: 'id', | |
88 | + actionColumn: { | |
89 | + width: 200, | |
90 | + title: '操作', | |
91 | + dataIndex: 'action', | |
92 | + slots: { customRender: 'action' }, | |
93 | + fixed: 'right', | |
94 | + }, | |
95 | + }); | |
96 | + // 弹框 | |
97 | + const [registerDrawer, { openDrawer }] = useDrawer(); | |
98 | + const { createMessage } = useMessage(); | |
99 | + | |
100 | + // 刷新 | |
101 | + const handleSuccess = () => { | |
102 | + reload(); | |
103 | + }; | |
104 | + // 新增或编辑 | |
105 | + const handleCreateOrEdit = (record: Recordable | null) => { | |
106 | + if (record) { | |
107 | + openDrawer(true, { | |
108 | + isUpdate: true, | |
109 | + record, | |
110 | + }); | |
111 | + } else { | |
112 | + openDrawer(true, { | |
113 | + isUpdate: false, | |
114 | + }); | |
115 | + } | |
116 | + }; | |
117 | + // 删除或批量删除 | |
118 | + const handleDeleteOrBatchDelete = async (record: Recordable | null) => { | |
119 | + if (record) { | |
120 | + try { | |
121 | + await deleteFeedbackManage([record.id]); | |
122 | + createMessage.success('删除成功'); | |
123 | + handleSuccess(); | |
124 | + } catch (e) {} | |
125 | + } else { | |
126 | + try { | |
127 | + await deleteFeedbackManage(selectedRowIds.value); | |
128 | + createMessage.success('批量删除成功'); | |
129 | + selectedRowIds.value = []; | |
130 | + handleSuccess(); | |
131 | + } catch (e) {} | |
132 | + } | |
133 | + }; | |
134 | + | |
135 | + // 树形选择器 | |
136 | + const handleSelect = (organizationId: string) => { | |
137 | + searchInfo.organizationId = organizationId; | |
138 | + handleSuccess(); | |
139 | + }; | |
140 | + const handleViewVideo = (record) => { | |
141 | + openDrawer(true, { | |
142 | + isUpdate: true, | |
143 | + record, | |
144 | + }); | |
145 | + }; | |
146 | + function showData(record: Recordable) { | |
147 | + try { | |
148 | + const jsonContact = JSON.parse(record.contact); | |
149 | + Modal.info({ | |
150 | + title: '当前反馈方式', | |
151 | + width: 600, | |
152 | + centered: true, | |
153 | + maskClosable: true, | |
154 | + content: h(JsonPreview, { data: jsonContact }), | |
155 | + }); | |
156 | + } catch (e) { | |
157 | + console.log('意见反馈', e); | |
158 | + } | |
159 | + } | |
160 | + return { | |
161 | + searchInfo, | |
162 | + hasBatchDelete, | |
163 | + handleCreateOrEdit, | |
164 | + handleDeleteOrBatchDelete, | |
165 | + handleSelect, | |
166 | + handleSuccess, | |
167 | + registerTable, | |
168 | + registerDrawer, | |
169 | + handleViewVideo, | |
170 | + registerModal, | |
171 | + showData, | |
172 | + }; | |
173 | + }, | |
174 | + }); | |
175 | +</script> | ... | ... |