feat(DeviceSn): 添加设备组件并更新本地接口地址

This commit is contained in:
2026-07-30 17:13:38 +08:00
parent 1ff46bc33b
commit 742b32490f
22 changed files with 3767 additions and 2 deletions

View File

@@ -0,0 +1,138 @@
import UserAPI from "@/api/system/user";
import type { UserForm } from "@/types/api";
import type { IModalConfig } from "@/components/DeviceSn/types";
import { deptArr, roleArr } from "./options";
const modalConfig: IModalConfig<UserForm> = {
permPrefix: "sys:user",
dialog: {
title: "新增用户",
width: 800,
draggable: true,
},
form: {
labelWidth: 100,
},
formAction: UserAPI.create,
beforeSubmit(data) {
console.warn("提交之前处理", data);
},
formItems: [
{
label: "用户名",
prop: "username",
rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入用户名",
},
col: {
xs: 24,
sm: 12,
},
},
{
label: "用户昵称",
prop: "nickname",
rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入用户昵称",
},
col: {
xs: 24,
sm: 12,
},
},
{
label: "所属部门",
prop: "deptId",
rules: [{ required: true, message: "所属部门不能为空", trigger: "change" }],
type: "tree-select",
attrs: {
placeholder: "请选择所属部门",
data: deptArr,
filterable: true,
"check-strictly": true,
"render-after-expand": false,
},
// async initFn(formItem) {
// // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
// formItem.attrs.data = await DeptAPI.getOptions();
// },
},
{
type: "custom",
label: "性别",
prop: "gender",
initialValue: 1,
attrs: { style: { width: "100%" } },
},
{
label: "角色",
prop: "roleIds",
rules: [{ required: true, message: "用户角色不能为空", trigger: "change" }],
type: "select",
attrs: {
placeholder: "请选择",
multiple: true,
},
options: roleArr,
initialValue: [],
// async initFn(formItem) {
// formItem.options = await RoleAPI.getOptions();
// },
},
{
type: "input",
label: "手机号码",
prop: "mobile",
rules: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
attrs: {
placeholder: "请输入手机号码",
maxlength: 11,
},
},
{
label: "邮箱",
prop: "email",
rules: [
{
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
message: "请输入正确的邮箱地址",
trigger: "blur",
},
],
type: "input",
attrs: {
placeholder: "请输入邮箱",
maxlength: 50,
},
},
{
label: "状态",
prop: "status",
type: "radio",
options: [
{ label: "正常", value: 1 },
{ label: "禁用", value: 0 },
],
initialValue: 1,
},
{
type: "custom",
label: "二级弹窗",
prop: "openModal",
slotName: "openModal",
},
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,182 @@
import DeviceAPI from "@/api/system/device";
import RoleAPI from "@/api/system/role";
import type { DeviceQueryParams, DeviceItem } from "@/api/system/device/types";
import type { IContentConfig } from "@/components/DeviceSn/types";
const contentConfig: IContentConfig<DeviceQueryParams, DeviceItem> = {
permPrefix: "sys:user", // 不写不进行按钮权限校验
table: {
border: true,
highlightCurrentRow: true,
},
pagination: {
background: true,
layout: "prev,pager,next,jumper,total,sizes",
pageSize: 20,
pageSizes: [10, 20, 30, 50],
},
indexAction(params) {
return DeviceAPI.getPage(params);
},
deleteAction: DeviceAPI.deleteByIds,
importAction(file) {
return DeviceAPI.import(file);
},
exportAction: DeviceAPI.export,
importTemplate: DeviceAPI.downloadTemplate,
importsAction(data) {
// 模拟导入数据
console.warn("importsAction", data);
return Promise.resolve();
},
async exportsAction(params) {
// 模拟获取到的是全量数据
const data = await DeviceAPI.getPage(params);
console.warn("exportsAction", data.list);
return data.list;
},
pk: "id",
toolbar: [
"add",
"delete",
"import",
"export",
{
name: "custom1",
text: "自定义1",
perm: "add",
attrs: { icon: "plus", color: "#626AEF" },
},
],
defaultToolbar: ["refresh", "filter", "imports", "exports", "search"],
cols: [
{ type: "selection", width: 50, align: "center" },
{ label: "编号", align: "center", prop: "id", width: 100, show: false },
{
label: "SN",
align: "center",
prop: "serialno",
templet: "custom",
slotName: "serialno",
width: 150,
},
{ label: "设备型号", align: "center", prop: "snModel", width: 200 },
{ label: "设备昵称", align: "center", prop: "snName", width: 200 },
{ label: "推送ID", align: "center", prop: "pushId", width: 200 },
// { label: "设备 IMEI", align: "center", prop: "snImei", width: 200 },
// { label: "设备版本号", align: "center", prop: "snDispalyId", width: 400 },
// { label: "头像", align: "center", prop: "avatar", templet: "image" },
// {
// label: "性别",
// align: "center",
// prop: "gender",
// width: 100,
// templet: "custom",
// slotName: "gender",
// },
// { label: "部门", align: "center", prop: "deptName", width: 120 },
// {
// label: "角色",
// align: "center",
// prop: "roleNames",
// width: 120,
// columnKey: "roleIds",
// filters: [],
// filterMultiple: true,
// filterJoin: ",",
// async initFn(colItem) {
// const roleOptions = await RoleAPI.getOptions();
// colItem.filters = roleOptions.map((item) => {
// return { text: item.label, value: item.value };
// });
// },
// },
{
label: "手机号码",
align: "center",
prop: "snMobile",
templet: "custom",
slotName: "mobile",
width: 150,
},
{
label: "状态",
align: "center",
prop: "status",
templet: "custom",
slotName: "status",
width: 120,
},
{ label: "创建时间", align: "center", prop: "createTime", width: 180 },
{ label: "激活时间", align: "center", prop: "activateTime", width: 180 },
{
label: "操作",
align: "center",
fixed: "right",
width: 280,
templet: "tool",
operat: [
{
name: "detail",
text: "详情",
attrs: { icon: "Document", type: "primary" },
},
// {
// name: "reset_pwd",
// text: "重置密码",
// // perm: "password-reset",
// attrs: {
// icon: "refresh-left",
// // color: "#626AEF", // 使用 text 属性,颜色不生效
// style: {
// "--el-button-text-color": "#626AEF",
// "--el-button-hover-link-text-color": "#9197f4",
// },
// },
// },
"edit",
"delete",
{
name: "more", // 主按钮名称
text: "更多", // 显示文本
attrs: { icon: "more", type: "primary" }, // 图标
// children 数组定义下拉菜单项
children: [
{
name: "refresh",
text: "刷新",
attrs: { icon: "Refresh" },
},
{
name: "screenshot",
text: "截图",
attrs: { icon: "FullScreen" },
},
{
name: "locate",
text: "定位",
attrs: { icon: "Position" },
},
{
name: "shutdown",
text: "关机",
attrs: { icon: "SwitchButton" },
},
{
name: "reboot",
text: "重启",
attrs: { icon: "SwitchButton" },
},
{
name: "refactory",
text: "重置",
attrs: { icon: "RefreshLeft" },
},
],
},
],
},
],
};
export default contentConfig;

