feat(device): 新增开发者模式指令及下拉菜单样式优化

新增推送开发者模式指令到设备的 API。

优化设备页面表格工具栏更多按钮的下拉菜单:
- 点击触发并支持点击菜单项后自动隐藏
- 移除触发按钮 hover 时的背景色
- 调整下拉菜单项的布局、间距与圆角样式
This commit is contained in:
TongTongStudio
2026-07-31 02:14:30 +08:00
parent bd348e7aeb
commit d698dd7395
5 changed files with 494 additions and 1 deletions

View File

@@ -0,0 +1,62 @@
import request from "@/utils/request";
import type { DeveloperQueryParams, DeveloperForm, DeveloperItem } from "./types";
import type { PageResult } from "@/api/common";
const DEVELOPER_BASE_URL = "/api/v1/developers";
const DeveloperAPI = {
/** 获取开发者选项分页数据 */
getPage(queryParams?: DeveloperQueryParams) {
return request<unknown, PageResult<DeveloperItem>>({
url: `${DEVELOPER_BASE_URL}`,
method: "get",
params: queryParams,
});
},
/** 获取开发者选项表单数据 */
getFormData(id: string) {
return request<unknown, DeveloperForm>({
url: `${DEVELOPER_BASE_URL}/${id}/form`,
method: "get",
});
},
/** 新增开发者选项配置 */
create(data: DeveloperForm) {
return request({ url: `${DEVELOPER_BASE_URL}`, method: "post", data });
},
/** 修改开发者选项配置 */
update(id: string, data: DeveloperForm) {
return request({ url: `${DEVELOPER_BASE_URL}/${id}`, method: "put", data });
},
/** 批量删除开发者选项配置,多个以英文逗号(,)分隔 */
deleteByIds(ids: string) {
return request({ url: `${DEVELOPER_BASE_URL}/${ids}`, method: "delete" });
},
/** 根据设备序列号获取开发者选项开关 */
getBySn(sn: string) {
return request<unknown, number>({
url: `${DEVELOPER_BASE_URL}/sn/${sn}`,
method: "get",
});
},
/** 根据设备序列号保存或更新开发者选项开关 */
saveOrUpdateBySn(sn: string, developerOptions: number) {
return request({
url: `${DEVELOPER_BASE_URL}/sn/${sn}`,
method: "put",
params: { developerOptions },
});
},
/** 推送开发者模式指令到设备 */
pushBySn(sn: string) {
return request({
url: `${DEVELOPER_BASE_URL}/sn/${sn}/push`,
method: "post",
});
},
};
export default DeveloperAPI;
// 重导出类型
export * from "./types";

View File

@@ -0,0 +1,37 @@
/**
* Developer 设备开发者选项类型定义
*/
import type { BaseQueryParams } from "@/api/common";
/** 开发者选项分页查询参数 */
export interface DeveloperQueryParams extends BaseQueryParams {
/** 搜索关键字(设备序列号) */
keywords?: string;
/** 开发者选项开关0关闭1开启 */
developerOptions?: number;
}
/** 开发者选项表单对象 */
export interface DeveloperForm {
/** 配置ID */
id?: string;
/** 设备序列号 */
sn?: string;
/** 开发者选项开关0关闭1开启 */
developerOptions?: number;
}
/** 开发者选项分页对象 */
export interface DeveloperItem {
/** 配置ID */
id: string;
/** 设备序列号 */
sn: string;
/** 开发者选项开关0关闭1开启 */
developerOptions: number;
/** 创建时间 */
createTime?: string;
/** 更新时间 */
updateTime?: string;
}

View File

@@ -307,6 +307,18 @@ const DeviceAPI = {
params: { sn },
});
},
/**
* 推送开发者模式指令到设备
* @param sn 设备SN号
*/
deviceDeveloper(sn: string) {
return request({
url: `${DEVICE_BASE_URL}/developer`,
method: "post",
params: { sn },
});
},
};
export default DeviceAPI;

View File

