StationConfigCrud.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <template>
  2. <div class="station-config-page">
  3. <a-card :bordered="false">
  4. <a-form :model="queryParams" layout="inline">
  5. <a-form-item v-if="hasStation" label="场站">
  6. <a-select v-model:value="queryParams[stationField]" allow-clear show-search class="query-select" :options="stationOptions" :filter-option="filterOption" />
  7. </a-form-item>
  8. <a-form-item v-for="item in searchFields" :key="item.name" :label="item.label">
  9. <a-select v-if="item.type === 'select'" v-model:value="queryParams[item.name]" allow-clear class="query-select" :options="item.options" />
  10. <a-input v-else v-model:value="queryParams[item.name]" allow-clear :placeholder="'请输入' + item.label" />
  11. </a-form-item>
  12. <a-form-item>
  13. <a-space>
  14. <a-button type="primary" :loading="loading" @click="handleQuery"><BearJiaIcon icon="SearchOutlined"/>查询</a-button>
  15. <a-button @click="resetQuery">重置</a-button>
  16. </a-space>
  17. </a-form-item>
  18. </a-form>
  19. </a-card>
  20. <a-card ref="tableCardRef" :bordered="false" class="section">
  21. <TableToolbar
  22. :columns="columns"
  23. :selected-columns="selectedColumnKeys"
  24. :loading="loading"
  25. :export-config="exportConfig"
  26. @columnSettingsChange="handleColumnSettingsChange"
  27. @refresh="handleQuery"
  28. @fullscreen="handleFullscreen"
  29. @export="handleExport"
  30. @reset="handleTableReset"
  31. >
  32. <template #left>
  33. <a-button v-hasPermi="[moduleConfig.perm + ':add']" type="primary" @click="openForm()"><BearJiaIcon icon="plus-outlined"/>新增</a-button>
  34. <a-button v-hasPermi="[moduleConfig.perm + ':remove']" danger type="primary" :disabled="selectedRowKeys.length <= 0" @click="handleBatchDelete"><BearJiaIcon icon="delete-outlined"/>删除</a-button>
  35. <a-button v-hasPermi="[moduleConfig.perm + ':export']" @click="handleExport"><BearJiaIcon icon="export-outlined"/>导出</a-button>
  36. </template>
  37. </TableToolbar>
  38. <a-table :columns="displayColumns" :data-source="rows" :loading="loading" :pagination="pagination" row-key="id" :row-selection="rowSelection" :size="tableConfigStore.size" :scroll="tableScroll" @change="handleTableChange">
  39. <template #bodyCell="{ column, record }">
  40. <a-switch v-if="isSwitchColumn(column)" :checked="record[column.dataIndex] === switchCheckedValue(column)" disabled size="small" />
  41. <template v-else-if="column.key === 'stateColumn'">
  42. <a-tag :color="stateColor(record.state)">{{ stateText(record.state) }}</a-tag>
  43. </template>
  44. <template v-else-if="column.key === 'operation'">
  45. <TableActionBar :record="record" :has-view="false" :has-edit="true" :has-delete="true">
  46. <template #edit>
  47. <a v-hasPermi="[moduleConfig.perm + ':edit']" class="action-btn edit-btn" @click="openForm(record)"><BearJiaIcon icon="edit-outlined"/>修改</a>
  48. </template>
  49. <template #delete>
  50. <a-divider type="vertical"/>
  51. <a v-hasPermi="[moduleConfig.perm + ':remove']" class="action-btn delete-btn" @click="handleDeleteRecord(record)"><BearJiaIcon icon="delete-outlined"/>删除</a>
  52. </template>
  53. <template v-if="stateActionOf(record)">
  54. <a-divider type="vertical"/>
  55. <a v-hasPermi="[moduleConfig.perm + ':edit']" class="action-btn" @click="handleRowStateAction(record)"><BearJiaIcon :icon="stateActionOf(record).icon"/>{{ stateActionOf(record).text }}</a>
  56. </template>
  57. </TableActionBar>
  58. </template>
  59. </template>
  60. </a-table>
  61. </a-card>
  62. <a-modal v-model:open="formOpen" :title="form.id ? '编辑' + moduleConfig.title : '新增' + moduleConfig.title" width="860px" @ok="submitForm">
  63. <a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 7 }" :wrapper-col="{ span: 15 }">
  64. <a-row :gutter="16">
  65. <a-col v-if="hasStation" :span="12">
  66. <a-form-item label="所属场站" :name="stationField">
  67. <a-select v-model:value="form[stationField]" show-search :options="stationOptions" :filter-option="filterOption" @change="handleStationChange" />
  68. </a-form-item>
  69. </a-col>
  70. <a-col v-for="item in moduleConfig.fields" :key="item[0]" :span="12">
  71. <a-form-item :label="item[1]" :name="item[0]">
  72. <a-select v-if="item[2] === 'select'" v-model:value="form[item[0]]" allow-clear :options="toOptions(item[4])" />
  73. <a-select v-else-if="item[2] === 'device'" v-model:value="form[deviceFieldName]" allow-clear show-search :options="deviceOptions" :filter-option="filterOption" @change="handleDeviceChange" />
  74. <a-select v-else-if="item[2] === 'channel'" v-model:value="form[channelFieldName]" allow-clear show-search :options="channelOptions" :filter-option="filterOption" />
  75. <a-select v-else-if="item[2] === 'transmit'" v-model:value="form[item[0]]" allow-clear show-search :options="transmitOptions" :filter-option="filterOption" />
  76. <a-select v-else-if="item[2] === 'protocol'" v-model:value="form[item[0]]" allow-clear show-search :options="protocolOptions" :filter-option="filterOption" />
  77. <a-switch v-else-if="item[2] === 'switch'" v-model:checked="form[item[0]]" :checked-value="item[4]?.[0]?.[1] ?? true" :un-checked-value="item[4]?.[1]?.[1] ?? false" />
  78. <a-input-number v-else-if="item[2] === 'number'" v-model:value="form[item[0]]" class="full-input" />
  79. <a-date-picker v-else-if="item[2] === 'date'" v-model:value="form[item[0]]" value-format="YYYY-MM-DD" class="full-input" />
  80. <a-input v-else v-model:value="form[item[0]]" allow-clear />
  81. </a-form-item>
  82. </a-col>
  83. </a-row>
  84. </a-form>
  85. </a-modal>
  86. </div>
  87. </template>
  88. <script setup>
  89. import { computed, reactive, ref } from 'vue';
  90. import { message, Modal } from 'ant-design-vue';
  91. import BearJiaUtil from '@/utils/BearJiaUtil';
  92. import { listStation } from '@/api/config/station';
  93. import { listStationDevice } from '@/api/config/stationDevice';
  94. import { listChannel } from '@/api/config/channel';
  95. import { listProtocol } from '@/api/config/protocol';
  96. import { listTransmit } from '@/api/config/transmit';
  97. import TableActionBar from '@/components/TableActionBar/index.vue';
  98. import TableToolbar from '@/components/BearJiaProTable/TableToolbar.vue';
  99. import { useTableConfigStore } from '@/stores/tableConfig';
  100. import { BearJiaIcon } from '@/utils/BearJiaIcon.js';
  101. const props = defineProps({ moduleConfig: { type: Object, required: true }, api: { type: Object, required: true } });
  102. const loading = ref(false);
  103. const rows = ref([]);
  104. const stationOptions = ref([]);
  105. const allDevices = ref([]);
  106. const allChannels = ref([]);
  107. const allTransmits = ref([]);
  108. const allProtocols = ref([]);
  109. const formOpen = ref(false);
  110. const formRef = ref();
  111. const selectedRowKeys = ref([]);
  112. const tableCardRef = ref();
  113. const selectedColumnKeys = ref([]);
  114. const tableConfigStore = useTableConfigStore();
  115. const stationField = computed(() => props.moduleConfig.stationField || 'stationId');
  116. const queryParams = reactive({ pageNum: 1, pageSize: 10, [stationField.value]: undefined });
  117. const pagination = reactive({ current: 1, pageSize: 10, total: 0, showSizeChanger: true });
  118. const form = reactive({});
  119. const hasStation = computed(() => props.moduleConfig.hasStation !== false);
  120. const deviceFieldName = computed(() => props.moduleConfig.fields.find((item) => item[2] === 'device')?.[0]);
  121. const channelFieldName = computed(() => props.moduleConfig.fields.find((item) => item[2] === 'channel')?.[0]);
  122. const transmitFieldName = computed(() => props.moduleConfig.fields.find((item) => item[2] === 'transmit')?.[0]);
  123. const protocolFieldName = computed(() => props.moduleConfig.fields.find((item) => item[2] === 'protocol')?.[0]);
  124. const fieldMap = computed(() => new Map(props.moduleConfig.fields.map((item) => [item[0], item])));
  125. const searchFields = computed(() => props.moduleConfig.search.filter((name) => name !== stationField.value).map((name) => ({ name, label: fieldMap.value.get(name)?.[1] || name, type: fieldMap.value.get(name)?.[2], options: toOptions(fieldMap.value.get(name)?.[4]) })));
  126. const columnTitle = (name) => {
  127. if (name === 'stationName') return '场站';
  128. if (name === 'channelName') return '通道';
  129. if (name === 'configTransmitName') return '通信模块';
  130. if (name === 'configProtocalName') return '协议模块';
  131. return fieldMap.value.get(name)?.[1] || name;
  132. };
  133. const columns = computed(() => [
  134. { title: 'ID', dataIndex: 'id', width: 80 },
  135. ...props.moduleConfig.columns.map((name) => ({ title: columnTitle(name), dataIndex: name, width: 140, ellipsis: true })),
  136. ...(props.moduleConfig.stateColumn ? [{ title: props.moduleConfig.stateColumn.title || '状态', dataIndex: 'state', key: 'stateColumn', width: 110 }] : []),
  137. { title: '操作', key: 'operation', dataIndex: 'operation', width: props.moduleConfig.stateColumn ? 260 : 200, fixed: 'right' }
  138. ]);
  139. const storageKey = 'station-config-columns-' + props.moduleConfig.api;
  140. const readSavedColumns = () => {
  141. try {
  142. const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
  143. if (Array.isArray(saved) && saved.length > 0) {
  144. const valid = saved.filter((k) => columns.value.some((c) => c.dataIndex === k));
  145. if (props.moduleConfig.stateColumn && !valid.includes('state')) valid.push('state');
  146. if (valid.length > 0) return valid;
  147. }
  148. } catch (e) { /* 忽略存储解析错误 */ }
  149. return columns.value.map((c) => c.dataIndex);
  150. };
  151. selectedColumnKeys.value = readSavedColumns();
  152. const displayColumns = computed(() => columns.value.filter((col) => selectedColumnKeys.value.includes(col.dataIndex)));
  153. const tableScroll = computed(() => ({ x: displayColumns.value.reduce((sum, col) => sum + (col.width || 140), 0) + 40 }));
  154. const exportConfig = { enabled: true };
  155. const rules = computed(() => ({
  156. ...(hasStation.value ? { [stationField.value]: [{ required: true, message: '请选择所属场站', trigger: 'change' }] } : {}),
  157. ...Object.fromEntries(props.moduleConfig.fields.filter((item) => item[3]).map((item) => [item[0], [{ required: true, message: '请输入' + item[1], trigger: item[2] === 'select' || item[2] === 'device' || item[2] === 'switch' ? 'change' : 'blur' }]]))
  158. }));
  159. const deviceOptions = computed(() => allDevices.value
  160. .filter((item) => !form[stationField.value] || String(item.stationId) === String(form[stationField.value]))
  161. .map((item) => ({ label: item.deviceName || item.name || item.id, value: item.id, raw: item })));
  162. const channelOptions = computed(() => allChannels.value.map((item) => ({ label: item.name || item.id, value: item.id })));
  163. const transmitOptions = computed(() => allTransmits.value.map((item) => ({ label: item.name || item.id, value: item.id })));
  164. const protocolOptions = computed(() => allProtocols.value.map((item) => ({ label: item.name || item.id, value: item.id })));
  165. const rowSelection = computed(() => ({
  166. selectedRowKeys: selectedRowKeys.value,
  167. onChange: (keys) => { selectedRowKeys.value = keys; }
  168. }));
  169. const toOptions = (items = []) => items.map((item) => ({ label: item[0], value: item[1] }));
  170. const isSwitchColumn = (column) => fieldMap.value.get(column.dataIndex)?.[2] === 'switch';
  171. const switchCheckedValue = (column) => fieldMap.value.get(column.dataIndex)?.[4]?.[0]?.[1] ?? true;
  172. const filterOption = (input, option) => String(option?.label || '').toLowerCase().includes(input.toLowerCase());
  173. const resetObject = (target, source = {}) => { Object.keys(target).forEach((key) => delete target[key]); Object.assign(target, source); };
  174. const loadStations = async () => {
  175. if (!hasStation.value) return;
  176. const res = await listStation({ pageNum: 1, pageSize: 500 });
  177. const list = res?.rows || res?.data || [];
  178. stationOptions.value = list.map((item) => ({ label: item.stationName || item.name || item.id, value: item.id }));
  179. };
  180. const loadDevices = async () => {
  181. if (!deviceFieldName.value) return;
  182. const res = await listStationDevice({ pageNum: 1, pageSize: 500 });
  183. allDevices.value = res?.rows || res?.data || [];
  184. };
  185. const loadChannels = async () => {
  186. if (!channelFieldName.value) return;
  187. const res = await listChannel({ pageNum: 1, pageSize: 500 });
  188. allChannels.value = res?.rows || res?.data || [];
  189. };
  190. const loadTransmits = async () => {
  191. if (!transmitFieldName.value) return;
  192. const res = await listTransmit({ pageNum: 1, pageSize: 500 });
  193. allTransmits.value = res?.rows || res?.data || [];
  194. };
  195. const loadProtocols = async () => {
  196. if (!protocolFieldName.value) return;
  197. const res = await listProtocol({ pageNum: 1, pageSize: 500 });
  198. allProtocols.value = res?.rows || res?.data || [];
  199. };
  200. const handleQuery = async () => {
  201. loading.value = true;
  202. try {
  203. const res = await props.api.list(queryParams);
  204. rows.value = res?.rows || res?.data || [];
  205. pagination.total = res?.total || rows.value.length;
  206. pagination.current = queryParams.pageNum;
  207. pagination.pageSize = queryParams.pageSize;
  208. if (props.moduleConfig.stateColumn) {
  209. const list = rows.value;
  210. list.forEach((row) => { row.state = -2; });
  211. refreshRowStates(list);
  212. }
  213. } finally {
  214. loading.value = false;
  215. }
  216. };
  217. const stateText = (state) => props.moduleConfig.stateColumn?.textMap?.[String(state)] ?? '';
  218. const stateColor = (state) => props.moduleConfig.stateColumn?.colorMap?.[String(state)] ?? (Number(state) === 1 ? 'success' : 'default');
  219. const stateActionOf = (record) => {
  220. const actions = props.moduleConfig.stateColumn?.actions;
  221. return (actions && actions[String(record.state)]) || null;
  222. };
  223. const refreshRowStates = async (list) => {
  224. if (!list.length || typeof props.api.queryState !== 'function') return;
  225. try {
  226. const res = await props.api.queryState({ ids: list.map((row) => row.id) });
  227. const states = res?.data || [];
  228. const stateMap = new Map(states.map((item) => [String(item.channelId), item.state]));
  229. list.forEach((row) => { row.state = stateMap.has(String(row.id)) ? Number(stateMap.get(String(row.id))) : 1; });
  230. } catch (e) {
  231. list.forEach((row) => { row.state = -1; });
  232. }
  233. };
  234. const handleRowStateAction = async (record) => {
  235. const action = stateActionOf(record);
  236. if (!action || typeof props.api.operate !== 'function') return;
  237. await props.api.operate({ id: record.id, command: action.command });
  238. message.success('操作成功!');
  239. refreshRowStates(rows.value);
  240. };
  241. const resetQuery = () => {
  242. Object.keys(queryParams).forEach((key) => { if (!['pageNum', 'pageSize'].includes(key)) queryParams[key] = undefined; });
  243. queryParams.pageNum = 1;
  244. handleQuery();
  245. };
  246. const handleTableChange = (page) => {
  247. queryParams.pageNum = page.current;
  248. queryParams.pageSize = page.pageSize;
  249. handleQuery();
  250. };
  251. const openForm = async (record) => {
  252. await loadDevices();
  253. await loadChannels();
  254. await loadTransmits();
  255. await loadProtocols();
  256. if (record?.id) {
  257. const res = await props.api.get(record.id);
  258. resetObject(form, { ...res.data });
  259. } else {
  260. resetObject(form, { [stationField.value]: queryParams[stationField.value] });
  261. }
  262. props.moduleConfig.fields.filter((item) => item[2] === 'switch').forEach((item) => { if (form[item[0]] === undefined) form[item[0]] = item[4]?.[1]?.[1] ?? false; });
  263. formOpen.value = true;
  264. };
  265. const handleStationChange = () => {
  266. if (deviceFieldName.value) {
  267. form[deviceFieldName.value] = undefined;
  268. form.deviceName = undefined;
  269. }
  270. };
  271. const handleDeviceChange = (value) => {
  272. const device = allDevices.value.find((item) => String(item.id) === String(value));
  273. form.deviceName = device?.deviceName;
  274. };
  275. const submitForm = async () => {
  276. await formRef.value?.validate();
  277. if (form.id) await props.api.update(form); else await props.api.add(form);
  278. message.success('保存成功');
  279. formOpen.value = false;
  280. handleQuery();
  281. };
  282. const handleDeleteRecord = (record) => {
  283. Modal.confirm({
  284. title: '确认删除',
  285. content: `确认删除该${moduleConfig.title}?`,
  286. onOk: async () => {
  287. await props.api.del(record.id);
  288. message.success('删除成功');
  289. handleQuery();
  290. }
  291. });
  292. };
  293. const handleBatchDelete = () => {
  294. if (selectedRowKeys.value.length <= 0) {
  295. message.warning('请至少选择一条数据');
  296. return;
  297. }
  298. Modal.confirm({
  299. title: '确认删除',
  300. content: `确认删除选中的 ${selectedRowKeys.value.length} 条数据?`,
  301. onOk: async () => {
  302. await props.api.del(selectedRowKeys.value);
  303. message.success('删除成功');
  304. selectedRowKeys.value = [];
  305. handleQuery();
  306. }
  307. });
  308. };
  309. const handleExport = () => BearJiaUtil.download('config/' + props.moduleConfig.api + '/export', queryParams, props.moduleConfig.exportName);
  310. const handleColumnSettingsChange = (keys) => {
  311. selectedColumnKeys.value = keys;
  312. localStorage.setItem(storageKey, JSON.stringify(keys));
  313. };
  314. const handleFullscreen = (isFullscreen) => {
  315. const el = tableCardRef.value?.$el || tableCardRef.value;
  316. if (isFullscreen) {
  317. el?.requestFullscreen?.();
  318. } else {
  319. document.exitFullscreen?.();
  320. }
  321. };
  322. const handleTableReset = () => {
  323. selectedColumnKeys.value = columns.value.map((c) => c.dataIndex);
  324. localStorage.removeItem(storageKey);
  325. handleQuery();
  326. };
  327. loadStations();
  328. loadDevices();
  329. loadChannels();
  330. loadTransmits();
  331. loadProtocols();
  332. handleQuery();
  333. </script>
  334. <style lang="less" scoped>
  335. .station-config-page {
  336. .section { margin-top: 12px; }
  337. .query-select { width: 180px; }
  338. .full-input { width: 100%; }
  339. }
  340. </style>