View File

@@ -0,0 +1,128 @@
import UserAPI from "@/api/system/user";
import type { UserForm } from "@/api/system/user/types";
import type { IModalConfig } from "@/components/DeviceSn/types";
import { DeviceEnum } from "@/enums/settings";
import { useAppStore } from "@/stores";
import { deptArr, roleArr } from "./options";
const modalConfig: IModalConfig<UserForm> = {
permPrefix: "sys:user",
component: "drawer",
drawer: {
title: "修改用户",
size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500,
},
pk: "id",
beforeSubmit(data) {
console.warn("beforeSubmit", data);
},
formAction(data) {
return UserAPI.update(data.id as string, data);
},
formItems: [
{
label: "用户名",
prop: "username",
rules: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入用户名",
readonly: true,
},
},
{
label: "用户昵称",
prop: "nickname",
rules: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入用户昵称",
},
},
{
label: "所属部门",
prop: "deptId",
rules: [{ required: true, message: "所属部门不能为空", trigger: "blur" }],
type: "tree-select",
attrs: {
placeholder: "请选择所属部门",
data: deptArr, // setupVue会自动解包ref不需要.value
filterable: true,
"check-strictly": true,
"render-after-expand": false,
},
// async initFn(formItem) {
// // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
// formItem.attrs.data = await DeptAPI.getOptions();
// },
},
{
type: "custom",
label: "性别",
prop: "gender",
initialValue: 1,
attrs: { style: { width: "100%" } },
},
{
label: "角色",
prop: "roleIds",
rules: [{ required: true, message: "用户角色不能为空", trigger: "blur" }],
type: "select",
attrs: {
placeholder: "请选择",
multiple: true,
},
options: roleArr,
initialValue: [],
// async initFn(formItem) {
// formItem.options = await RoleAPI.getOptions();
// },
},
{
type: "input",
label: "手机号码",
prop: "mobile",
rules: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
attrs: {
placeholder: "请输入手机号码",
maxlength: 11,
},
},
{
label: "邮箱",
prop: "email",
rules: [
{
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
message: "请输入正确的邮箱地址",
trigger: "blur",
},
],
type: "input",
attrs: {
placeholder: "请输入邮箱",
maxlength: 50,
},
},
{
label: "状态",
prop: "status",
type: "switch",
attrs: {
inlinePrompt: true,
activeText: "正常",
inactiveText: "禁用",
activeValue: 1,
inactiveValue: 0,
},
},
],
};
export default reactive(modalConfig);

