feat: 添加设备详情页面权限控制和CRON表达式配置功能
- 在设备详情页新增权限访问控制,防止无权限用户访问产品详情 - 引入并集成 shiyzhangcron 组件库实现 CRON 表达式可视化配置 - 新增 CronPickerModal 弹窗组件用于选择和确认 CRON 表达式 - 更新 package.json 添加 shiyzhangcron 依赖 - 调整按钮权限控制逻辑以适配新的权限校验方式 - 优化部分页面样式与交互逻辑
This commit is contained in:
parent
df397ba648
commit
8727d91415
|
@ -53,6 +53,7 @@
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
|
"shiyzhangcron": "^0.1.5",
|
||||||
"tinymce": "^7.3.0",
|
"tinymce": "^7.3.0",
|
||||||
"unplugin-vue-components": "^0.27.3",
|
"unplugin-vue-components": "^0.27.3",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Modal } from 'ant-design-vue';
|
||||||
|
import { EasyCronInner } from 'shiyzhangcron';
|
||||||
|
|
||||||
|
import 'shiyzhangcron/dist/style.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
value: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(['update:visible', 'confirm', 'cancel']);
|
||||||
|
|
||||||
|
const innerValue = ref(props.value);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(v) => {
|
||||||
|
innerValue.value = v;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const isVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (value) => emit('update:visible', value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOk = () => {
|
||||||
|
emit('confirm', innerValue.value);
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCronChange = (v: string) => {
|
||||||
|
innerValue.value = v;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:open="isVisible"
|
||||||
|
:title="title || 'Cron 配置'"
|
||||||
|
width="860px"
|
||||||
|
:mask-closable="false"
|
||||||
|
@ok="onOk"
|
||||||
|
@cancel="onCancel"
|
||||||
|
>
|
||||||
|
<EasyCronInner
|
||||||
|
v-model:value="innerValue"
|
||||||
|
input-area="true"
|
||||||
|
@change="onCronChange"
|
||||||
|
/>
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<div>当前选择: {{ innerValue }}</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
|
@ -1,10 +1,57 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, ref } from 'vue';
|
||||||
|
|
||||||
|
import 'shiyzhangcron/dist/style.css';
|
||||||
|
|
||||||
|
const CronPickerModal = defineAsyncComponent(
|
||||||
|
() => import('../../../components/CronPickerModal/index.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const easyCronInnerValue = ref('* * * * * ? *');
|
||||||
|
|
||||||
|
const cronModalVisible = ref(false);
|
||||||
|
|
||||||
|
const openCronModal = () => {
|
||||||
|
cronModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCronConfirm = (val: string) => {
|
||||||
|
easyCronInnerValue.value = val;
|
||||||
|
cronModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCronCancel = () => {
|
||||||
|
cronModalVisible.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h1 class="text-2xl font-bold mb-4">Test Detail Page</h1>
|
<h1 class="mb-4 text-2xl font-bold">Test Detail Page</h1>
|
||||||
<p>This is a placeholder for the test detail page.</p>
|
|
||||||
|
<div class="flex items-center gap-2" style="max-width: 640px">
|
||||||
|
<input
|
||||||
|
:value="easyCronInnerValue"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入 CRON 表达式"
|
||||||
|
class="flex-1 rounded border border-gray-300 px-3 py-1"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="rounded bg-blue-600 px-3 py-1 text-white"
|
||||||
|
@click="openCronModal"
|
||||||
|
>
|
||||||
|
配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4">当前cron值为: {{ easyCronInnerValue }}</p>
|
||||||
|
|
||||||
|
<CronPickerModal
|
||||||
|
v-if="cronModalVisible"
|
||||||
|
:visible="cronModalVisible"
|
||||||
|
:value="easyCronInnerValue"
|
||||||
|
@confirm="onCronConfirm"
|
||||||
|
@cancel="onCronCancel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAccess } from '@vben/access';
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||||
|
@ -12,6 +13,8 @@ import { deviceStateOptions } from '#/constants/dicts';
|
||||||
import { useDeviceStore } from '#/store/device';
|
import { useDeviceStore } from '#/store/device';
|
||||||
import { getWebSocket } from '#/utils/websocket';
|
import { getWebSocket } from '#/utils/websocket';
|
||||||
|
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const BasicInfo = defineAsyncComponent(
|
const BasicInfo = defineAsyncComponent(
|
||||||
() => import('./components/BasicInfo.vue'),
|
() => import('./components/BasicInfo.vue'),
|
||||||
);
|
);
|
||||||
|
@ -106,6 +109,10 @@ const handleTabChange = (key: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProductClick = () => {
|
const handleProductClick = () => {
|
||||||
|
if (!hasAccessByCodes(['device:product:query'])) {
|
||||||
|
message.warning('暂无权限');
|
||||||
|
return;
|
||||||
|
}
|
||||||
router.push(`/device/product/detail/${currentDevice.value.productId}`);
|
router.push(`/device/product/detail/${currentDevice.value.productId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -349,10 +349,7 @@ const [DeviceDrawer, drawerApi] = useVbenDrawer({
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'action'">
|
<template v-else-if="column.key === 'action'">
|
||||||
<Space>
|
<Space>
|
||||||
<ghost-button
|
<ghost-button @click.stop="handleView(record)">
|
||||||
v-access:code="['device:device:view']"
|
|
||||||
@click.stop="handleView(record)"
|
|
||||||
>
|
|
||||||
详情
|
详情
|
||||||
</ghost-button>
|
</ghost-button>
|
||||||
<ghost-button
|
<ghost-button
|
||||||
|
|
|
@ -211,7 +211,7 @@ onMounted(() => {
|
||||||
<div v-if="!accessInfo.id" class="empty-state">
|
<div v-if="!accessInfo.id" class="empty-state">
|
||||||
<Empty>
|
<Empty>
|
||||||
<template #description>
|
<template #description>
|
||||||
<span v-if="hasAccessByCodes('device:product:edit')">
|
<span v-if="hasAccessByCodes(['device:product:edit'])">
|
||||||
请先
|
请先
|
||||||
<a-button type="link" @click="handleSelectAccess">选择</a-button>
|
<a-button type="link" @click="handleSelectAccess">选择</a-button>
|
||||||
设备接入网关,用以提供设备接入能力
|
设备接入网关,用以提供设备接入能力
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
// types/shiyzhangcron.d.ts
|
||||||
|
declare module 'shiyzhangcron';
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect width="20" height="20" fill="none"/><g fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"><path d="M18 6.923H2v11h16zm-16-1a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-11a1 1 0 0 0-1-1z"/><path d="M6 9.423a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-1 3a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m1 3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m5.768-13.025a2.5 2.5 0 0 0-3.536 0L4.354 6.277a.5.5 0 1 1-.708-.708l3.88-3.878a3.5 3.5 0 0 1 4.949 0l3.879 3.879a.5.5 0 1 1-.708.707z"/></g></svg>
|
After Width: | Height: | Size: 626 B |
|
@ -63,6 +63,9 @@ const SvgMaterialSymbolsDashboardOutlineIcon = createIconifyIcon(
|
||||||
const SvgAntDesignDashboardOutlinedIcon = createIconifyIcon(
|
const SvgAntDesignDashboardOutlinedIcon = createIconifyIcon(
|
||||||
'svg:ant-design--dashboard-outlined',
|
'svg:ant-design--dashboard-outlined',
|
||||||
);
|
);
|
||||||
|
const SvgPepiconsPencilBulletinNoticeIcon = createIconifyIcon(
|
||||||
|
'svg:pepicons-pencil--bulletin-notice',
|
||||||
|
);
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SvgAntDesignDashboardOutlinedIcon,
|
SvgAntDesignDashboardOutlinedIcon,
|
||||||
|
@ -87,6 +90,7 @@ export {
|
||||||
SvgMaterialSymbolsDashboardOutlineIcon,
|
SvgMaterialSymbolsDashboardOutlineIcon,
|
||||||
SvgMaxKeyIcon,
|
SvgMaxKeyIcon,
|
||||||
SvgMdiAccountOnlineOutlineIcon,
|
SvgMdiAccountOnlineOutlineIcon,
|
||||||
|
SvgPepiconsPencilBulletinNoticeIcon,
|
||||||
SvgProiconsDocumentIcon,
|
SvgProiconsDocumentIcon,
|
||||||
SvgQQIcon,
|
SvgQQIcon,
|
||||||
SvgRiAlarmWarningLineIcon,
|
SvgRiAlarmWarningLineIcon,
|
||||||
|
|
Loading…
Reference in New Issue