feat: 添加设备详情页面权限控制和CRON表达式配置功能

- 在设备详情页新增权限访问控制,防止无权限用户访问产品详情
- 引入并集成 shiyzhangcron 组件库实现 CRON 表达式可视化配置
- 新增 CronPickerModal 弹窗组件用于选择和确认 CRON 表达式
- 更新 package.json 添加 shiyzhangcron 依赖
- 调整按钮权限控制逻辑以适配新的权限校验方式
- 优化部分页面样式与交互逻辑
This commit is contained in:
fhysy 2025-09-29 09:12:40 +08:00
parent df397ba648
commit 8727d91415
9 changed files with 131 additions and 7 deletions

View File

@ -53,6 +53,7 @@
"monaco-editor": "^0.52.2",
"pinia": "catalog:",
"rxjs": "^7.8.2",
"shiyzhangcron": "^0.1.5",
"tinymce": "^7.3.0",
"unplugin-vue-components": "^0.27.3",
"vite-plugin-monaco-editor": "^1.1.0",

View File

@ -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>

View File

@ -1,10 +1,57 @@
<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>
<template>
<div class="p-4">
<h1 class="text-2xl font-bold mb-4">Test Detail Page</h1>
<p>This is a placeholder for the test detail page.</p>
<h1 class="mb-4 text-2xl font-bold">Test Detail Page</h1>
<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>
</template>

View File

@ -2,6 +2,7 @@
import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAccess } from '@vben/access';
import { Page } from '@vben/common-ui';
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
@ -12,6 +13,8 @@ import { deviceStateOptions } from '#/constants/dicts';
import { useDeviceStore } from '#/store/device';
import { getWebSocket } from '#/utils/websocket';
const { hasAccessByCodes } = useAccess();
const BasicInfo = defineAsyncComponent(
() => import('./components/BasicInfo.vue'),
);
@ -106,6 +109,10 @@ const handleTabChange = (key: string) => {
};
const handleProductClick = () => {
if (!hasAccessByCodes(['device:product:query'])) {
message.warning('暂无权限');
return;
}
router.push(`/device/product/detail/${currentDevice.value.productId}`);
};

View File

@ -349,10 +349,7 @@ const [DeviceDrawer, drawerApi] = useVbenDrawer({
</template>
<template v-else-if="column.key === 'action'">
<Space>
<ghost-button
v-access:code="['device:device:view']"
@click.stop="handleView(record)"
>
<ghost-button @click.stop="handleView(record)">
详情
</ghost-button>
<ghost-button

View File

@ -211,7 +211,7 @@ onMounted(() => {
<div v-if="!accessInfo.id" class="empty-state">
<Empty>
<template #description>
<span v-if="hasAccessByCodes('device:product:edit')">
<span v-if="hasAccessByCodes(['device:product:edit'])">
请先
<a-button type="link" @click="handleSelectAccess">选择</a-button>
设备接入网关用以提供设备接入能力

View File

@ -0,0 +1,2 @@
// types/shiyzhangcron.d.ts
declare module 'shiyzhangcron';

View File

@ -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

View File

@ -63,6 +63,9 @@ const SvgMaterialSymbolsDashboardOutlineIcon = createIconifyIcon(
const SvgAntDesignDashboardOutlinedIcon = createIconifyIcon(
'svg:ant-design--dashboard-outlined',
);
const SvgPepiconsPencilBulletinNoticeIcon = createIconifyIcon(
'svg:pepicons-pencil--bulletin-notice',
);
export {
SvgAntDesignDashboardOutlinedIcon,
@ -87,6 +90,7 @@ export {
SvgMaterialSymbolsDashboardOutlineIcon,
SvgMaxKeyIcon,
SvgMdiAccountOnlineOutlineIcon,
SvgPepiconsPencilBulletinNoticeIcon,
SvgProiconsDocumentIcon,
SvgQQIcon,
SvgRiAlarmWarningLineIcon,