View File

@@ -0,0 +1,27 @@
/** 公共下载数据,减少重复请求次数 */
import DeptAPI from "@/api/system/dept";
import RoleAPI from "@/api/system/role";
interface OptionItem {
label: string;
value: any;
[key: string]: any; // 允许其他属性
}
// 明确指定类型为 OptionItem[]
export const deptArr = ref<OptionItem[]>([]);
export const roleArr = ref<OptionItem[]>([]);
export const stateArr = ref<OptionItem[]>([
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 },
]);
// 初始化逻辑,在 onMounted 钩子中调用
export const initOptions = async () => {
// 使用Promise.all并行请求
const [dept, roles] = await Promise.all([DeptAPI.getOptions(), RoleAPI.getOptions()]);
// 获取部门选项并赋值
deptArr.value = dept;
// 获取角色选项并赋值
roleArr.value = roles;
};

View File

@@ -0,0 +1,63 @@
import type { ISearchConfig } from "@/components/DeviceSn/types";
import { deptArr, stateArr } from "./options";
const searchConfig: ISearchConfig = {
permPrefix: "sys:user",
formItems: [
{
tips: "支持模糊搜索",
type: "input",
label: "关键字",
prop: "keywords",
attrs: {
placeholder: "用户名/昵称/手机号",
clearable: true,
style: { width: "200px" },
},
},
{
type: "tree-select",
label: "部门",
prop: "deptId",
attrs: {
placeholder: "请选择",
data: deptArr,
filterable: true,
"check-strictly": true,
"render-after-expand": false,
clearable: true,
style: { width: "200px" },
},
// async initFn(formItem) {
// // 注意:如果initFn函数不是箭头函数,this会指向此配置项对象,那么也就可以用this来替代形参formItem
// formItem.attrs.data = await DeptAPI.getOptions();
// },
},
{
type: "select",
label: "状态",
prop: "status",
attrs: {
placeholder: "全部",
clearable: true,
style: { width: "200px" },
},
options: stateArr,
},
{
type: "date-picker",
label: "创建时间",
prop: "createTime",
attrs: {
type: "daterange",
"range-separator": "~",
"start-placeholder": "开始时间",
"end-placeholder": "截止时间",
"value-format": "YYYY-MM-DD",
style: { width: "200px" },
},
},
],
};
export default searchConfig;

View File