@@ -160,11 +160,17 @@
<template v-else-if="col.templet === 'tool'">
<div class="flex items-center justify-center gap-x-2">
<template v-for="(btn, index) in tableToolbarBtn" :key="index">
<el-dropdown v-if="btn.children !== undefined && btn.children.length > 0">
<el-dropdown
v-if="btn.children !== undefined && btn.children.length > 0"
trigger="click"
:hide-on-click="true"
popper-class="more-dropdown-menu"
>
<el-button
v-if="btn.render === undefined || btn.render(scope.row)"
v-hasPerm="btn.perm ?? '*:*:*'"
v-bind="btn.attrs"
class="more-trigger-btn"
>
{{ btn.text }}
<el-icon class="el-icon--right"><arrow-down /></el-icon>
@@ -976,4 +982,40 @@ defineExpose({ fetchPageData, exportPageData, getFilterParams, getSelectionData,
margin-left: 0 !important;
}
}
/* 更多按钮:去掉 link 类型 hover 时的浅蓝背景层 */
.more-trigger-btn {
&:hover,
&:focus,
&:focus-visible {
background-color: transparent !important;
}
}
</style>
<!-- scoped下拉菜单通过 teleport 渲染到 body需用全局样式覆盖 -->
<style lang="scss">
.more-dropdown-menu {
.el-dropdown-menu {
gap: 2px;
padding: 6px;
}
.el-dropdown-menu__item {
display: flex;
gap: 8px;
align-items: center;
padding: 8px 16px;
line-height: 1.4;
border-radius: 6px;
& + .el-dropdown-menu__item {
margin-top: 2px;
}
.el-icon {
margin-right: 0;
}
}
}
</style>

View File

