feat: 新增产品详情页面,完成设备配置信息页和参数配置功能
- 添加产品详情页面路由和访问权限 - 实现产品基本信息展示和编辑功能 - 增加产品参数编辑功能 - 实现产品状态变更和配置应用 - 添加设备数量统计和跳转功能
This commit is contained in:
parent
aef11b310f
commit
1e5302ee64
|
@ -52,6 +52,24 @@ export function productUpdate(data: ProductForm) {
|
|||
return requestClient.putWithMsg<void>('/device/product', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备产品(部分)
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function productUpdateById(id: ID, data: ProductForm) {
|
||||
return requestClient.putWithMsg<void>(`/device/product/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备产品状态
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function productUpdateStatus(data: ProductForm) {
|
||||
return requestClient.putWithMsg<void>('/device/product/enable', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备产品
|
||||
* @param id id
|
||||
|
@ -60,3 +78,15 @@ export function productUpdate(data: ProductForm) {
|
|||
export function productRemove(id: ID | IDS) {
|
||||
return requestClient.deleteWithMsg<void>(`/device/product/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询产品存储策略列表
|
||||
* @param params
|
||||
* @returns 产品存储策略列表
|
||||
*/
|
||||
export function getPoliciesList(params?: any) {
|
||||
return requestClient.get<PageResult<GatewayVO>>(
|
||||
'/device/product/storage/policies',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
|
|
@ -65,6 +65,11 @@ export interface ProductVO {
|
|||
* 存储策略配置
|
||||
*/
|
||||
storePolicyConf: string;
|
||||
|
||||
/**
|
||||
* 产品参数
|
||||
*/
|
||||
productParam?: string;
|
||||
}
|
||||
|
||||
export interface ProductForm extends BaseEntity {
|
||||
|
@ -132,6 +137,11 @@ export interface ProductForm extends BaseEntity {
|
|||
* 存储策略配置
|
||||
*/
|
||||
storePolicyConf?: string;
|
||||
|
||||
/**
|
||||
* 产品参数
|
||||
*/
|
||||
productParam?: string;
|
||||
}
|
||||
|
||||
export interface ProductQuery extends PageQuery {
|
||||
|
|
|
@ -31,6 +31,12 @@ const routeMetaMapping: Record<string, Omit<RouteMeta, 'title'>> = {
|
|||
activePath: '/system/role',
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
|
||||
'/device/product/detail/:id': {
|
||||
activePath: '/device/product',
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
|
||||
'/system/oss-config/index': {
|
||||
activePath: '/system/oss',
|
||||
requireHomeRedirect: true,
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './auth';
|
||||
export * from './notify';
|
||||
export * from './product';
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import type { ProductVO } from '#/api/device/product/model';
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { productInfo } from '#/api/device/product';
|
||||
|
||||
export interface ProductDetailState {
|
||||
current: Partial<ProductVO>;
|
||||
detail: Partial<ProductVO>;
|
||||
tabActiveKey: string;
|
||||
deviceCount: number;
|
||||
}
|
||||
|
||||
export const useProductStore = defineStore('product', {
|
||||
state: (): ProductDetailState => ({
|
||||
current: {},
|
||||
detail: {},
|
||||
tabActiveKey: 'BasicInfo',
|
||||
deviceCount: 0,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getCurrentProduct: (state) => state.current,
|
||||
getDetailProduct: (state) => state.detail,
|
||||
getTabActiveKey: (state) => state.tabActiveKey,
|
||||
getDeviceCount: (state) => state.deviceCount,
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 设置当前产品信息
|
||||
*/
|
||||
setCurrent(current: Partial<ProductVO>) {
|
||||
this.current = current;
|
||||
this.detail = current;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取产品详情
|
||||
*/
|
||||
async getDetail(id: number | string) {
|
||||
try {
|
||||
const resp = await productInfo(id);
|
||||
if (resp) {
|
||||
this.current = {
|
||||
...this.current,
|
||||
...resp,
|
||||
};
|
||||
this.detail = resp;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取产品详情失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新产品信息和设备数量
|
||||
*/
|
||||
async refresh(id: number | string) {
|
||||
await this.getDetail(id);
|
||||
// TODO: 这里需要添加获取设备数量的 API 调用
|
||||
// const res = await getDeviceNumber({ productId: id });
|
||||
// if (res) {
|
||||
// this.deviceCount = res;
|
||||
// }
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置选中的 tab
|
||||
*/
|
||||
setTabActiveKey(key: string) {
|
||||
this.tabActiveKey = key;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置设备数量
|
||||
*/
|
||||
setDeviceCount(count: number) {
|
||||
this.deviceCount = count;
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
reset() {
|
||||
this.current = {};
|
||||
this.detail = {};
|
||||
this.tabActiveKey = 'BasicInfo';
|
||||
this.deviceCount = 0;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -6,6 +6,11 @@ import { h } from 'vue';
|
|||
import { deviceTypeOptions, enabledOptions } from '#/constants/dicts';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'productKey',
|
||||
label: '产品编码',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'productName',
|
||||
|
@ -53,9 +58,14 @@ export const columns: VxeGridProps['columns'] = [
|
|||
title: '产品名称',
|
||||
field: 'productName',
|
||||
},
|
||||
{
|
||||
title: '产品分类',
|
||||
field: 'categoryName',
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
field: 'deviceType',
|
||||
slots: { default: 'deviceType' },
|
||||
},
|
||||
{
|
||||
title: '启用状态',
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
<script setup lang="ts">
|
||||
import type { ProductVO } from '#/api/device/product/model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { EditOutlined } from '@ant-design/icons-vue';
|
||||
import { Button, Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { deviceTypeOptions } from '#/constants/dicts';
|
||||
import { useProductStore } from '#/store/product';
|
||||
|
||||
import productDrawer from '../../product-drawer.vue';
|
||||
import productParamDrawer from './ProductParamDrawer.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
productInfo: ProductVO;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const productStore = useProductStore();
|
||||
|
||||
const [ProductDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: productDrawer,
|
||||
});
|
||||
|
||||
const [ProductParamDrawer, paramDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: productParamDrawer,
|
||||
});
|
||||
|
||||
// 编辑配置信息
|
||||
const handleEdit = () => {
|
||||
drawerApi.setData({ id: props.productInfo.id });
|
||||
drawerApi.open();
|
||||
};
|
||||
|
||||
// 编辑产品参数
|
||||
const handleEditParams = () => {
|
||||
paramDrawerApi.setData({
|
||||
productId: props.productInfo.id,
|
||||
productParam: props.productInfo.productParam,
|
||||
});
|
||||
paramDrawerApi.open();
|
||||
};
|
||||
|
||||
// 配置接入方式
|
||||
const handleAccessConfig = () => {
|
||||
productStore.setTabActiveKey('DeviceAccess');
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-';
|
||||
};
|
||||
|
||||
// 解析产品参数
|
||||
const parseProductParams = (productParam?: string) => {
|
||||
if (!productParam) return [];
|
||||
|
||||
try {
|
||||
return JSON.parse(productParam);
|
||||
} catch (error) {
|
||||
console.error('解析产品参数失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取产品参数
|
||||
const productParams = computed(() =>
|
||||
parseProductParams(props.productInfo.productParam),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="basic-info">
|
||||
<div class="info-header">
|
||||
<h3>配置信息</h3>
|
||||
<div class="header-actions">
|
||||
<Button
|
||||
type="link"
|
||||
@click="handleEdit"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Descriptions bordered :column="3">
|
||||
<DescriptionsItem label="产品ID">
|
||||
{{ productInfo.id }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="产品KEY">
|
||||
{{ productInfo.productKey }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="产品名称">
|
||||
{{ productInfo.productName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="产品分类">
|
||||
{{ productInfo.categoryName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="设备类型">
|
||||
{{
|
||||
deviceTypeOptions.find(
|
||||
(option) => option.value === productInfo.deviceType,
|
||||
)?.label
|
||||
}}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="接入方式">
|
||||
<Button
|
||||
type="link"
|
||||
@click="handleAccessConfig"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
{{ productInfo.provider || '配置接入方式' }}
|
||||
</Button>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">
|
||||
{{ formatTime(productInfo.createTime) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="更新时间">
|
||||
{{ formatTime(productInfo.updateTime) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="描述" :span="2">
|
||||
{{ productInfo.description || '暂无描述' }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
|
||||
<!-- 产品参数展示 -->
|
||||
<div class="product-params-section">
|
||||
<div class="section-header">
|
||||
<h3>产品参数</h3>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEditParams"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
编辑参数
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="productParams.length === 0" class="empty-params">
|
||||
<p>暂无产品参数</p>
|
||||
</div>
|
||||
|
||||
<Descriptions bordered :column="3" v-else>
|
||||
<DescriptionsItem
|
||||
v-for="param in productParams"
|
||||
:key="param.key"
|
||||
:label="param.label"
|
||||
>
|
||||
{{ param.value }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
<ProductDrawer @reload="emit('refresh')" />
|
||||
<ProductParamDrawer
|
||||
:product-id="productInfo.id"
|
||||
:product-param="productInfo.productParam"
|
||||
@refresh="emit('refresh')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.basic-info {
|
||||
.info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.product-params-section {
|
||||
padding: 16px 0;
|
||||
margin-top: 24px;
|
||||
border-radius: 6px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-params {
|
||||
padding: 20px 0;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
.param-card {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
|
||||
.param-key {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item-label) {
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item-content) {
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,297 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { Button, Input, message } from 'ant-design-vue';
|
||||
|
||||
import { productUpdateById } from '#/api/device/product';
|
||||
|
||||
interface ProductParam {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
productId: number | string;
|
||||
productParam?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
class: 'w-[600px]',
|
||||
onConfirm: handleConfirm,
|
||||
});
|
||||
|
||||
const params = ref<ProductParam[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 监听产品参数变化
|
||||
watch(
|
||||
() => props.productParam,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
try {
|
||||
params.value = JSON.parse(newVal);
|
||||
} catch {
|
||||
params.value = [];
|
||||
}
|
||||
} else {
|
||||
params.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 添加参数
|
||||
const addParam = () => {
|
||||
params.value.push({
|
||||
key: '',
|
||||
label: '',
|
||||
value: '',
|
||||
});
|
||||
};
|
||||
|
||||
// 删除参数
|
||||
const removeParam = (index: number) => {
|
||||
params.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 更新参数
|
||||
const updateParam = (
|
||||
index: number,
|
||||
field: keyof ProductParam,
|
||||
value: string,
|
||||
) => {
|
||||
if (params.value[index]) {
|
||||
params.value[index][field] = value;
|
||||
}
|
||||
};
|
||||
|
||||
// 确认保存
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 验证参数
|
||||
const validParams = params.value.filter(
|
||||
(param) => param.key.trim() && param.label.trim() && param.value.trim(),
|
||||
);
|
||||
|
||||
if (validParams.length === 0) {
|
||||
message.warning('请至少添加一个有效的产品参数');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有重复的key
|
||||
const keys = validParams.map((param) => param.key.trim());
|
||||
const uniqueKeys = new Set(keys);
|
||||
if (keys.length !== uniqueKeys.size) {
|
||||
message.error('参数key不能重复');
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换为JSON字符串
|
||||
const productParam = JSON.stringify(validParams);
|
||||
|
||||
// 调用API更新产品参数
|
||||
await updateProductParam(props.productId, productParam);
|
||||
|
||||
message.success('产品参数保存成功');
|
||||
emit('refresh');
|
||||
drawerApi.close();
|
||||
} catch (error) {
|
||||
console.error('保存产品参数失败:', error);
|
||||
message.error('保存失败,请重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新产品参数API调用
|
||||
async function updateProductParam(id: number | string, productParam: string) {
|
||||
await productUpdateById(id, { id, productParam });
|
||||
}
|
||||
|
||||
// 打开抽屉
|
||||
const open = () => {
|
||||
drawerApi.open();
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer title="编辑产品参数" :loading="loading">
|
||||
<div class="product-param-editor">
|
||||
<div class="param-header">
|
||||
<span class="param-title">产品参数配置</span>
|
||||
<Button type="primary" @click="addParam">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加参数
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="param-list">
|
||||
<div v-if="params.length === 0" class="empty-state">
|
||||
<p>暂无产品参数,请点击"添加参数"按钮添加</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="param-items header-item">
|
||||
<div class="param-item">
|
||||
<div class="param-row">
|
||||
<div class="param-field">
|
||||
<label>参数key:</label>
|
||||
</div>
|
||||
|
||||
<div class="param-field">
|
||||
<label>参数名(单位):</label>
|
||||
</div>
|
||||
|
||||
<div class="param-field">
|
||||
<label>参数值:</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(param, index) in params" :key="index" class="param-item">
|
||||
<div class="param-row">
|
||||
<div class="param-field">
|
||||
<Input
|
||||
:value="param.key"
|
||||
placeholder="请输入参数key"
|
||||
@input="(e) => updateParam(index, 'key', e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-field">
|
||||
<Input
|
||||
:value="param.label"
|
||||
placeholder="请输入参数名和单位,如:电压(V)"
|
||||
@input="(e) => updateParam(index, 'label', e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-field">
|
||||
<Input
|
||||
:value="param.value"
|
||||
placeholder="请输入参数值"
|
||||
@input="(e) => updateParam(index, 'value', e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
@click="removeParam(index)"
|
||||
class="delete-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-tips">
|
||||
<p class="tip-text">
|
||||
<strong>说明:</strong>
|
||||
参数key用于标识参数,参数名(单位)用于显示,参数值为默认值
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.product-param-editor {
|
||||
.param-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.param-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.param-list {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.param-items {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.param-item {
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-bottom: none;
|
||||
|
||||
.param-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
|
||||
.param-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
height: 32px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.param-tips {
|
||||
padding: 12px 16px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 6px;
|
||||
|
||||
.tip-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,219 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, 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';
|
||||
import { message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { productUpdate, productUpdateStatus } from '#/api/device/product';
|
||||
import { useProductStore } from '#/store/product';
|
||||
|
||||
import BasicInfo from './components/BasicInfo.vue';
|
||||
import DeviceAccess from './components/DeviceAccess.vue';
|
||||
import Metadata from './components/Metadata.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const productStore = useProductStore();
|
||||
|
||||
const productId = computed(() => route.params.id as string);
|
||||
const activeTab = computed(() => productStore.getTabActiveKey);
|
||||
const deviceCount = computed(() => productStore.getDeviceCount);
|
||||
const currentProduct = computed(() => productStore.getCurrentProduct);
|
||||
|
||||
// 加载产品信息
|
||||
const loadProductInfo = async () => {
|
||||
try {
|
||||
await productStore.getDetail(productId.value);
|
||||
// TODO: 这里需要添加获取设备数量的逻辑
|
||||
// 暂时设置为 0
|
||||
productStore.setDeviceCount(0);
|
||||
} catch {
|
||||
message.error('加载产品信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理状态变更
|
||||
const handleStatusChange = async (checked: boolean) => {
|
||||
try {
|
||||
await productUpdateStatus({
|
||||
id: productId.value,
|
||||
enabled: checked,
|
||||
});
|
||||
await loadProductInfo();
|
||||
} catch {
|
||||
message.error('状态更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 应用配置
|
||||
const handleApplyConfig = async () => {
|
||||
try {
|
||||
await productUpdate({
|
||||
id: productId.value,
|
||||
enabled: '1',
|
||||
});
|
||||
message.success('配置应用成功');
|
||||
await loadProductInfo();
|
||||
} catch {
|
||||
message.error('配置应用失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到设备列表
|
||||
const jumpToDevices = () => {
|
||||
router.push({
|
||||
path: '/device/instance',
|
||||
query: {
|
||||
productId: productId.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 标签页切换
|
||||
const handleTabChange = (key: string) => {
|
||||
productStore.setTabActiveKey(key);
|
||||
if (key === 'BasicInfo') {
|
||||
loadProductInfo();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadProductInfo();
|
||||
});
|
||||
|
||||
// 组件销毁时重置 store
|
||||
onUnmounted(() => {
|
||||
productStore.reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="product-detail">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
<div class="header-left">
|
||||
<a-button @click="goBack" class="back-btn">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
返回
|
||||
</a-button>
|
||||
<div class="product-info">
|
||||
<h2 class="product-name">{{ currentProduct.productName }}</h2>
|
||||
<div class="product-status">
|
||||
<Switch
|
||||
:checked="currentProduct.enabled"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
checked-value="1"
|
||||
un-checked-value="0"
|
||||
@change="handleStatusChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleApplyConfig"
|
||||
:disabled="currentProduct.enabled === '0'"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
应用配置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 产品统计信息 -->
|
||||
<div class="product-stats">
|
||||
<span>设备数量:</span><a @click="jumpToDevices">{{ deviceCount }}</a>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<Tabs
|
||||
v-model:active-key="activeTab"
|
||||
class="detail-tabs"
|
||||
@change="handleTabChange"
|
||||
>
|
||||
<TabPane key="BasicInfo" tab="配置信息">
|
||||
<BasicInfo
|
||||
:product-info="currentProduct"
|
||||
@refresh="loadProductInfo"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="Metadata" tab="物模型">
|
||||
<Metadata :product-id="productId" />
|
||||
</TabPane>
|
||||
<TabPane key="DeviceAccess" tab="设备接入">
|
||||
<DeviceAccess
|
||||
:product-info="currentProduct"
|
||||
@refresh="loadProductInfo"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.product-detail {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.back-btn {
|
||||
// margin-right: 16px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.product-name {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.product-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-stats {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
:deep(.ant-tabs-content-holder) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,6 +4,8 @@ import type { VbenFormProps } from '@vben/common-ui';
|
|||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { ProductForm } from '#/api/device/product/model';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
|
@ -16,6 +18,8 @@ import {
|
|||
productRemove,
|
||||
} from '#/api/device/product';
|
||||
import { productCategoryTreeList } from '#/api/device/productCategory';
|
||||
import { deviceTypeOptions } from '#/constants/dicts';
|
||||
import { useProductStore } from '#/store/product';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
|
@ -40,6 +44,8 @@ const formOptions: VbenFormProps = {
|
|||
// ],
|
||||
// ],
|
||||
};
|
||||
const router = useRouter();
|
||||
const productStore = useProductStore();
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
|
@ -111,6 +117,11 @@ function handleAdd() {
|
|||
}
|
||||
|
||||
async function handleView(row: Required<ProductForm>) {
|
||||
// 保存产品信息到 store
|
||||
productStore.setCurrent(row);
|
||||
// 设置默认选中的 tab
|
||||
productStore.setTabActiveKey('BasicInfo');
|
||||
// 跳转到详情页
|
||||
router.push(`/device/product/detail/${row.id}`);
|
||||
}
|
||||
|
||||
|
@ -179,6 +190,14 @@ function handleDownloadExcel() {
|
|||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #deviceType="{ row }">
|
||||
<Tag color="processing">
|
||||
{{
|
||||
deviceTypeOptions.find((option) => option.value === row.deviceType)
|
||||
?.label
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #enabled="{ row }">
|
||||
<Tag :color="row.enabled === '1' ? 'success' : 'error'">
|
||||
{{ row.enabled === '1' ? '启用' : '禁用' }}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
networkList,
|
||||
networkRemove,
|
||||
} from '#/api/operations/network';
|
||||
import { networkTypeOptions } from '#/constants/dicts';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
|
@ -122,31 +123,6 @@ function handleDownloadExcel() {
|
|||
);
|
||||
}
|
||||
|
||||
// 解析网络配置JSON
|
||||
function parseNetworkConfig(networkConfig: string) {
|
||||
try {
|
||||
return JSON.parse(networkConfig);
|
||||
} catch (error) {
|
||||
console.error('解析网络配置失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化网络配置显示
|
||||
function formatNetworkConfig(row: any) {
|
||||
const config = parseNetworkConfig(row.networkConfig);
|
||||
if (!config) {
|
||||
return '配置解析失败';
|
||||
}
|
||||
|
||||
if (row.networkType === 'HTTP_SERVER') {
|
||||
return `本地: ${config.localAddress}:${config.localPort} | 公网: ${config.publicAddress}:${config.publicPort} | TLS: ${config.enableTls === '1' ? '是' : '否'}`;
|
||||
} else if (row.networkType === 'MQTT_CLIENT') {
|
||||
return `远程: ${config.remoteAddress}:${config.remotePort} | ClientId: ${config.clientId} | TLS: ${config.enableTls === '1' ? '是' : '否'}`;
|
||||
}
|
||||
|
||||
return '未知配置';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -179,7 +155,13 @@ function formatNetworkConfig(row: any) {
|
|||
</Space>
|
||||
</template>
|
||||
<template #networkType="{ row }">
|
||||
<Tag color="processing">{{ row.networkType }}</Tag>
|
||||
<Tag color="processing">
|
||||
{{
|
||||
networkTypeOptions.find(
|
||||
(option) => option.value === row.networkType,
|
||||
)?.label
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #enabled="{ row }">
|
||||
<Tag :color="row.enabled === '1' ? 'success' : 'error'">
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { VxeGridProps } from '#/adapter/vxe-table';
|
|||
|
||||
import { enabledOptions } from '#/constants/dicts';
|
||||
|
||||
const protocolTypeOptions = [
|
||||
export const protocolTypeOptions = [
|
||||
{ label: 'local', value: 'local' },
|
||||
{ label: 'jar', value: 'jar' },
|
||||
];
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from '#/api/operations/protocol';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import { columns, protocolTypeOptions, querySchema } from './data';
|
||||
import protocolDrawer from './protocol-drawer.vue';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
|
@ -153,7 +153,13 @@ function handleDownloadExcel() {
|
|||
</Space>
|
||||
</template>
|
||||
<template #protocolType="{ row }">
|
||||
<Tag color="processing">{{ row.protocolType }}</Tag>
|
||||
<Tag color="processing">
|
||||
{{
|
||||
protocolTypeOptions.find(
|
||||
(option) => option.value === row.protocolType,
|
||||
)?.label
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #enabled="{ row }">
|
||||
<Tag :color="row.enabled === '1' ? 'success' : 'error'">
|
||||
|
|
Loading…
Reference in New Issue