@@ -0,0 +1,223 @@
<template>
<div class="app-container h-full flex flex-1 flex-col">
<!-- 列表 -->
<div v-show="!dialogVisible" class="flex flex-col h-full">
<!-- 搜索 -->
<device-page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<device-page-content
ref="contentRef"
:content-config="contentConfig"
class="flex-1 overflow-hidden"
@add-click="handleAddClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operate-click="handleOperateClick"
@filter-change="handleFilterChange"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop as string] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop as string] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<template #gender="scope">
<DictTag v-model="scope.row[scope.prop as string]" code="gender" />
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop as string] }}</el-text>
<copy-button
v-if="scope.row[scope.prop as string]"
:text="scope.row[scope.prop as string]"
:style="{ marginLeft: '2px' }"
/>
</template>
<template #serialno="scope">
<el-link type="primary" @click="handleSerialnoClick(scope.row)">
{{ scope.row[scope.prop as string] }}
</el-link>
<copy-button
v-if="scope.row[scope.prop as string]"
:text="scope.row[scope.prop as string]"
:style="{ marginLeft: '2px' }"
/>
</template>
</device-page-content>
<!-- 新增 -->
<device-page-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<DictSelect v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template>
<template #openModal>
<el-button type="primary" @click="openSecondModal">打开二级弹窗</el-button>
</template>
</device-page-modal>
<!-- 编辑 -->
<device-page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<DictSelect v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template>
</device-page-modal>
</div>
<div v-show="dialogVisible">
<device-sn-detail
v-model="dialogVisible"
:serialno="selectedRow?.serialno"
@return-click="handleReturnClick"
/>
</div>
</div>
</template>
<script setup lang="ts">
import DeviceAPI from "@/api/system/device";
import type { IObject, PageModalInstance } from "@/components/DeviceSn/types";
import usePage from "@/components/DeviceSn/usePage";
import addModalConfig from "./config/add";
import contentConfig from "./config/content";
import editModalConfig from "./config/edit";
import searchConfig from "./config/search";
import { initOptions } from "./config/options";
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
handleAddClick,
handleEditClick,
handleViewClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 其他工具
function handleToolbarClick(name: string) {
console.warn(name);
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
function handleReturnClick() {
dialogVisible.value = false;
}
// 表格工具
const handleOperateClick = (data: IObject) => {
if (data.name === "detail") {
editModalConfig.drawer = { ...editModalConfig.drawer, title: "查看" };
handleViewClick(data.row, async () => {
// 加载下拉数据源,建议在初始化配置项"initFn 中加载,避免多次请求
// editModalConfig.formItems[2]!.attrs!.data = await DeptAPI.getOptions();
return await DeviceAPI.getFormData(data.row.serialno as string); // 根据SN获取详情
});
} else if (data.name === "edit") {
editModalConfig.drawer = { ...editModalConfig.drawer, title: "修改" };
handleEditClick(data.row, async () => {
return await DeviceAPI.getFormData(data.row.serialno as string); // 根据SN获取详情
});
} else if (data.name === "reset_pwd") {
ElMessageBox.prompt("请输入用户名" + data.row.username + "」的新密码", "重置密码", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).then(
(result) => {
const value = result.value;
if (!value || value.length < 6) {
ElMessage.warning("密码至少需6位字符请重新输入");
return false;
}
DeviceAPI.resetPassword(data.row.id, value).then(() => {
ElMessage.success("密码重置成功,新密码是:" + value);
});
},
() => {
// 用户取消
}
);
} else if (data.name === "more") {
// 处理新增的自定义操作
ElMessage.info(`执行了自定义操作设备ID: ${data.row.id}`);
} else if (data.name === "refresh") {
DeviceAPI.deviceRefresh(data.row.serialno as string).then(() => {
ElMessage.success(`刷新设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "screenshot") {
DeviceAPI.deviceScreenshot(data.row.serialno as string).then(() => {
ElMessage.success(`截图设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "reboot") {
ElMessageBox.confirm("确定要重启该设备吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
DeviceAPI.deviceReboot(data.row.serialno as string).then(() => {
ElMessage.success(`重启设备设备SN: ${data.row.serialno as string}`);
});
})
.catch(() => {});
} else if (data.name === "shutdown") {
DeviceAPI.deviceShutdown(data.row.serialno as string).then(() => {
ElMessage.success(`关机设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "locate") {
DeviceAPI.deviceLocate(data.row.serialno as string).then(() => {
ElMessage.success(`定位设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "refactory") {
ElMessageBox.confirm("确定要重置该设备吗?此操作将清除设备所有数据,是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "error",
})
.then(() => {
DeviceAPI.deviceRestore(data.row.serialno as string).then(() => {
ElMessage.success(`重置设备设备SN: ${data.row.serialno as string}`);
});
})
.catch(() => {});
}
};
// 打开二级弹窗
const addModalRef2 = ref();
const openSecondModal = () => {
handleAddClick(addModalRef2 as Ref<PageModalInstance>);
};
// SN点击事件 - 打开详情弹窗
const handleSerialnoClick = async (row: IObject) => {
// 将当前行的数据传递给详情组件
selectedRow.value = row;
dialogVisible.value = true;
};
const dialogVisible = ref(false);
const selectedRow = ref<IObject>({});
onMounted(() => {
initOptions();
});
</script>