@@ -0,0 +1,340 @@
<template>
<div class="page-container">
<el-card class="page-search" shadow="never">
<el-form ref="queryFormRef" :model="params" :inline="true">
<el-form-item label="设备序列号" prop="keywords">
<el-input
v-model="params.keywords"
placeholder="请输入设备序列号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="开关状态" prop="developerOptions">
<el-select
v-model="params.developerOptions"
placeholder="全部"
clearable
style="width: 140px"
@change="handleQuery"
>
<el-option label="已开启" :value="1" />
<el-option label="已关闭" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card ref="tableWrapperRef" class="page-content" shadow="never">
<div class="page-toolbar">
<div class="page-toolbar__left">
<el-button v-hasPerm="['device:developer:create']" type="primary" @click="openDialog()">
新增
</el-button>
<el-button
v-hasPerm="['device:developer:delete']"
type="danger"
:disabled="selectIds.length === 0"
@click="handleDelete()"
>
批量删除
</el-button>
</div>
<div class="page-toolbar__right">
<el-tooltip content="刷新" placement="top">
<el-button class="page-icon-btn" @click="handleQuery">
<el-icon><Refresh /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="全屏" placement="top">
<el-button class="page-icon-btn" @click="toggleFullscreen">
<el-icon><FullScreen /></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
<div class="page-table-wrapper">
<el-table
ref="dataTableRef"
v-loading="loading"
:data="list"
class="page-table"
height="100%"
highlight-current-row
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="序号" width="60" />
<el-table-column key="sn" label="设备序列号" prop="sn" min-width="180" />
<el-table-column key="developerOptions" label="开发者选项" width="140" align="center">
<template #default="scope">
<el-switch
:model-value="scope.row.developerOptions === 1"
:loading="switchLoadingSn === scope.row.sn"
:disabled="!hasSwitchPerm"
inline-prompt
active-text=""
inactive-text=""
@change="
(val: string | number | boolean) =>
handleSwitchChange(scope.row as DeveloperItem, Boolean(val))
"
/>
</template>
</el-table-column>
<el-table-column key="createTime" label="创建时间" prop="createTime" min-width="160" />
<el-table-column key="updateTime" label="更新时间" prop="updateTime" min-width="160" />
<el-table-column fixed="right" label="操作" width="240">
<template #default="scope">
<el-button
v-hasPerm="['device:developer:push']"
type="success"
size="small"
link
@click="handlePush(scope.row.sn)"
>
推送设备
</el-button>
<el-button
v-hasPerm="['device:developer:update']"
type="primary"
size="small"
link
@click="openDialog(scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPerm="['device:developer:delete']"
type="danger"
size="small"
link
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="params.pageNum"
v-model:limit="params.pageSize"
@pagination="fetchData"
/>
</el-card>
<el-dialog
v-model="dialogState.visible"
:title="dialogState.title"
width="500px"
@close="closeDialog"
>
<el-form
ref="dataFormRef"
:model="formData"
:rules="rules"
label-suffix=":"
label-width="120px"
>
<el-form-item label="设备序列号" prop="sn">
<el-input
v-model="formData.sn"
placeholder="请输入设备序列号"
:maxlength="64"
:disabled="!!formData.id"
/>
</el-form-item>
<el-form-item label="开发者选项" prop="developerOptions">
<el-radio-group v-model="formData.developerOptions">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确定</el-button>
<el-button @click="closeDialog">取消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import { Refresh, FullScreen } from "@element-plus/icons-vue";
import DeveloperAPI from "@/api/system/developer";
import type { DeveloperItem, DeveloperForm, DeveloperQueryParams } from "@/api/system/developer";
import { usePageTable } from "@/composables";
import { hasPerm } from "@/utils/auth";
defineOptions({
name: "Developer",
inheritAttrs: false,
});
const tableWrapperRef = ref<HTMLElement | null>(null);
const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
const queryFormRef = ref<FormInstance>();
const dataFormRef = ref<FormInstance>();
/** 分页表格数据管理 */
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
DeveloperItem,
DeveloperQueryParams
>({
initialParams: { pageNum: 1, pageSize: 10, keywords: "", developerOptions: undefined },
request: DeveloperAPI.getPage,
onBeforeReset: () => queryFormRef.value?.resetFields(),
});
const selectIds = ref<string[]>([]);
const switchLoadingSn = ref<string>("");
/** 是否拥有开关快捷切换权限 */
const hasSwitchPerm = computed(() => hasPerm("device:developer:update"));
const dialogState = reactive({ title: "", visible: false });
const formData = reactive<DeveloperForm>({
id: undefined,
sn: "",
developerOptions: 0,
});
const rules: FormRules = {
sn: [{ required: true, message: "请输入设备序列号", trigger: "blur" }],
developerOptions: [{ required: true, message: "请选择开发者选项开关", trigger: "change" }],
};
function handleSelectionChange(selection: DeveloperItem[]): void {
selectIds.value = selection.map((item) => item.id);
}
/**
* 打开新增/编辑开发者选项弹窗
*/
async function openDialog(id?: string): Promise<void> {
dialogState.visible = true;
if (id) {
dialogState.title = "修改开发者选项";
const data = await DeveloperAPI.getFormData(id);
Object.assign(formData, data);
} else {
dialogState.title = "新增开发者选项";
formData.id = undefined;
formData.sn = "";
formData.developerOptions = 0;
}
}
/**
* 表格内快捷切换开发者选项开关
*/
async function handleSwitchChange(row: DeveloperItem, checked: boolean): Promise<void> {
switchLoadingSn.value = row.sn;
try {
await DeveloperAPI.saveOrUpdateBySn(row.sn, checked ? 1 : 0);
row.developerOptions = checked ? 1 : 0;
ElMessage.success(checked ? "已开启开发者选项" : "已关闭开发者选项");
} finally {
switchLoadingSn.value = "";
}
}
/**
* 推送开发者模式指令到设备
*/
async function handlePush(sn: string): Promise<void> {
await ElMessageBox.confirm(`确认向设备【${sn}】推送开发者模式指令?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
loading.value = true;
try {
await DeveloperAPI.pushBySn(sn);
ElMessage.success("推送成功");
} finally {
loading.value = false;
}
}
/**
* 校验并提交开发者选项表单
*/
async function handleSubmit(): Promise<void> {
const valid = await dataFormRef.value?.validate().catch(() => false);
if (!valid) return;
loading.value = true;
try {
const id = formData.id;
if (id) {
await DeveloperAPI.update(id, formData);
ElMessage.success("修改成功");
} else {
await DeveloperAPI.create(formData);
ElMessage.success("新增成功");
}
closeDialog();
handleResetQuery();
} finally {
loading.value = false;
}
}
/**
* 关闭弹窗并重置表单
*/
function closeDialog(): void {
dialogState.visible = false;
dataFormRef.value?.resetFields();
dataFormRef.value?.clearValidate();
formData.id = undefined;
}
/**
* 删除开发者选项配置
*/
async function handleDelete(id?: string): Promise<void> {
const ids = id ? [id] : selectIds.value;
if (ids.length === 0) {
ElMessage.warning("请勾选要删除的数据");
return;
}
await ElMessageBox.confirm("确认删除选中的开发者选项配置?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
loading.value = true;
try {
await DeveloperAPI.deleteByIds(ids.join(","));
ElMessage.success("删除成功");
selectIds.value = [];
handleResetQuery();
} finally {
loading.value = false;
}
}
onMounted(() => {
handleQuery();
});
</script>