feat: 项目初始化,未病局

This commit is contained in:
fuenmao
2025-09-20 10:36:39 +08:00
commit 39f70340a0
421 changed files with 48180 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
<template>
<div>
<div class="card mb10">
<h4 class="title">简介</h4>
<span class="text">
<el-link type="primary" href="https://docs.spicyboy.cn" target="_blank">Geeker-Admin</el-link>
一款基于 Vue3.3、TypeScript、Vite4、Pinia、Element-Plus 开源的后台管理框架,使用目前最新技术栈开发。项目提供强大的
<el-link type="primary" href="https://juejin.cn/post/7166068828202336263" target="_blank">ProTable 组件</el-link>
在一定程度上提高您的开发效率。另外本项目还封装了一些常用组件、Hooks、指令、动态路由、按钮级别权限控制等功能。
</span>
</div>
<div class="card mb10">
<h4 class="title">项目信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="版本号" label-align="left">
<el-tag>{{ version }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="发布时间" label-align="left">
<el-tag>{{ lastBuildTime }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="Gitee" label-align="left">
<el-link type="primary" href="https://gitee.com/HalseySpicy/Geeker-Admin" target="_blank"> Gitee </el-link>
</el-descriptions-item>
<el-descriptions-item label="Github" label-align="left">
<el-link type="primary" href="https://gitee.com/dotnetmoyu/SimpleAdmin" target="_blank"> Github </el-link>
</el-descriptions-item>
<el-descriptions-item label="文档地址" label-align="left">
<el-link type="primary" href="https://docs.spicyboy.cn" target="_blank"> 文档地址 </el-link>
</el-descriptions-item>
<el-descriptions-item label="预览地址" label-align="left">
<el-link type="primary" href="https://admin.spicyboy.cn" target="_blank"> 预览地址 </el-link>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="card mb10">
<h4 class="title">生产环境依赖</h4>
<el-descriptions :column="3" border>
<el-descriptions-item v-for="(value, key) in dependencies" :key="key" width="400px" :label="key">
<el-tag type="info">
{{ value }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="card">
<h4 class="title">开发环境依赖</h4>
<el-descriptions :column="3" border>
<el-descriptions-item v-for="(value, key) in devDependencies" :key="key" width="400px" :label="key">
<el-tag type="info">
{{ value }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</template>
<script setup lang="ts" name="about">
const { pkg, lastBuildTime } = __APP_INFO__;
const { dependencies, devDependencies, version } = pkg;
</script>
<style lang="scss" scoped>
.card {
.title {
margin: 0 0 15px;
font-size: 17px;
font-weight: bold;
color: var(--el-text-color-primary);
}
.text {
font-size: 15px;
line-height: 25px;
color: var(--el-text-color-regular);
.el-link {
font-size: 15px;
}
}
}
</style>
+126
View File
@@ -0,0 +1,126 @@
<!--
* @Description: 测试管理表单页面
* @Author: superAdmin
* @Date: 2025-03-20 14:12:55
!-->
<template>
<form-container v-model="visible" :title="`${testPros.opt}测试`" form-size="600px">
<el-form
ref="testFormRef"
:rules="rules"
:disabled="testPros.disabled"
:model="testPros.record"
:hide-required-asterisk="testPros.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="姓名" prop="name">
<s-input v-model="testPros.record.name"></s-input>
</s-form-item>
<s-form-item label="性别" prop="sex">
<s-radio-group v-model="testPros.record.sex" :options="sexOptions" button />
</s-form-item>
<s-form-item label="民族" prop="nation">
<s-select v-model="testPros.record.nation" :options="nationOptions"></s-select>
</s-form-item>
<s-form-item label="年龄" prop="age">
<el-input-number v-model="testPros.record.age" :min="1" :max="100" />
</s-form-item>
<s-form-item label="生日" prop="bir">
<date-picker v-model="testPros.record.bir"></date-picker>
</s-form-item>
<s-form-item label="存款" prop="money">
<s-input v-model="testPros.record.money"></s-input>
</s-form-item>
<s-form-item label="排序码" prop="sortCode">
<s-input v-model="testPros.record.sortCode"></s-input>
</s-form-item>
<s-form-item label="状态" prop="status">
<s-select v-model="testPros.record.status" :options="statusOptions"></s-select>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!testPros.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { genTestApi, GenTest } from "@/api";
import { FormOptEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore();
const sexOptions = dictStore.getDictList("GENDER"); //性别选项
const nationOptions = dictStore.getDictList("NATION"); //民族选项
const statusOptions = dictStore.getDictList("COMMON_STATUS"); //状态选项
// 表单参数
const testPros = reactive<FormProps.Base<GenTest.GenTestInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
const rules = reactive({
name: [required("请输入姓名")],
sex: [required("请选择性别")],
nation: [required("请选择民族")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<GenTest.GenTestInfo>) {
Object.assign(testPros, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
testPros.record.sex = sexOptions[0].value;
testPros.record.nation = nationOptions[0].value;
testPros.record.age = 0;
//如果是新增,设置默认值
testPros.record.sortCode = 99;
testPros.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
genTestApi.detail({ id: props.record.id }).then(res => {
testPros.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const testFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
testFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await genTestApi
.submitForm(testPros.record, testPros.record.id != undefined)
.then(() => {
testPros.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
+90
View File
@@ -0,0 +1,90 @@
<!--
* @Description: 测试管理
* @Author: superAdmin
* @Date: 2025-03-20 14:12:55
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="测试列表" :columns="columns" :request-api="genTestApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="测试" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="测试"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选测试')"
/>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], '删除所选测试')" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</template>
<script setup lang="ts" name="test">
import { genTestApi, GenTest } from "@/api";
import { FormOptEnum } from "@/enums";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import Form from "./components/form.vue";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const dictStore = useDictStore();
const sexOptions = dictStore.getDictList("GENDER"); //性别选项
const nationOptions = dictStore.getDictList("NATION"); //民族选项
const statusOptions = dictStore.getDictList("COMMON_STATUS"); //状态选项
const columns: ColumnProps<GenTest.GenTestInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "name", label: "姓名", search: { el: "input" } },
{ prop: "sex", label: "性别", enum: sexOptions, search: { el: "select" } },
{ prop: "nation", label: "民族", enum: nationOptions, search: { el: "select" } },
{ prop: "age", label: "年龄" },
{ prop: "bir", label: "生日" },
{ prop: "money", label: "存款" },
{ prop: "sortCode", label: "排序码" },
{ prop: "status", label: "状态", enum: statusOptions },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | GenTest.GenTestInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(genTestApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,104 @@
<!--
* @Description: 复制
* @Author: huguodong
* @Date: 2023-12-15 15:45:25
!-->
<template>
<form-container v-model="visible" title="复制机构" form-size="600px">
<el-form ref="orgFormRef" :rules="rules" :model="copyProps" label-width="auto">
<s-form-item label="目标机构" prop="targetId">
<org-selector v-model:org-value="copyProps.targetId" :org-tree-api="bizOrgApi.tree" :show-all="false" />
</s-form-item>
<s-form-item label="包含子集" prop="containsChild" :tooltip="{ content: '将组织下的子机构也复制一份到目标组织' }">
<s-radio-group v-model="copyProps.containsChild" :options="yesOptions" button />
</s-form-item>
<s-form-item label="包含职位" prop="containsPosition" :tooltip="{ content: '将组织下的岗位也复制一份到目标组织' }">
<s-radio-group v-model="copyProps.containsPosition" :options="yesOptions" button />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { bizOrgApi } from "@/api";
import { SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 是否选项
const yesOptions = dictStore.getDictList(SysDictEnum.YES_NO);
// 表单参数
interface copyProps {
/** 目标组织id */
targetId: string | number;
/** 选择的组织id */
ids: string[] | number[];
/** 是否包含子集 */
containsChild: boolean;
/** 是否包含职位 */
containsPosition: boolean;
/** 成功后的回调 */
successful?: () => void;
}
// 表单参数
const copyProps = reactive<copyProps>({
targetId: "",
ids: [],
containsChild: true,
containsPosition: true
});
// 表单验证规则
const rules = reactive({
targetId: [required("请选择目标机构")],
containsChild: [required("请选择是否包含子集")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(ids: string[] | number[], successful: () => void) {
visible.value = true; //显示表单
copyProps.ids = ids; //设置机构id
copyProps.successful = successful;
}
// 提交数据(新增/编辑)
const orgFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
orgFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await bizOrgApi
.copy(copyProps)
.then(() => {
copyProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,157 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:28
!-->
<template>
<div>
<form-container v-model="visible" :title="`${orgProps.opt}机构`" form-size="600px">
<el-form
ref="orgFormRef"
:rules="rules"
:disabled="orgProps.disabled"
:model="orgProps.record"
:hide-required-asterisk="orgProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="上级机构" prop="parentId">
<org-selector v-model:org-value="orgProps.record.parentId!" :org-tree-api="bizOrgApi.tree" :show-all="false" />
</s-form-item>
<s-form-item label="机构名称" prop="name">
<s-input v-model="orgProps.record.name"></s-input>
</s-form-item>
<s-form-item label="机构编码" prop="code">
<s-input v-model="orgProps.record.code" placeholder="请填写机构编码,不填则为随机值" clearable></s-input>
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="orgProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="指定主管" prop="sortCode">
<el-button link type="primary" @click="showSelector">选择</el-button>
<el-tag v-if="orgProps.record.directorId" class="ml-3px" type="warning" closable @close="removeDirector">{{
orgProps.record.directorInfo?.name
}}</el-tag>
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="orgProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!orgProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
<user-selector
ref="userSelectorRef"
biz
:org-tree-api="bizOrgApi.tree"
:position-tree-api="bizPositionApi.tree"
:role-tree-api="bizRoleApi.tree"
:user-selector-api="bizUserApi.selector"
@successful="handleChooseUser"
/>
</div>
</template>
<script setup lang="ts">
import { SysOrg, SysUser, bizOrgApi, bizRoleApi, bizPositionApi, bizUserApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const orgProps = reactive<FormProps.Base<SysOrg.SysOrgInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
name: [required("请输入机构名称")],
parentId: [required("请选择上级机构")],
category: [required("请选择机构类型")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysOrg.SysOrgInfo>) {
Object.assign(orgProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
orgProps.record.sortCode = 99;
orgProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
bizOrgApi.detail({ id: props.record.id }).then(res => {
orgProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const orgFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
orgFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await bizOrgApi
.submitForm(orgProps.record, orgProps.record.id != undefined)
.then(() => {
orgProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
const userSelectorRef = ref<UserSelectorInstance>(); //用户选择器引用
/** 显示用户选择器 */
function showSelector() {
//将orgProps.record.directorInfo转为 SysUser.SysUserInfo[]类型
const directorInfo = orgProps.record.directorInfo ? [orgProps.record.directorInfo] : [];
userSelectorRef.value?.showSelector(directorInfo);
}
/** 选择用户 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
// 选择用户后,将用户id赋值给orgProps.record.directorId
if (data.length > 0) {
orgProps.record.directorId = data[0].id;
orgProps.record.directorInfo = data[0];
}
}
/** 移除主管 */
function removeDirector() {
orgProps.record.directorId = null;
orgProps.record.directorInfo = null;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+162
View File
@@ -0,0 +1,162 @@
<!--
* @Description: 机构管理
* @Author: huguodong
* @Date: 2023-12-15 15:45:32
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="机构列表"
:show-all="false"
:request-api="bizOrgApi.tree"
:default-value="initParam.parentId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" title="机构列表" :columns="columns" :request-api="bizOrgApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button v-auth="bizOrgButtonCode.add" suffix="机构" @click="onOpen(FormOptEnum.ADD)" />
<s-button v-auth="bizOrgButtonCode.copy" :disabled="!scope.isSelected" :icon="CopyDocument" @click="onOpenCopy(scope.selectedListIds)">
复制机构
</s-button>
<s-button
type="danger"
v-auth="bizOrgButtonCode.batchDelete"
plain
suffix="机构"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选机构')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button v-auth="bizOrgButtonCode.edit" link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button
v-auth="bizOrgButtonCode.delete"
link
:opt="FormOptEnum.DELETE"
@click="onDelete([scope.row.id], `删除【${scope.row.name}】机构`)"
/>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 复制表单 -->
<Copy ref="copyRef" />
</div>
</div>
</template>
<script setup lang="ts" name="sysOrg">
import { bizOrgApi, SysOrg, bizOrgButtonCode } from "@/api";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { CopyDocument } from "@element-plus/icons-vue";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form.vue";
import Copy from "./components/copy.vue";
const dictStore = useDictStore(); //字典仓库
// 机构类型选项
const orgCategoryOptions = dictStore.getDictList(SysDictEnum.ORG_CATEGORY);
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
interface InitParam {
parentId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ parentId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysOrg.SysOrgInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "name", label: "机构名称", search: { el: "input" } },
{ prop: "names", label: "机构全称" },
{
prop: "category",
label: "分类",
enum: orgCategoryOptions,
search: { el: "select" }
},
{ prop: "code", label: "编码", search: { el: "input" } },
{ prop: "status", label: "状态", enum: statusOptions, search: { el: "select" } },
{ prop: "sortCode", label: "排序" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 150, fixed: "right" }
];
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.parentId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.parentId = 0;
}
}
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysOrg.SysOrgInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
// 表单引用
const copyRef = ref<InstanceType<typeof Copy> | null>(null);
/**
* 打开复制表单
* @param opt 操作类型
* @param record 记录
*/
function onOpenCopy(ids: string[] | number[]) {
copyRef.value?.onOpen(ids, RefreshTable);
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(bizOrgApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,122 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:37
!-->
<template>
<div>
<form-container v-model="visible" :title="`${positionProps.opt}岗位`" form-size="600px">
<el-form
ref="positionFormRef"
:rules="rules"
:disabled="positionProps.disabled"
:model="positionProps.record"
:hide-required-asterisk="positionProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="所属组织" prop="orgId">
<org-selector v-model:org-value="positionProps.record.orgId!" :org-tree-api="bizOrgApi.tree" :show-all="false" />
</s-form-item>
<s-form-item label="岗位名称" prop="name">
<s-input v-model="positionProps.record.name"></s-input>
</s-form-item>
<s-form-item label="岗位分类" prop="category">
<s-radio-group v-model="positionProps.record.category" :options="posCategoryOptions" button />
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="positionProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="positionProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!positionProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>
<script setup lang="ts">
import { bizOrgApi, SysPosition, bizPositionApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 岗位类型选项
const posCategoryOptions = dictStore.getDictList(SysDictEnum.POSITION_CATEGORY);
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const positionProps = reactive<FormProps.Base<SysPosition.SysPositionInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
parentId: [required("请选择所属组织")],
name: [required("请输入岗位名称")],
orgId: [required("请选择所属岗位")],
category: [required("请选择岗位类型")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysPosition.SysPositionInfo>) {
Object.assign(positionProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
positionProps.record.sortCode = 99;
positionProps.record.category = posCategoryOptions[0].value;
positionProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
bizPositionApi.detail({ id: props.record.id }).then(res => {
positionProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const positionFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
positionFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await bizPositionApi
.submitForm(positionProps.record, positionProps.record.id != undefined)
.then(() => {
positionProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,141 @@
<!--
* @Description: 岗位管理
* @Author: huguodong
* @Date: 2023-12-15 15:45:43
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="机构列表"
:show-all="false"
:request-api="bizOrgApi.tree"
:default-value="initParam.orgId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" title="岗位列表" :columns="columns" :request-api="bizPositionApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button v-auth="bizPositionButtonCode.add" suffix="岗位" @click="onOpen(FormOptEnum.ADD)" />
<s-button
v-auth="bizPositionButtonCode.batchDelete"
type="danger"
plain
suffix="岗位"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选岗位')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button v-auth="bizPositionButtonCode.edit" link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button
v-auth="bizPositionButtonCode.delete"
link
:opt="FormOptEnum.DELETE"
@click="onDelete([scope.row.id], `删除【${scope.row.name}】岗位`)"
/>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</div>
</template>
<script setup lang="ts" name="sysPosition">
import { bizOrgApi, bizPositionApi, SysPosition, bizPositionButtonCode } from "@/api";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form.vue";
const dictStore = useDictStore(); //字典仓库
// 岗位类型选项
const posCategoryOptions = dictStore.getDictList(SysDictEnum.POSITION_CATEGORY);
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
interface InitParam {
orgId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ orgId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysPosition.SysPositionInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "name", label: "岗位名称", search: { el: "input" } },
{
prop: "category",
label: "岗位分类",
enum: posCategoryOptions,
search: { el: "select" }
},
{ prop: "code", label: "岗位编码", search: { el: "input" } },
{ prop: "status", label: "状态", enum: statusOptions, search: { el: "select" } },
{ prop: "sortCode", label: "排序" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 150, fixed: "right" }
];
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.orgId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.orgId = 0;
}
}
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysPosition.SysPositionInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(bizPositionApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,118 @@
<!--
* @Description: 数据范围选择器
* @Author: huguodong
* @Date: 2023-12-15 15:43:27
!-->
<template>
<div>
<el-space>
<s-radio-group v-model="defaultLevel" :options="scopeOptions" value-key="level" label-key="title" @change="changeDataScope">
<template #radio="{ item }">
<el-badge
v-if="
dataScope.level === defineScope?.level && item.scopeCategory === SysRole.DataScopeEnum.SCOPE_ORG_DEFINE && item.scopeDefineOrgIdList
"
:value="dataScope.scopeDefineOrgIdList.length"
type="success"
>
<span class="mb-5">{{ item.title }}</span>
</el-badge>
<span v-else>{{ item.title }}</span>
</template>
</s-radio-group>
<div v-if="dataScope.level === defineScope?.level">
<el-button link type="primary" class="ml-15px" @click="handleDefineOrg">选择机构</el-button>
</div>
</el-space>
<el-dialog v-model="dialogVisible" append-to-body title="选择机构" width="400" destroy-on-close>
<TreeFilter
title="机构列表"
multiple
check-strictly
class="treeFilter"
label="name"
:request-api="bizOrgApi.tree"
:default-value="dataScope.scopeDefineOrgIdList"
@change="changeTreeFilter"
/>
<template #footer>
<el-button @click="dialogCancel">取消</el-button>
<el-button type="primary" @click="chooseDefineOrg"> 确定 </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { SysRole, bizOrgApi } from "@/api";
const dialogVisible = ref(false); //是否显示弹窗
// props
interface DataScopeProps {
modelValue: SysRole.DefaultDataScope | undefined;
}
const emit = defineEmits(["update:modelValue"]); //定义emit
const props = defineProps<DataScopeProps>(); //定义props
// 用户信息
const dataScope = computed({
get: () => props.modelValue ?? scopeOptions[0], // 判断是否有值,没有值则取第一个
set: val => emit("update:modelValue", val)
});
// 数据范围列表,去掉全部
const scopeOptions = SysRole.dataScopeOptions.filter(item => item.scopeCategory !== SysRole.DataScopeEnum.SCOPE_ALL);
// 默认数据范围
const defaultLevel = ref(dataScope.value.level);
// 自定义数据范围
const defineScope = SysRole.dataScopeOptions.find(item => item.scopeCategory === SysRole.DataScopeEnum.SCOPE_ORG_DEFINE);
// 自定义机构id列表
const defineOrgIdList = ref<number[] | string[]>(dataScope.value.scopeDefineOrgIdList);
/** 修改数据权限 */
function changeDataScope(value: any) {
let scope = SysRole.dataScopeOptions.find(item => item.level === value);
if (scope) {
dataScope.value = scope;
}
}
// /** 处理自定义 */
function handleDefineOrg() {
dialogVisible.value = true;
console.log("[ dataScope.value ] >", dataScope.value);
}
/** 机构树数据变化 */
function changeTreeFilter(val: any[]) {
defineOrgIdList.value = val;
}
/** 取消选择机构 */
function dialogCancel() {
// 关闭弹窗
dialogVisible.value = false;
}
/** 选择机构 */
function chooseDefineOrg() {
// 设置默认数据权限中的机构ID列表
// 关闭弹窗
dialogVisible.value = false;
dataScope.value.scopeDefineOrgIdList = defineOrgIdList.value;
console.log("[ dataScope.value ] >", dataScope.value);
}
</script>
<style lang="scss" scoped>
.treeFilter {
width: 100% !important;
}
:deep(.el-badge__content.is-fixed) {
transform: translateY(-30%) translateX(100%);
}
</style>
@@ -0,0 +1,118 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:43:34
!-->
<template>
<form-container v-model="visible" :title="`${sysRoleProps.opt}角色`" form-size="800px" @close="onClose">
<el-form
ref="sysRoleFormRef"
:rules="rules"
:disabled="sysRoleProps.disabled"
:model="sysRoleProps.record"
:hide-required-asterisk="sysRoleProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="角色名称" prop="name">
<s-input v-model="sysRoleProps.record.name"></s-input>
</s-form-item>
<s-form-item label="所属组织" prop="orgId">
<org-selector v-model:org-value="sysRoleProps.record.orgId!" :org-tree-api="bizOrgApi.tree" :show-all="false" />
</s-form-item>
<s-form-item label="默认数据范围" prop="code">
<DataScopeSelector v-model="sysRoleProps.record.defaultDataScope"></DataScopeSelector>
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="sysRoleProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="sysRoleProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!sysRoleProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { SysRole, bizRoleApi, bizOrgApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import DataScopeSelector from "./dataScopeSelector.vue";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS); // 通用状态选项
// 表单参数
const sysRoleProps = reactive<FormProps.Base<SysRole.SysRoleInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
name: [required("请输入角色名称")],
orgId: [required("请选择所属组织")],
category: [required("请选择角色类型")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysRole.SysRoleInfo>) {
Object.assign(sysRoleProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
sysRoleProps.record.sortCode = 99;
sysRoleProps.record.status = statusOptions[0].value;
sysRoleProps.record.defaultDataScope = SysRole.dataScopeOptions[0];
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
bizRoleApi.detail({ id: props.record.id }).then(res => {
sysRoleProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const sysRoleFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
sysRoleFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await bizRoleApi
.submitForm(sysRoleProps.record, sysRoleProps.record.id != undefined)
.then(() => {
sysRoleProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,414 @@
<!--
* @Description: 授权资源表单
* @Author: huguodong
* @Date: 2023-12-15 15:43:49
!-->
<template>
<div>
<form-container v-model="visible" :title="`授权资源->${grantProps.name}`" form-size="1200px" @close="onClose">
<el-alert title="非超管角色不可被授权系统模块菜单资源" type="warning" show-icon style="margin-bottom: 10px" />
<ProTable
ref="proTable"
:columns="columns"
:tool-button="false"
:cell-style="{ padding: '0px' }"
:row-style="{ height: '0' }"
:data="grantProps.tableData"
:pagination="false"
:span-method="objectSpanMethod"
>
<!-- 表格 header 按钮 -->
<template #tableHeader>
<s-radio-group
v-model="grantProps.moduleId"
:options="grantProps.echoDatalist"
value-key="id"
label-key="title"
button
class="mb-2"
@change="moduleClick"
>
<template #radio="{ item }">
<el-space>
<svg-icon :icon="item.icon" class="h-4 w-4"></svg-icon>
{{ item.title }}
</el-space>
</template>
</s-radio-group>
</template>
<!-- 一级目录 -->
<template #parentName="scope">
<el-checkbox v-model="scope.row.parentCheck" @change="val => changeParent(scope.row, val)">
{{ scope.row.parentName }}
</el-checkbox>
</template>
<!-- 菜单 -->
<template #title="scope">
<el-checkbox v-model="scope.row.nameCheck" @change="val => changeSub(scope.row, val)">
{{ scope.row.title }}
</el-checkbox>
</template>
<!-- 按钮授权 -->
<template #button="scope">
<template v-if="scope.row.button.length > 0">
<template v-for="(item, index) in scope.row.button" :key="item.id">
<el-checkbox v-model="item.check" style="margin-right: 5px" @change="val => changeChildCheckBox(scope.row, val)">
{{ item.title }}
</el-checkbox>
<br v-if="(index + 1) % 5 === 0" />
</template>
</template>
</template>
</ProTable>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
<el-dialog v-model="dialogVisible" title="请选择数据范围" width="30%" destroy-on-close>
<DataScopeSelector v-model="grantProps.defaultDataScope"></DataScopeSelector>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUserSubmit"> 确定 </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { SysRole, SysUser, bizRoleApi, bizUserApi } from "@/api";
import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import type { TableColumnCtx } from "element-plus";
import DataScopeSelector from "./dataScopeSelector.vue";
const visible = ref(false); //授权资源表单是否显示
const dialogVisible = ref(false); // 数据范围弹窗显示
/** 授权资源表单参数接口 */
interface GrantResourceFormProps {
/** id */
id: number | string;
/** 名称 */
name: string;
/** 模块id */
moduleId: number | string;
/** 资源树 */
resTree: SysRole.ResTreeSelector[];
/** 已拥有的资源 */
ownResource: SysRole.RoleOwnResource | null;
/** 一级菜单索引 */
firstShowMap: Record<string, number[]>;
/** 回显数据列表 */
echoDatalist: SysRole.ResTreeSelector[];
/** 表格数据 */
tableData: SysRole.RoleGrantResourceMenu[];
//默认数据范围,用户才有
defaultDataScope: SysRole.DefaultDataScope;
}
/** 授权资源表单参数 */
const grantProps = reactive<GrantResourceFormProps>({
id: 0,
name: "",
moduleId: 0,
resTree: [],
ownResource: null,
firstShowMap: {},
echoDatalist: [],
tableData: [],
defaultDataScope: SysRole.dataScopeOptions[0]
});
/** props */
const props = defineProps<{
/** 是否是用户 */
user?: boolean;
}>();
// 获取 ProTable DOM
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysRole.ResTreeSelector>[] = [
// { type: "selection", width: 80 },
{ prop: "parentName", label: "一级目录", width: 150, align: "left" },
{ prop: "title", label: "菜单", width: 180, align: "left" },
{ prop: "button", label: "按钮授权", align: "left" }
];
/** 列合并 */
interface SpanMethodProps {
// 行
row: SysRole.RoleGrantResourceMenu;
// 列
column: TableColumnCtx<SysRole.RoleGrantResourceMenu>;
// 行索引
rowIndex: number;
// 列索引
columnIndex: number;
}
/** 列合并*/
const objectSpanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps) => {
// 如果列索引为0
if (columnIndex == 0) {
// 获取父名称
const parentName = row.parentName;
// 获取父名称的索引数组
const indexArr = grantProps.firstShowMap[parentName];
// 如果行索引等于索引数组的第一个
if (rowIndex === indexArr[0]) {
return { rowspan: indexArr.length, colspan: 1 };
} else {
return { rowspan: 0, colspan: 0 };
}
}
};
/**
* 打开表单
* @param props 表单参数
*/
async function onOpen(props: FormProps.Base<SysRole.SysRoleInfo | SysUser.SysUserInfo>) {
grantProps.id = props.record.id || 0;
grantProps.name = props.record.name || "";
await loadData();
visible.value = true;
}
/** 加载数据 */
async function loadData() {
if (grantProps.echoDatalist.length > 0) {
let data = grantProps.echoDatalist.find(f => f.id === grantProps.moduleId)!.menu;
grantProps.tableData = data;
} else {
// 获取角色资源树
await bizRoleApi.resourceTreeSelector().then(res => {
grantProps.resTree = res.data;
grantProps.moduleId = res.data[0].id;
});
await getResources(); // 获取资源
// 如果有已拥有的资源
if (grantProps.ownResource) {
// 回显模块数据
// 获取echoDatalist
grantProps.echoDatalist = echoModuleData(grantProps.resTree, grantProps.ownResource);
// 获取模块id
grantProps.moduleId = grantProps.resTree[0].id;
// 获取表格数据
grantProps.tableData = grantProps.echoDatalist[0].menu;
}
}
}
/** 获取资源 */
async function getResources() {
if (props.user) {
// 获取用户拥有资源
await bizUserApi.ownResource({ id: grantProps.id }).then(res => {
grantProps.ownResource = res.data;
});
} else {
// 获取角色拥有资源
await bizRoleApi.ownResource({ id: grantProps.id }).then(res => {
grantProps.ownResource = res.data;
});
}
}
/**
* 回显模块数据
* @param resTree 资源树
* @param ownResource 已拥有的资源
*/
function echoModuleData(resTree: SysRole.ResTreeSelector[], ownResource: SysRole.RoleOwnResource) {
// 通过应用循环
resTree.forEach(module => {
//如果有菜单
if (module.menu) {
module.menu.forEach(menu => {
const menuCheck = ref(0);
// 如果已授权资源信息列表有数据
if (ownResource.grantInfoList.length > 0) {
// 通过已授权资源信息列表循环
ownResource.grantInfoList.forEach(grantInfo => {
// 如果已授权资源信息列表中的菜单id等于当前菜单id
if (menu.id == grantInfo.menuId) {
menuCheck.value++;
// 处理按钮
if (grantInfo.buttonInfo.length > 0) {
// 通过已授权资源信息列表中的按钮循环
grantInfo.buttonInfo.forEach(button => {
menu.button.forEach(itemButton => {
// 如果已授权资源信息列表中的按钮id等于当前按钮id
if (button === itemButton.id) {
itemButton.check = true;
}
});
});
}
}
});
}
// 回显前面的2个
if (menuCheck.value > 0) {
menu.parentCheck = true;
menu.nameCheck = true;
}
});
// 缓存加入索引
module.menu.forEach((item: SysRole.RoleGrantResourceMenu, index) => {
// 下面就是用来知道不同的一级菜单里面有几个二级菜单,以及他们所在的索引
if (grantProps.firstShowMap[item.parentName]) {
grantProps.firstShowMap[item.parentName].push(index);
} else {
grantProps.firstShowMap[item.parentName] = [index];
}
});
}
});
return resTree;
}
/** 选择模块 */
function moduleClick() {
loadData();
}
/** 一级目录点击 */
function changeParent(row: SysRole.RoleGrantResourceMenu, val: any) {
row.parentCheck = val;
// 通过这个应用id,找到应用下的所有菜单
const moduleMenu = grantProps.echoDatalist.find(f => row.module === f.id);
const parentName = row.parentName;
// 获取同一级菜单的所有索引
const indexArr = grantProps.firstShowMap[parentName];
indexArr.forEach(indexItem => {
// 获取同一级菜单的所有行
const row = moduleMenu?.menu[indexItem];
if (row)
// 给这些菜单的索引去勾选
changeSub(row, val);
});
}
/** 二级菜单的点击 */
function changeSub(row: SysRole.RoleGrantResourceMenu, val: any) {
// 选中二级菜单
row.nameCheck = val;
row.button.forEach(item => {
// 选中按钮
item.check = val;
});
}
/** 权限按钮点击 */
function changeChildCheckBox(row: SysRole.RoleGrantResourceMenu, val: any) {
// 如果val为false,并且row的子菜单没有选中,则取消所有按钮的选中
if (!val && checkAllChildNotChecked(row)) {
// 这里注释掉勾选去掉所有按钮,联动去掉菜单
/*record.nameCheck = false
record.parentCheck = false*/
} else if (val) {
// 如果val为true,则将row的nameCheck和parentCheck属性设置为true
row.nameCheck = val;
row.parentCheck = val;
}
}
/** 检查所有子级是否都没有勾选 */
function checkAllChildNotChecked(record: SysRole.RoleGrantResourceMenu) {
// 获取记录的子节点
const child = record.button;
// 判断子节点是否被选中,如果有一个子节点未被选中,则返回false
return child.every(field => !field.check);
}
/** 提交之前转换数据 */
function convertData() {
const params: SysRole.GrantResourceReq = {
id: grantProps.id,
grantInfoList: []
};
// 遍历echoDatalist,获取每一项
grantProps.echoDatalist.forEach(data => {
// 如果data有menu属性
if (data.menu) {
// 遍历data.menu,获取每一项
data.menu.forEach(item => {
// 初始化grantInfo
const grantInfo: SysRole.RelationRoleResource = {
menuId: 0,
buttonInfo: []
};
// 如果被选中了
if (item.nameCheck) {
// 获取item的id
grantInfo.menuId = item.id;
// 遍历item.button,获取每一项
item.button.forEach(button => {
// 如果button被选中了
if (button.check) {
// 将button的id添加到grantInfo.buttonInfo中
grantInfo.buttonInfo.push(button.id as never);
}
});
// 将grantInfo添加到params.grantInfoList中
params.grantInfoList.push(grantInfo);
}
});
}
});
// 返回params
return params;
}
/** 提交 */
function handleSubmit() {
if (props.user) {
dialogVisible.value = true;
} else {
let params = convertData();
grantResource(params);
}
}
/** 用户授权资源提交 */
const handleUserSubmit = () => {
const param = convertData();
param.defaultDataScope = grantProps.defaultDataScope;
grantResource(param);
dialogVisible.value = false;
};
/** 授权资源 */
function grantResource(params: SysRole.GrantResourceReq) {
// 根据用户判断走哪个方法
if (props.user) {
bizUserApi.grantResource(params).then(() => {
onClose();
});
} else {
bizRoleApi.grantResource(params).then(() => {
onClose();
});
}
}
/** 关闭表单 */
function onClose() {
visible.value = false;
grantProps.echoDatalist = [];
grantProps.moduleId = 0;
grantProps.firstShowMap = {};
grantProps.tableData = [];
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+222
View File
@@ -0,0 +1,222 @@
<!--
* @Description: 角色管理
* @Author: huguodong
* @Date: 2023-12-15 15:43:53
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="组织列表"
:show-all="false"
:request-api="bizOrgApi.tree"
:default-value="initParam.orgId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" :columns="columns" :request-api="bizRoleApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button v-auth="bizRoleButtonCode.add" suffix="角色" @click="onOpen(FormOptEnum.ADD)" />
<s-button
v-auth="bizRoleButtonCode.batchDelete"
type="danger"
plain
suffix="角色"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选角色')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<el-space>
<s-button v-auth="bizRoleButtonCode.edit" link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button
v-auth="bizRoleButtonCode.delete"
link
:opt="FormOptEnum.DELETE"
@click="onDelete([scope.row.id], `删除【${scope.row.name}】角色`)"
/>
<el-dropdown @command="handleCommand">
<el-link type="primary" :underline="false" :icon="ArrowDown"> 授权 </el-link>
<template #dropdown>
<el-dropdown-menu>
<div v-auth="bizRoleButtonCode.grantResource">
<el-dropdown-item :command="command(scope.row, cmdEnum.GrantResource)">{{ cmdEnum.GrantResource }}</el-dropdown-item>
</div>
<div v-auth="bizRoleButtonCode.grantUser">
<el-dropdown-item :command="command(scope.row, cmdEnum.GrantUser)">{{ cmdEnum.GrantUser }}</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 授权资源组件 -->
<GrantResource ref="grantResourceRef" />
<!-- 用户选择器 -->
<user-selector
ref="userSelectorRef"
biz
:org-tree-api="bizOrgApi.tree"
:position-tree-api="bizPositionApi.tree"
:role-tree-api="bizRoleApi.tree"
:user-selector-api="bizUserApi.selector"
multiple
@successful="handleChooseUser"
/>
</div>
</div>
</template>
<script setup lang="ts" name="sysRole">
import { bizOrgApi, bizRoleApi, SysRole, SysUser, bizUserApi, bizPositionApi, bizRoleButtonCode } from "@/api";
import { ArrowDown } from "@element-plus/icons-vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form.vue";
import GrantResource from "./components/grantResource.vue";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";
const dictStore = useDictStore(); //字典仓库
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 组织列表组件
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
interface InitParam {
orgId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ orgId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysRole.SysRoleInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "name", label: "角色名称", search: { el: "input" } },
{ prop: "code", label: "角色编码", search: { el: "input" } },
{ prop: "status", label: "状态", enum: statusOptions, search: { el: "select" } },
{ prop: "sortCode", label: "排序" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysRole.SysRoleInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(bizRoleApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.orgId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.orgId = 0;
}
}
/** 更多下拉菜单命令枚举 */
enum cmdEnum {
GrantResource = "授权资源",
GrantUser = "授权用户"
}
/** 下拉菜单参数接口 */
interface Command {
row: SysRole.SysRoleInfo;
command: cmdEnum;
}
/**配置command的参数 */
function command(row: SysRole.SysRoleInfo, command: cmdEnum): Command {
return {
row: row,
command: command
};
}
const grantResourceRef = ref<InstanceType<typeof GrantResource> | null>(null); //授权资源组件引用
const userSelectorRef = ref<UserSelectorInstance>(); //用户选择器引用
const roleId = ref<number | string>(0); //角色id
/**
* 更多下拉菜单点击事件
* @param command
*/
function handleCommand(command: Command) {
switch (command.command) {
case cmdEnum.GrantUser:
roleId.value = command.row.id; //获取角色id
bizRoleApi.ownUser({ id: command.row.id }).then(res => {
userSelectorRef.value?.showSelector(res.data); //显示用户选择器
});
break;
case cmdEnum.GrantResource:
// 打开授权资源组件
grantResourceRef.value?.onOpen({
opt: FormOptEnum.EDIT,
record: command.row,
successful: RefreshTable
});
break;
}
}
/** 选择用户 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
//组装参数
const grantUser: SysRole.GrantUserReq = {
id: roleId.value,
grantInfoList: data.map(item => item.id) as number[] | string[]
};
bizRoleApi.grantUser(grantUser);
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,169 @@
<!--
* @Description: 基本信息
* @Author: huguodong
* @Date: 2023-12-15 15:45:50
!-->
<template>
<div>
<el-tab-pane label="基础信息" name="basic">
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="账号" prop="account">
<s-input v-model="userInfo.account"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="姓名" prop="name">
<s-input v-model="userInfo.name"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="性别" prop="gender">
<s-radio-group v-model="userInfo.gender" :options="genderOptions" />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="昵称" prop="nickname">
<s-input v-model="userInfo.nickname"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="手机号" prop="phone">
<s-input v-model="userInfo.phone"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="邮箱" prop="email">
<s-input v-model="userInfo.email"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="出生日期" prop="birthday">
<date-picker v-model="userInfo.birthday"></date-picker>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="入职日期" prop="entryDate">
<date-picker v-model="userInfo.entryDate"></date-picker>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="员工编号" prop="empNo">
<s-input v-model="userInfo.empNo"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="职级" prop="positionLevel">
<s-input v-model="userInfo.positionLevel" clearable></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="部门和职位" prop="positionId">
<position-selector v-model="userInfo.orgAndPosIdList" @change="handleChange" :selector-api="bizPositionApi.selector" />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="主管" prop="directorId">
<el-button link type="primary" @click="showSelector">选择</el-button>
<el-tag v-if="userInfo.directorId" class="ml-3px" type="warning" closable @close="removeDirector">{{
userInfo.directorInfo?.name
}}</el-tag>
</s-form-item>
</el-col>
</el-row>
</el-tab-pane>
<user-selector
ref="userSelectorRef"
biz
:org-tree-api="bizOrgApi.tree"
:position-tree-api="bizPositionApi.tree"
:role-tree-api="bizRoleApi.tree"
:user-selector-api="bizUserApi.selector"
@successful="handleChooseUser"
/>
</div>
</template>
<script setup lang="ts">
import { SysUser, bizOrgApi, bizPositionApi, bizRoleApi, bizUserApi } from "@/api";
import { useDictStore } from "@/stores/modules";
import { SysDictEnum } from "@/enums";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";
const dictStore = useDictStore(); //字典仓库
// props
interface FormProps {
modelValue: Partial<SysUser.SysUserInfo>;
}
const emit = defineEmits(["update:modelValue"]); //定义emit
const props = defineProps<FormProps>(); //定义props
// 人员信息
const userInfo = computed({
get: () => props.modelValue,
set: val => emit("update:modelValue", val)
});
// 通用状态选项
const genderOptions = dictStore.getDictList(SysDictEnum.GENDER);
onMounted(() => {
// 初始化
userInfo.value.gender = userInfo.value.gender ? userInfo.value.gender : genderOptions[0].value;
});
/** 用于处理职位选择器改变值的情况 */
function handleChange(value: any) {
if (!value) {
return;
}
// 获取改变后的值中最后一位的值,即positionId
let positionId = value[value.length - 1];
// 获取改变后的值中最后两位的值,即orgId
let orgId = value[value.length - 2];
// 将orgId赋值给userInfo的value的orgId
userInfo.value.orgId = orgId;
// 将positionId赋值给userInfo的value的positionId
userInfo.value.positionId = positionId;
}
const userSelectorRef = ref<UserSelectorInstance>(); //人员选择器ref
/** 显示人员选择器 */
function showSelector() {
//将userInfo.value.directorInfo转为 SysUser.SysUserInfo[]类型
const directorInfo = userInfo.value.directorInfo ? [userInfo.value.directorInfo] : [];
userSelectorRef.value?.showSelector(directorInfo);
}
/** 选择人员 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
// 选择人员后,将人员id赋值给userInfo.value.directorId
if (data.length > 0) {
userInfo.value.directorId = data[0].id;
userInfo.value.directorInfo = data[0];
}
}
/** 移除主管 */
function removeDirector() {
userInfo.value.directorId = null;
userInfo.value.directorInfo = null;
}
</script>
<style lang="scss" scoped>
:deep(.el-input__wrapper) {
width: 100% !important;
}
:deep(.el-date-editor.el-input) {
width: 92% !important;
}
</style>
@@ -0,0 +1,145 @@
<!--
* @Description: 更多信息
* @Author: huguodong
* @Date: 2023-12-15 15:45:55
!-->
<template>
<el-tab-pane label="更多信息" name="more">
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="民族" prop="nation">
<s-select v-model="userInfo.nation" :options="nationOptions"></s-select>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="籍贯" prop="nativePlace">
<s-input v-model="userInfo.nativePlace"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="家庭住址" prop="homeAddress">
<s-input v-model="userInfo.homeAddress" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="通信地址" prop="mailingAddress">
<s-input v-model="userInfo.mailingAddress" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="证件类型" prop="nation">
<s-select v-model="userInfo.idCardType" :options="cardTypeOptions"></s-select>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="证件号码" prop="idCardNumber">
<s-input v-model="userInfo.idCardNumber"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="文化程度" prop="cultureLevel">
<s-select v-model="userInfo.cultureLevel" :options="cultureLevelOptions"></s-select>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="政治面貌" prop="politicalOutlook">
<s-input v-model="userInfo.politicalOutlook"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="毕业学校" prop="college">
<s-input v-model="userInfo.college"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="学历" prop="education">
<s-input v-model="userInfo.education"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="学制" prop="eduLength">
<s-input v-model="userInfo.eduLength"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="学位" prop="degree">
<s-input v-model="userInfo.degree"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="家庭电话" prop="homeTel">
<s-input v-model="userInfo.homeTel"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="办公电话" prop="officeTel">
<s-input v-model="userInfo.officeTel"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="紧急联系人" prop="emergencyContact">
<s-input v-model="userInfo.emergencyContact"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="紧急联系电话" prop="emergencyPhone">
<s-input v-model="userInfo.emergencyPhone"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="紧急联系人地址" prop="emergencyAddress">
<s-input v-model="userInfo.emergencyAddress"></s-input>
</s-form-item>
</el-col>
</el-row>
</el-tab-pane>
</template>
<script setup lang="ts">
import { SysUser } from "@/api";
import { useDictStore } from "@/stores/modules";
import { SysDictEnum } from "@/enums";
const dictStore = useDictStore(); //字典仓库
// props
interface FormProps {
modelValue: Partial<SysUser.SysUserInfo>;
}
const emit = defineEmits(["update:modelValue"]); //定义emit
const props = defineProps<FormProps>(); //定义props
// 人员信息
const userInfo = computed({
get: () => props.modelValue,
set: val => emit("update:modelValue", val)
});
// 通用状态选项
const nationOptions = dictStore.getDictList(SysDictEnum.NATION);
// 证件类型选项
const cardTypeOptions = dictStore.getDictList(SysDictEnum.IDCARD_TYPE);
//文化程度选项
const cultureLevelOptions = dictStore.getDictList(SysDictEnum.CULTURE_LEVEL);
onMounted(() => {
// 初始化默认值
// userInfo.value.nation = userInfo.value.nation ? userInfo.value.nation : nationOptions[0].value;
userInfo.value.idCardType = userInfo.value.idCardType ? userInfo.value.idCardType : cardTypeOptions[0].value;
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,111 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:59
!-->
<template>
<div>
<form-container v-model="visible" :title="`${sysUserProps.opt}人员`" form-size="800px" @close="onClose">
<el-form
ref="sysUserFormRef"
:rules="rules"
:disabled="sysUserProps.disabled"
:model="sysUserProps.record"
:hide-required-asterisk="sysUserProps.disabled"
label-width="auto"
label-suffix=" :"
>
<el-tabs v-model="activeName">
<Basic v-model="sysUserProps.record"></Basic>
<More v-model="sysUserProps.record"></More>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!sysUserProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>
<script setup lang="ts" name="sysUserForm">
import { SysUser, bizUserApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import Basic from "./form_basic.vue";
import More from "./form_more.vue";
const dictStore = useDictStore(); //字典仓库
const visible = ref(false); //是否显示表单
const activeName = ref("basic");
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const sysUserProps = reactive<FormProps.Base<SysUser.SysUserInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
account: [required("请输入账号")],
name: [required("请输入姓名")],
gender: [required("请选择性别")],
positionId: [required("请选择部门和职位")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysUser.SysUserInfo>) {
Object.assign(sysUserProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
sysUserProps.record.sortCode = 99;
sysUserProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
bizUserApi.detail({ id: props.record.id }).then(res => {
sysUserProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const sysUserFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
sysUserFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await bizUserApi
.submitForm(sysUserProps.record, sysUserProps.record.id != undefined)
.then(() => {
sysUserProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
activeName.value = "basic";
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+249
View File
@@ -0,0 +1,249 @@
<!--
* @Description: 人员管理
* @Author: huguodong
* @Date: 2023-12-15 15:46:03
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="机构列表"
:show-all="false"
:request-api="bizOrgApi.tree"
:default-value="initParam.orgId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" title="人员列表" :columns="columns" :request-api="bizUserApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button v-auth="bizUserButtonCode.add" suffix="人员" @click="onOpen(FormOptEnum.ADD)" />
<s-button
v-auth="bizUserButtonCode.batchDelete"
type="danger"
plain
suffix="人员"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选人员')"
/>
</template>
<!-- 状态 -->
<template #avatar="scope">
<el-avatar :src="scope.row.avatar" :size="30" />
<!-- <span>{{ scope.row.avatar }}}</span> -->
</template>
<!-- 状态 -->
<template #status="scope">
<el-switch :model-value="scope.row.status === CommonStatusEnum.ENABLE" :loading="switchLoading" @change="editStatus(scope.row)" />
</template>
<!-- 操作 -->
<template #operation="scope">
<el-space>
<s-button v-auth="bizUserButtonCode.edit" link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button
v-auth="bizUserButtonCode.delete"
link
:opt="FormOptEnum.DELETE"
@click="onDelete([scope.row.id], `删除【${scope.row.account}】人员`)"
/>
<el-dropdown @command="handleCommand">
<el-link type="primary" :underline="false" :icon="ArrowDown"> 更多 </el-link>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-auth="bizUserButtonCode.pwdReset" :command="command(scope.row, cmdEnum.ResetPassword)">{{
cmdEnum.ResetPassword
}}</el-dropdown-item>
<el-dropdown-item v-auth="bizUserButtonCode.grantRole" :command="command(scope.row, cmdEnum.GrantRole)">{{
cmdEnum.GrantRole
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 角色选择器组件 -->
<role-selector
ref="roleSelectorRef"
multiple
:org-tree-api="bizOrgApi.tree"
:role-selector-api="bizRoleApi.roleSelector"
@successful="handleChooseRole"
></role-selector>
<!-- 授权资源组件 -->
<GrantResource user ref="grantResourceRef" />
<!-- 授权权限组件 -->
<GrantPermission user ref="grantPermissionRef" />
</div>
</div>
</template>
<script setup lang="ts" name="SysUser">
import { bizOrgApi, bizUserApi, SysUser, SysRole, bizRoleApi, bizUserButtonCode } from "@/api";
import { ArrowDown } from "@element-plus/icons-vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form/index.vue";
import { ElMessage } from "element-plus";
import { RoleSelectorInstance } from "@/components/Selectors/RoleSelector/interface";
import GrantResource from "@/views/sys/limit/role/components/grantResource.vue";
import GrantPermission from "@/views/sys/limit/role/components/grantPermission.vue";
const dictStore = useDictStore(); //字典仓库
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
//状态开关loading
const switchLoading = ref(false);
interface InitParam {
orgId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ orgId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysUser.SysUserInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "searchKey", label: "姓名或账号", search: { el: "input" }, isShow: false },
{ prop: "avatar", label: "头像", width: 80 },
{ prop: "account", label: "账号" },
{ prop: "name", label: "姓名" },
{ prop: "gender", label: "性别", width: 80 },
{ prop: "phone", label: "手机号" },
{ prop: "orgName", label: "机构" },
{ prop: "positionName", label: "职位" },
{
prop: "status",
label: "状态",
width: 90,
enum: statusOptions,
search: { el: "select" }
},
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.orgId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.orgId = 0;
}
}
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysUser.SysUserInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(bizUserApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
/** 更多下拉菜单命令枚举 */
enum cmdEnum {
ResetPassword = "重置密码",
GrantRole = "授权角色"
}
/** 下拉菜单参数接口 */
interface Command {
row: SysUser.SysUserInfo;
command: cmdEnum;
}
/**配置command的参数 */
function command(row: SysUser.SysUserInfo, command: cmdEnum): Command {
return {
row: row,
command: command
};
}
const roleSelectorRef = ref<RoleSelectorInstance>(); //角色选择器引用
const userId = ref<number | string>(0); //人员id
const grantResourceRef = ref<InstanceType<typeof GrantResource> | null>(null); //授权资源组件引用
const grantPermissionRef = ref<InstanceType<typeof GrantPermission> | null>(null); //授权权限组件引用
/**
* 更多下拉菜单点击事件
* @param command
*/
function handleCommand(command: Command) {
switch (command.command) {
case cmdEnum.ResetPassword:
useHandleData(bizUserApi.resetPassword, { id: command.row.id }, cmdEnum.ResetPassword);
break;
case cmdEnum.GrantRole:
userId.value = command.row.id; //获取人员id
bizUserApi.ownRole({ id: command.row.id }).then(res => {
roleSelectorRef.value?.showSelector(res.data); //显示人员选择器
});
break;
}
}
/** 选择角色 */
function handleChooseRole(data: SysRole.SysRoleInfo[]) {
//组装参数
const grantUser: SysUser.GrantRoleReq = {
id: userId.value,
roleIdList: data.map(item => item.id) as number[] | string[]
};
bizUserApi.grantRole(grantUser); //授权角色
}
/**
* 修改状态
* @param row 当前行数据
*/
function editStatus(row: SysUser.SysUserInfo) {
switchLoading.value = true;
const status = row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE; // 状态取反
bizUserApi
.updateStatus({ id: row.id }, status)
.then(() => {
ElMessage.success("修改成功");
row.status = status;
})
.finally(() => {
switchLoading.value = false;
});
}
</script>
<style lang="scss" scoped></style>
+12
View File
@@ -0,0 +1,12 @@
.home {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
.home-bg {
width: 70%;
max-width: 1200px;
margin-bottom: 20px;
}
}
+11
View File
@@ -0,0 +1,11 @@
<template>
<div class="home card">
<img class="home-bg" src="@/assets/images/welcome.png" alt="welcome" />
</div>
</template>
<script setup lang="ts" name="home"></script>
<style scoped lang="scss">
@use "./index";
</style>
View File
+14
View File
@@ -0,0 +1,14 @@
<template>
<div class="card content-box">
<span class="text">
Gitee 仓库
<a href="https://gitee.com/dotnetmoyu/SimpleAdmin" target="_blank">https://gitee.com/dotnetmoyu/SimpleAdmin</a> 🍒🍉🍊
</span>
</div>
</template>
<script setup lang="ts" name="gitee"></script>
<style scoped lang="scss">
@use "./index";
</style>
@@ -0,0 +1,224 @@
<template>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large">
<s-form-item v-if="tenantOption == TenantEnum.CHOSE" prop="orgId" class="-ml-12px">
<s-select v-model="loginForm.tenantId" :options="tenantOptions" label="name" value="id" placeholder="请选择租户"></s-select>
</s-form-item>
<el-form-item prop="phone">
<el-input v-model="loginForm.phone" placeholder="请输入手机号码">
<template #prefix>
<el-icon class="el-input__icon">
<Iphone />
</el-icon>
</template>
</el-input>
</el-form-item>
<!-- 验证码 -->
<el-form-item prop="validCode">
<div class="flex-y-center w-full">
<el-input v-model="loginForm.validCode" placeholder="请输入短信验证码">
<template #prefix>
<el-icon class="el-input__icon">
<message />
</el-icon>
</template>
</el-input>
<div class="w-18px"></div>
<el-button size="large" @click="getCaptchaCode" :disabled="smsState.smsSendBtn">
{{ (!smsState.smsSendBtn && "获取验证码") || smsState.time + " s" }}
</el-button>
</div>
</el-form-item>
</el-form>
<div class="login-btn">
<el-button :icon="CircleClose" round size="large" @click="resetForm(loginFormRef)"> 重置 </el-button>
<el-button :icon="UserFilled" round size="large" type="primary" :loading="auth.loginLoading" @click="handleSubmit(loginFormRef)">
登录
</el-button>
</div>
<!-- 验证码弹窗 -->
<el-dialog v-model="dialogVisible" title="机器验证" width="20%" show-close destroy-on-close>
<el-form ref="captchaFormRef" :model="captchaForm" :rules="captchaRules" class="mt-25px" size="large">
<el-row :gutter="8">
<el-col :span="17">
<el-form-item prop="validCode">
<div class="flex-y-center w-full">
<el-input v-model="captchaForm.validCode" placeholder="请输入验证码">
<template #prefix>
<el-icon class="el-input__icon">
<key />
</el-icon>
</template>
</el-input>
</div>
</el-form-item>
</el-col>
<el-col :span="7"> <img :src="captchaForm.validCodeBase64" @click="loginCaptcha" /></el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="getPhoneValidCode">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onBeforeUnmount } from "vue";
import { Login, SysOrg, loginApi } from "@/api";
import { CircleClose, UserFilled } from "@element-plus/icons-vue";
import type { ElForm } from "element-plus";
import { TenantEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { useAuthStore, useConfigStore } from "@/stores/modules";
import { ElMessage } from "element-plus";
const auth = useAuthStore();
const config = useConfigStore();
const { loginPhone } = useAuthStore();
// 手机登录接口
interface PhoneProps {
/** 多租户选项 */
tenantOption: TenantEnum;
/** 租户列表 */
tenantOptions: SysOrg.SysOrgInfo[];
}
//props
const props = defineProps<PhoneProps>();
const dialogVisible = ref(false); // 是否显示弹窗
const loginForm = reactive<Login.PhoneLoginForm>({
phone: "18652848888",
validCode: "", // 验证码
validCodeReqNo: "", // 验证码请求号
tenantId: config.tenantIdGet
});
// 如果没有租户id就默认选中第一个
if (!config.tenantIdGet) {
loginForm.tenantId = props.tenantOptions[0]?.id;
}
// 发送验证码状态
let smsState = ref({
/** 倒计时 */
time: 60,
/** 发送验证码按钮状态 */
smsSendBtn: false
});
// 图形验证码表单参数
const captchaForm = reactive({
/** 图形验证码 */
validCode: "",
/** 图形验证码请求号 */
validCodeReqNo: "",
/** 图形验证码base64 */
validCodeBase64: ""
});
const captchaFormRef = ref<FormInstance>(); // 图形验证码表单实例
type FormInstance = InstanceType<typeof ElForm>; // 表单实例
const loginFormRef = ref<FormInstance>(); // 表单实例引用
const loginRules = reactive({
phone: [required("请输入手机号码")],
validCode: [required("请输入短信验证码")],
tenantId: [required("请选择租户")]
});
const captchaRules = reactive({
validCode: [required("请输入验证码")]
});
//监听tenantOptions变化
watch(
() => props.tenantOptions,
(newVal: SysOrg.SysOrgInfo[]) => {
if (newVal.length > 0) {
// 如果没有选择租户就默认选中第一个
if (!loginForm.tenantId) {
loginForm.tenantId = newVal[0].id;
}
}
}
);
// login
const handleSubmit = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async valid => {
if (!valid) return;
await loginPhone({ ...loginForm }).catch(async () => {
await loginCaptcha(); // 加载验证码
}); // 调用登录接口
});
};
// resetForm
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
/** 获取图片验证码 */
async function getCaptchaCode() {
await loginCaptcha(); // 加载验证码
dialogVisible.value = true;
}
/** 加载图片验证码 */
async function loginCaptcha() {
const { data } = await loginApi.picCaptcha(); // 获取验证码
if (data) {
captchaForm.validCodeBase64 = data.validCodeBase64; // 验证码base64
captchaForm.validCodeReqNo = data.validCodeReqNo; // 验证码请求号
}
}
/** 获取手机验证码 */
function getPhoneValidCode() {
captchaFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
dialogVisible.value = false;
//设置发送验证码按钮状态
smsState.value.smsSendBtn = true;
const interval = window.setInterval(() => {
if (smsState.value.time-- <= 0) {
smsState.value.time = 60;
smsState.value.smsSendBtn = false;
window.clearInterval(interval);
}
}, 1000);
loginApi
.getPhoneValidCode({
phone: loginForm.phone,
validCode: captchaForm.validCode,
validCodeReqNo: captchaForm.validCodeReqNo
})
.then(res => {
console.log("[ res.data ] >", res.data);
loginForm.validCodeReqNo = res.data;
//发送成功
dialogVisible.value = false;
// 提示发送成功
ElMessage.success("验证码发送成功");
})
.catch(() => {
//发送失败
ElMessage.error("短信验证码发送失败");
smsState.value.time = 60;
smsState.value.smsSendBtn = false;
window.clearInterval(interval);
});
});
}
onBeforeUnmount(() => {
document.onkeydown = null;
});
</script>
<style scoped lang="scss">
@use "../../index";
</style>
@@ -0,0 +1,166 @@
<!-- 密码登录 -->
<template>
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large">
<s-form-item v-if="tenantOption == TenantEnum.CHOSE" prop="orgId" class="-ml-12px">
<s-select v-model="loginForm.tenantId" :options="tenantOptions" label="name" value="id" placeholder="请选择租户"></s-select>
</s-form-item>
<el-form-item prop="account">
<el-input v-model="loginForm.account" placeholder="请输入用户名">
<template #prefix>
<el-icon class="el-input__icon">
<user />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" show-password autocomplete="new-password">
<template #prefix>
<el-icon class="el-input__icon">
<lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="captchaOpen" prop="validCode">
<div class="flex-y-center w-full">
<el-input v-model="loginForm.validCode" placeholder="请输入验证码">
<template #prefix>
<el-icon class="el-input__icon">
<key />
</el-icon>
</template>
</el-input>
<div class="w-18px"></div>
<img :src="validCodeBase64" @click="loginCaptcha" />
</div>
</el-form-item>
</el-form>
<div class="login-btn">
<el-button :icon="CircleClose" round size="large" @click="resetForm(loginFormRef)"> 重置 </el-button>
<el-button :icon="UserFilled" round size="large" type="primary" :loading="auth.loginLoading" @click="handleSubmit(loginFormRef)">
登录
</el-button>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount } from "vue";
import { Login } from "@/api/interface";
import { SysOrg, commonApi, loginApi } from "@/api";
import { useAuthStore, useConfigStore } from "@/stores/modules";
import { CircleClose, UserFilled } from "@element-plus/icons-vue";
import type { ElForm } from "element-plus";
import { SysBaseEnum, TenantEnum } from "@/enums";
import smCrypto from "@/utils/smCrypto";
import { required } from "@/utils/formRules";
const auth = useAuthStore();
const config = useConfigStore();
const { loginPwd } = useAuthStore();
type FormInstance = InstanceType<typeof ElForm>;
const captchaOpen = ref(false); // 是否开启验证码
const validCodeBase64 = ref(); // 验证码base64
// 密码登录接口
interface PwdProps {
/** 多租户选项 */
tenantOption: TenantEnum;
/** 租户列表 */
tenantOptions: SysOrg.SysOrgInfo[];
}
//props
const props = defineProps<PwdProps>();
//监听tenantOptions变化
watch(
() => props.tenantOptions,
(newVal: SysOrg.SysOrgInfo[]) => {
if (newVal.length > 0) {
// 如果没有选择租户就默认选中第一个
if (!loginForm.tenantId) {
loginForm.tenantId = newVal[0].id;
}
}
}
);
const loginFormRef = ref<FormInstance>(); // 表单实例
// 表单数据
const loginForm = reactive<Login.LoginForm>({
account: "superAdmin", //用户名
password: "123456", // 密码
validCode: "", // 验证码
validCodeReqNo: "", // 验证码请求号
tenantId: config.tenantIdGet
});
// 如果没有租户id就默认选中第一个
if (!config.tenantIdGet) {
loginForm.tenantId = props.tenantOptions[0]?.id;
}
// 表单验证规则
const loginRules = reactive({
account: [required("请输入用户名")],
password: [required("请输入密码")],
tenantId: [required("请选择租户")],
validCode: [required("请输入验证码")]
});
const handleSubmit = (formEl: FormInstance | undefined) => {
if (!formEl) return;
// 表单验证
formEl.validate(async valid => {
if (!valid) return;
await loginPwd({ ...loginForm, password: smCrypto.doSm2Encrypt(loginForm.password) }).catch(async () => {
await loginCaptcha(); // 加载验证码
}); // 调用登录接口
});
};
// 重置表单
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
onMounted(() => {
// 监听 enter 事件(调用登录)
document.onkeydown = (e: KeyboardEvent) => {
e = (window.event as KeyboardEvent) || e;
if (e.code === "Enter" || e.code === "enter" || e.code === "NumpadEnter") {
if (auth.loginLoading) return;
handleSubmit(loginFormRef.value);
}
};
// 获取验证码开关
commonApi.loginPolicy().then(async res => {
// 如果验证码开关是开就加载验证码
if (res.data) {
captchaOpen.value = res.data.find(item => item.configKey === SysBaseEnum.LOGIN_CAPTCHA_OPEN)?.configValue === "true"; // 判断是否开启验证码
if (captchaOpen.value) {
await loginCaptcha(); // 加载验证码
}
}
});
});
/** 加载图片验证码 */
async function loginCaptcha() {
const { data } = await loginApi.picCaptcha(); // 获取验证码
if (data) {
validCodeBase64.value = data.validCodeBase64; // 验证码base64
loginForm.validCodeReqNo = data.validCodeReqNo; // 验证码请求号
}
}
onBeforeUnmount(() => {
document.onkeydown = null;
});
</script>
<style scoped lang="scss">
@use "../../index";
</style>
+93
View File
@@ -0,0 +1,93 @@
.login-container {
height: 100%;
min-height: 550px;
background-color: #eeeeee;
background-image: url("@/assets/images/login_bg.svg");
background-size: 100% 100%;
background-size: cover;
.login-box {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-around;
width: 96.5%;
height: 94%;
padding: 0 50px;
background-color: rgb(255 255 255 / 80%);
border-radius: 10px;
.dark {
position: absolute;
top: 13px;
right: 18px;
}
.login-left {
width: 800px;
margin-right: 10px;
.login-left-img {
width: 100%;
height: 100%;
}
}
.login-form {
position: relative;
width: 420px;
padding: 50px 40px 45px;
background-color: var(--el-bg-color);
border-radius: 10px;
box-shadow: rgb(0 0 0 / 10%) 0 2px 10px 2px;
.login-logo {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 15px;
.login-icon {
width: 60px;
height: 52px;
}
.logo-text {
padding: 0 0 0 25px;
margin: 0;
font-size: 42px;
font-weight: bold;
color: #34495e;
white-space: nowrap;
}
}
.el-form-item {
margin-bottom: 20px;
}
.login-btn {
display: flex;
justify-content: space-between;
width: 100%;
margin-top: 30px;
white-space: nowrap;
.el-button {
width: 185px;
}
}
/* 这是需要固定在右下角的元素的样式 */
.fixed-corner {
position: absolute;
right: 0;
bottom: 0;
/* 其余样式... */
}
}
}
}
@media screen and (width <= 1250px) {
.login-left {
display: none;
}
}
@media screen and (width <= 600px) {
.login-form {
width: 97% !important;
}
}
+116
View File
@@ -0,0 +1,116 @@
<!--
* @Description: 登录页
* @Author: huguodong
* @Date: 2023-12-15 15:42:19
!-->
<template>
<div class="login-container flx-center">
<div class="login-box">
<SwitchDark class="dark" />
<div class="login-left">
<img class="login-left-img" src="@/assets/images/login_left.png" alt="login" />
</div>
<div class="login-form">
<div class="login-logo">
<img v-show="props.sysLogo" class="login-icon" :src="props.sysLogo" alt="" />
<h2 class="logo-text">{{ props.sysName }}</h2>
</div>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="账号登录" name="pwd">
<LoginForm :tenant-option="props.sysTenantOption" :tenant-options="props.tenantOptions" />
</el-tab-pane>
<el-tab-pane label="手机号登录" name="phone">
<LoginPhoneForm :tenant-option="props.sysTenantOption" :tenant-options="props.tenantOptions" />
</el-tab-pane>
</el-tabs>
<div class="fixed-corner">
<span>{{ props.sysVersion }}</span>
</div>
</div>
</div>
</div>
<Footer />
<choose-module v-if="auth.showChooseModule" :show-modal="true" />
</template>
<script setup lang="ts" name="login">
import LoginForm from "./components/pwd-login/index.vue";
import LoginPhoneForm from "./components/phone-login/index.vue";
import SwitchDark from "@/components/SwitchDark/index.vue";
import { useAuthStore, useConfigStore } from "@/stores/modules";
import Footer from "@/layouts/components/Footer/index.vue";
import { TenantEnum } from "@/enums";
import { SysConfig, SysOrg } from "@/api";
const auth = useAuthStore();
const config = useConfigStore();
const activeName = ref("pwd");
/** 登录参数接口 */
interface LoginProps {
/** 系统名称 */
sysName: string;
/** 系统版本 */
sysVersion: string;
/** 系统logo */
sysLogo: string;
/** 多租户选项 */
sysTenantOption: TenantEnum;
/** 多租户列表 */
tenantOptions: SysOrg.SysOrgInfo[]; // 租户列表
}
//默认值
const props = reactive<LoginProps>({
sysName: "SimpleAdmin",
sysVersion: "",
sysLogo: "",
sysTenantOption: TenantEnum.CLOSE,
tenantOptions: config.tenantListGet
});
onMounted(() => {
// 更新系统配置
updateSysConfig(props, config);
});
/**
* 更新系统配置
* @param props 登录参数
* @param config 配置store
*/
function updateSysConfig(props: LoginProps, config: ReturnType<typeof useConfigStore>) {
// 定义更新配置的逻辑
const updateConfig = (res: SysConfig.SysBaseConfig) => {
props.sysName = res.SYS_NAME;
props.sysVersion = res.SYS_VERSION;
props.sysLogo = res.SYS_LOGO;
props.sysTenantOption = res.SYS_TENANT_OPTIONS;
// 如果多租户选项是选择就加载租户列表
if (props.sysTenantOption === TenantEnum.CHOSE) {
config.setTenantList().then(res => {
props.tenantOptions = res;
});
} else {
props.tenantOptions = [];
config.delTenantId();
}
};
// 先从本地存储获取系统配置,放置网络延迟导致页面组件显示延迟
config.getSysBaseInfo().then(updateConfig);
// 再从网络获取系统配置
config.setSysBaseInfo().then(updateConfig);
}
</script>
<style scoped lang="scss">
@use "./index";
.demo-tabs > .el-tabs__content {
padding: 32px;
font-size: 32px;
font-weight: 600;
color: #6b778c;
}
</style>
@@ -0,0 +1,147 @@
<template>
<el-dialog :model-value="visible" @update:model-value="handleUpdateVisible" title="添加药材" width="600px" destroy-on-close>
<el-form :model="form" label-width="120px" ref="formRef" class="mt-2" :rules="formRules">
<!-- 药材下拉 + 未找到按钮 -->
<el-form-item v-if="!isAddingNew" label="药材名称" prop="medicinalTypeId" style="display: flex; align-items: center">
<el-select v-model="form.medicinalTypeId" placeholder="请选择药材" filterable style="flex: 1; margin-right: 8px">
<el-option v-for="item in medicinalList" :key="item.id" :label="item.dictLabel" :value="item.id" />
</el-select>
<el-button type="text" @click="handleNotFound">未找到</el-button>
</el-form-item>
<!-- 新药材输入框 -->
<el-form-item label="新药材名称" v-if="isAddingNew" prop="medicinalName">
<el-input v-model="form.medicinalName" placeholder="请输入新药材名称" />
</el-form-item>
<el-form-item label="添加量(g)" prop="addWeight">
<el-input v-model.number="form.addWeight" type="number" min="0.01" step="0.01" placeholder="请输入添加量" />
</el-form-item>
<!-- 改为总价输入 -->
<el-form-item label="总价" prop="totalPrice">
<el-input v-model.number="form.totalPrice" type="number" min="0" step="0.01" placeholder="请输入总价(元)" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确认添加</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from "vue";
import { medicinalApi } from "@/api/modules/biz/WeiBingJu/medicinal";
import { ElMessage, ElForm, FormRules } from "element-plus";
const props = defineProps({
visible: { type: Boolean, default: false }
});
const emit = defineEmits(["update:visible", "close", "refreshList"]);
const medicinalList = ref<any[]>([]);
const isAddingNew = ref(false);
const formRef = ref<InstanceType<typeof ElForm>>();
const form = reactive({
medicinalTypeId: null as number | null,
addWeight: 0,
// 前端改为 totalPrice
totalPrice: 0,
medicinalName: ""
});
// 动态表单校验(注意 totalPrice 字段)
const formRules = computed<FormRules>(() => ({
medicinalTypeId: [{ required: !isAddingNew.value, message: "请选择药材", trigger: "change" }],
medicinalName: [{ required: isAddingNew.value, message: "请输入新药材名称", trigger: "blur" }],
addWeight: [
{ required: true, message: "请输入添加量", trigger: "blur" },
{ type: "number", min: 0.01, message: "添加量不能小于0.01", trigger: "blur" }
],
totalPrice: [
{ required: true, message: "请输入总价", trigger: "blur" },
{ type: "number", min: 0, message: "总价不能为负", trigger: "blur" }
]
}));
const fetchMedicinalList = async () => {
try {
const res = await medicinalApi.getDictList();
medicinalList.value = res.data || [];
} catch (err) {
ElMessage.error("加载药材列表失败");
medicinalList.value = [];
}
};
watch(
() => props.visible,
async newVal => {
if (newVal) {
handleResetForm();
await fetchMedicinalList();
}
}
);
const handleUpdateVisible = (newVisible: boolean) => {
emit("update:visible", newVisible);
if (!newVisible) {
handleResetForm();
emit("close");
}
};
const handleCancel = () => emit("update:visible", false);
const handleResetForm = () => {
form.medicinalTypeId = null;
form.addWeight = 0;
form.totalPrice = 0;
form.medicinalName = "";
isAddingNew.value = false;
formRef.value?.resetFields();
};
const handleNotFound = () => {
isAddingNew.value = true;
form.medicinalTypeId = null;
};
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
} catch {
return;
}
// 计算单价,并校验 addWeight > 0
if (!form.addWeight || form.addWeight <= 0) {
ElMessage.warning("添加量需大于 0");
return;
}
// unitPrice = totalPrice / addWeight,保留两位
const unitPrice = parseFloat((form.totalPrice / form.addWeight).toFixed(2));
// 构造后端期望的 payload(注意把 totalPrice 改为 unitPrice)
const payload: any = {
medicinalTypeId: form.medicinalTypeId,
addWeight: form.addWeight,
unitPrice: unitPrice,
medicinalName: form.medicinalName
};
try {
await medicinalApi.addMedicinal(payload);
ElMessage.success("添加成功");
emit("update:visible", false);
emit("refreshList");
} catch (err: any) {
ElMessage.error(err.message || "添加失败");
}
};
</script>
@@ -0,0 +1,225 @@
<template>
<el-dialog
:model-value="props.visible"
:title="`${props.medicinalName} - 添加记录`"
width="1200px"
@close="handleDialogClose"
@update:model-value="handleUpdateVisible"
append-to-body
>
<ProTable ref="proTableRef" title="添加记录" :columns="columns" :request-api="getAdditionRecordsApi" :row-key="rowKey" :show-selection="false">
<!-- 行内列 slot:addWeight -->
<template #addWeight="{ row }">
<template v-if="editingId === row.id">
<el-input-number v-model.number="editModel.addWeight" :min="0.01" :step="0.01" controls-position="right" size="small" />
</template>
<template v-else>
{{ row.addWeight }}
</template>
</template>
<!-- unitPrice 列:只展示单价,不编辑 -->
<template #unitPrice="{ row }">
{{ (row.unitPrice ?? 0).toFixed(2) }}
</template>
<!-- totalPrice 列:查看时显示合计,编辑时可以修改 -->
<template #totalPrice="{ row }">
<template v-if="editingId === row.id">
<el-input-number
v-model.number="editModel.totalPrice"
:min="0"
:step="0.01"
controls-position="right"
size="small"
placeholder="输入总价(元)"
/>
</template>
<template v-else>
{{ ((row.addWeight ?? 0) * (row.unitPrice ?? 0)).toFixed(2) }}
</template>
</template>
<!-- addTime 格式化 -->
<template #addTime="{ row }">
{{ row.addTime ? formatDate(row.addTime) : "-" }}
</template>
<!-- 操作列 -->
<template #operation="{ row }">
<template v-if="editingId === row.id">
<el-button size="small" type="primary" @click="saveRowEdit(row)" :loading="saving"> 保存 </el-button>
<el-button size="small" @click="cancelRowEdit">取消</el-button>
</template>
<template v-else>
<el-button type="text" size="small" @click="startRowEdit(row)"> 编辑 </el-button>
<el-button type="text" size="small" text-color="#ff4d4f" @click="handleDeleteRecord(row.id)"> 删除 </el-button>
</template>
</template>
</ProTable>
</el-dialog>
</template>
<script setup lang="ts">
/**
* 单行行内编辑版本(总价 totalPrice 输入):
* - 点击「编辑」:将该行复制到 editModel(editModel.addWeight & editModel.totalPrice),记录 editingId(只允许单行编辑)
* - 保存:计算 unitPrice = totalPrice / addWeight,发送 unitPrice 到后端
* - 取消:不调用后端,直接清除 editingId(恢复显示)
*/
import { defineProps, defineEmits, ref, reactive, watch, nextTick } from "vue";
import ProTable from "@/components/ProTable/index.vue";
import { medicinalApi } from "@/api";
import { ElMessage, ElMessageBox } from "element-plus";
const props = defineProps({
visible: { type: Boolean, default: false },
medicinalId: { type: Number, required: true },
medicinalName: { type: String, default: "药材" }
});
const emit = defineEmits(["update:visible", "close", "refreshList"]);
// proTable 引用(用于 refresh)
const proTableRef = ref<any | null>(null);
// 当前正在编辑的行 id(单行)
const editingId = ref<number | null>(null);
// 编辑模型(只存一行):注意 totalPrice 而不是 unitPrice
const editModel = reactive({
addWeight: 0,
totalPrice: 0
});
// 保存中标记,避免重复提交
const saving = ref(false);
// 列定义,slotName 使用 prop 名称(与 ProTable slot 对应)
const columns = [
{ prop: "addWeight", label: "添加量(g)", minWidth: 120, slotName: "addWeight" },
{ prop: "unitPrice", label: "单价(元/g)", minWidth: 120, slotName: "unitPrice" },
{ prop: "totalPrice", label: "总价(元)", minWidth: 120, slotName: "totalPrice" },
{ prop: "addTime", label: "添加时间", minWidth: 200, slotName: "addTime" },
{ prop: "addUserName", label: "添加人员", minWidth: 150 },
{ prop: "operation", label: "操作", width: 180, fixed: "right", slotName: "operation" }
];
const rowKey = (row: any) => row.id;
// 打开弹窗时刷新表格
watch(
() => props.visible,
async val => {
if (val && props.medicinalId) {
await nextTick();
proTableRef.value?.refresh?.();
}
},
{ immediate: true }
);
// 将 ProTable 的请求透传到后端(ProTable 会带上 pageNum/pageSize 等)
const getAdditionRecordsApi = (params: any) => {
return medicinalApi.getAdditionRecords(props.medicinalId, params);
};
// 开始编辑:把当前行复制到 editModel(addWeight 和 totalPrice)
const startRowEdit = (row: any) => {
if (editingId.value !== null && editingId.value !== row.id) {
ElMessage.warning("请先保存或取消当前正在编辑的行");
return;
}
editingId.value = row.id;
editModel.addWeight = row.addWeight ?? 0;
// 后端返回的是 unitPrice,前端要显示/编辑 totalPrice = unitPrice * addWeight
editModel.totalPrice = Number(((row.unitPrice ?? 0) * (row.addWeight ?? 1)).toFixed(2));
};
// 取消编辑:清空 editingId(恢复显示)
const cancelRowEdit = () => {
editingId.value = null;
editModel.addWeight = 0;
editModel.totalPrice = 0;
};
// 保存编辑:计算 unitPrice = totalPrice / addWeight -> 发送到后端
const saveRowEdit = async (row: any) => {
// 基本校验
if (!editModel.addWeight || editModel.addWeight <= 0) {
ElMessage.warning("添加量需大于 0");
return;
}
if (editModel.totalPrice < 0) {
ElMessage.warning("总价不能为负");
return;
}
try {
saving.value = true;
// 计算 unitPrice(保留两位)
const unitPrice = parseFloat((editModel.totalPrice / editModel.addWeight).toFixed(2));
await medicinalApi.updateAdditionRecord(row.id, {
addWeight: editModel.addWeight,
unitPrice: unitPrice
});
ElMessage.success("保存成功");
// 刷新表格并退出编辑
await proTableRef.value?.refresh?.();
emit("refreshList");
cancelRowEdit();
} catch (err: any) {
ElMessage.error(err?.message || "保存失败");
} finally {
saving.value = false;
}
};
// 删除记录
const handleDeleteRecord = async (id: number) => {
ElMessageBox.confirm("确定要删除这条添加记录吗?删除后不可恢复!", "确认删除", {
confirmButtonText: "确认",
cancelButtonText: "取消",
type: "warning"
})
.then(async () => {
try {
await medicinalApi.deleteAdditionRecord(id);
ElMessage.success("删除成功");
await proTableRef.value?.refresh?.();
emit("refreshList");
} catch (err: any) {
ElMessage.error(err?.message || "删除失败");
}
})
.catch(() => {
/* 用户取消删除 */
});
};
// 格式化时间
const formatDate = (dateStr: string) => {
const d = new Date(String(dateStr).replace(" ", "T"));
return isNaN(d.getTime()) ? dateStr : d.toLocaleString();
};
// 处理弹窗关闭
const handleDialogClose = () => {
editingId.value = null;
editModel.addWeight = 0;
editModel.totalPrice = 0;
emit("update:visible", false);
emit("close");
};
const handleUpdateVisible = (newVal: boolean) => {
if (!newVal) handleDialogClose();
};
</script>
<style scoped lang="scss">
.mb-4 {
margin-bottom: 16px;
}
</style>
@@ -0,0 +1,85 @@
<template>
<!-- 关键修改:v-model → :visible + @update:visible -->
<el-dialog :visible="visible" @update:visible="handleUpdateVisible" :title="`${medicinalName} - 消耗记录`" width="800px">
<el-table v-loading="loading" :data="consumptionRecords" border style="width: 100%" empty-text="暂无消耗记录">
<el-table-column prop="amountG" label="使用量(g)" width="120"></el-table-column>
<el-table-column label="使用人员" width="200">
<template #default="scope">
<span>{{ scope.row.personName }}</span>
<el-tag size="small" type="info" v-if="scope.row.isRelative" style="margin-left: 8px">
家属({{ scope.row.employeeName || "未知员工" }})
</el-tag>
</template>
</el-table-column>
<el-table-column prop="useTime" label="使用时间" width="200">
<template #default="scope">
{{ scope.row.useTime ? new Date(scope.row.useTime).toLocaleString() : "-" }}
</template>
</el-table-column>
</el-table>
</el-dialog>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, ref, watch } from "vue";
import { getConsumptionRecords, Medicinal } from "@/api/modules/biz/WeiBingJu/medicinal";
import { ElMessage } from "element-plus";
// 1. 接收父组件prop
const props = defineProps({
visible: {
type: Boolean,
default: false
},
medicinalId: {
type: Number,
required: true
},
medicinalName: {
type: String,
default: "药材"
}
});
// 2. 定义发射事件:update:visible + 关闭通知
const emit = defineEmits(["update:visible", "close"]);
// 状态数据
const loading = ref(false);
const consumptionRecords = ref<Medicinal.ConsumptionRecord[]>([]);
// 3. 监听弹窗状态变化
const handleUpdateVisible = (newVisible: boolean) => {
emit("update:visible", newVisible);
// 弹窗关闭时清空数据
if (!newVisible) {
consumptionRecords.value = [];
emit("close");
}
};
// 监听visible变化:弹窗显示时加载消耗记录
watch(
() => props.visible,
async newVal => {
if (newVal && props.medicinalId) {
await fetchConsumptionRecords();
}
},
{ immediate: true }
);
// 加载消耗记录
const fetchConsumptionRecords = async () => {
loading.value = true;
try {
const res = await getConsumptionRecords({ medicinalId: props.medicinalId });
consumptionRecords.value = res;
} catch (err: any) {
ElMessage.error(err.message || "加载消耗记录失败");
consumptionRecords.value = [];
} finally {
loading.value = false;
}
};
</script>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div class="table-box">
<el-card class="mt-4">
<ProTable ref="proTable" title="药材列表" :columns="columns" :request-api="medicinalApi.page">
<template #tableHeader>
<!-- 新增按钮:保持框架一致的 s-button 写法 -->
<s-button suffix="药材" @click="onOpenAdd" />
</template>
<template #operation="scope">
<s-button link :opt="FormOptEnum.VIEW" size="small" @click="handleViewConsumption(scope.row)"> 查看消耗记录 </s-button>
<s-button link :opt="FormOptEnum.VIEW" size="small" class="ml-2" @click="handleViewAddition(scope.row)"> 查看添加记录 </s-button>
</template>
</ProTable>
</el-card>
<!-- 消耗记录弹窗 -->
<consumption-record-dialog v-model:visible="showConsumptionDialog" :medicinal-id="currentMedicinalId" :medicinal-name="currentMedicinalName" />
<!-- 添加记录弹窗 -->
<addition-record-dialog
:visible="showAdditionDialog"
:medicinal-id="currentMedicinalId"
:medicinal-name="currentMedicinalName"
@update:visible="val => (showAdditionDialog = val)"
@refresh-list="handleRefresh"
/>
<!-- 新增药材弹窗 -->
<add-medicinal-dialog v-model:visible="showAddMedicinalDialog" @refresh-list="handleRefresh" />
</div>
</template>
<script setup lang="tsx" name="sysModule">
import { medicinalApi, MedicinalInfo } from "@/api";
import { ref } from "vue";
import ProTable from "@/components/ProTable/index.vue";
import type { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { FormOptEnum } from "@/enums";
// 子组件
//import ConsumptionRecordDialog from "./components/ConsumptionRecordDialog.vue";
import AdditionRecordDialog from "./components/AdditionRecordDialog.vue";
import AddMedicinalDialog from "./components/AddMedicinalDialog.vue";
const proTable = ref<ProTableInstance | null>(null);
const showConsumptionDialog = ref(false);
const showAdditionDialog = ref(false);
const showAddMedicinalDialog = ref(false);
const currentMedicinalId = ref<number | null>(null);
const currentMedicinalName = ref<string>("");
const columns: ColumnProps<MedicinalInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "medicinalName", label: "药材名称", search: { el: "input" } },
{ prop: "stock", label: "药材库存(g)" },
{ prop: "usedCost", label: "使用成本" },
{ prop: "remainingValue", label: "剩余价值" },
{ prop: "operation", label: "操作", width: 240, fixed: "right" }
];
// 打开新增药材弹窗
function onOpenAdd() {
showAddMedicinalDialog.value = true;
}
// 查看消耗记录
function handleViewConsumption(row: MedicinalInfo) {
currentMedicinalId.value = row.medicinalId ?? (row as any).id ?? null;
currentMedicinalName.value = row.medicinalName ?? "";
showConsumptionDialog.value = true;
}
// 查看添加记录
function handleViewAddition(row: MedicinalInfo) {
console.log("查看添加记录", row);
// 直接设置正确的ID和名称,然后显示弹窗
currentMedicinalId.value = row.medicinalId ?? (row as any).id ?? null;
currentMedicinalName.value = row.medicinalName ?? "";
showAdditionDialog.value = true;
}
// 刷新列表
function handleRefresh() {
proTable.value?.refresh?.();
}
</script>
<style scoped lang="scss">
.table-box {
padding-bottom: 24px;
}
.mt-4 {
margin-top: 16px;
}
.ml-2 {
margin-left: 8px;
}
</style>
@@ -0,0 +1,714 @@
<template>
<el-dialog :model-value="visible" @update:model-value="handleUpdateVisible" :title="dialogTitle" width="900px" destroy-on-close>
<!-- 人员信息 -->
<el-card shadow="hover" class="mb-4">
<template #header><span>人员信息</span></template>
<el-form :model="personForm" label-width="120px" :disabled="!isEdit" ref="personFormRef" :rules="personFormRules">
<!-- 1. 人员类型 -->
<el-form-item label="人员类型" required prop="isEmployee">
<el-radio-group v-model="personForm.isEmployee" @change="handlePersonTypeChange">
<el-radio :label="true">公司员工</el-radio>
<el-radio :label="false">员工家属</el-radio>
</el-radio-group>
</el-form-item>
<!-- 2. 员工场景:员工选择/新增 -->
<template v-if="personForm.isEmployee">
<!-- 员工下拉选择(默认显示) -->
<el-form-item v-if="showEmployeeSelect" label="员工姓名" required prop="employeeId">
<el-select
v-model="personForm.employeeId"
placeholder="搜索或选择员工"
filterable
remote
:remote-method="handleEmployeeSearch"
@change="handleEmployeeChange"
clearable
>
<!-- 正常员工选项 -->
<el-option v-for="emp in employeeOptions" :key="emp.id" :label="emp.name" :value="emp.id" />
<!-- 未找到选项(仅搜索后无结果时显示) -->
<el-option v-if="employeeOptions.length === 0 && employeeKeyword" :key="0" :label="`未找到,新增员工:${employeeKeyword}`" :value="0" />
</el-select>
</el-form-item>
<!-- 员工新增文本框(点击"未找到"后显示) -->
<el-form-item v-if="!showEmployeeSelect" label="员工姓名" required prop="newEmployeeName">
<el-input
v-model="personForm.newEmployeeName"
placeholder="输入新员工姓名"
@blur="() => (personForm.name = personForm.newEmployeeName)"
/>
</el-form-item>
</template>
<!-- 3. 家属场景:关联员工选择 + 家属选择/新增 -->
<template v-else>
<!-- 3.1 关联员工选择(第一步) -->
<el-form-item v-if="showRelatedEmployeeSelect" label="关联员工" required prop="relatedEmployeeId">
<el-select
v-model="personForm.relatedEmployeeId"
placeholder="搜索或选择关联员工"
filterable
remote
:remote-method="handleRelatedEmployeeSearch"
@change="handleRelatedEmployeeChange"
clearable
>
<!-- 正常关联员工选项 -->
<el-option v-for="emp in relatedEmployeeOptions" :key="emp.id" :label="emp.name" :value="emp.id" />
<!-- 未找到选项 -->
<el-option
v-if="relatedEmployeeOptions.length === 0 && relatedEmployeeKeyword"
:key="0"
:label="`未找到,新增关联员工:${relatedEmployeeKeyword}`"
:value="0"
/>
</el-select>
</el-form-item>
<!-- 3.2 关联员工新增(点击"未找到"后显示) -->
<el-form-item v-if="!showRelatedEmployeeSelect" label="关联员工姓名" required prop="newRelatedEmployeeName">
<el-input v-model="personForm.newRelatedEmployeeName" placeholder="输入新关联员工姓名" />
</el-form-item>
<!-- 3.3 家属选择(选择关联员工后显示) -->
<el-form-item v-if="showRelativeSelect && personForm.relatedEmployeeId" label="家属姓名" required prop="relativeId">
<el-select
v-model="personForm.relativeId"
placeholder="搜索或选择家属"
filterable
remote
:remote-method="handleRelativeSearch"
@change="handleRelativeChange"
clearable
>
<!-- 正常家属选项 -->
<el-option v-for="rel in relativeOptions" :key="rel.id" :label="rel.name" :value="rel.id" />
<!-- 未找到选项 -->
<el-option v-if="relativeOptions.length === 0 && relativeKeyword" :key="0" :label="`未找到,新增家属:${relativeKeyword}`" :value="0" />
</el-select>
</el-form-item>
<!-- 3.4 家属新增(点击"未找到"后显示 / 关联员工新增时同步显示) -->
<el-form-item v-if="!showRelativeSelect || !showRelatedEmployeeSelect" label="家属姓名" required prop="newRelativeName">
<el-input
v-model="personForm.newRelativeName"
placeholder="输入新家属姓名"
@blur="() => (personForm.name = personForm.newRelativeName)"
/>
</el-form-item>
<!-- 3.5 亲属关系(家属场景必选) -->
<el-form-item label="亲属关系" required prop="relationShip" v-if="personForm.relatedEmployeeId || !showRelatedEmployeeSelect">
<el-select v-model="personForm.relationShip" clearable>
<el-option v-for="item in relationTypes" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</template>
<!-- 4. 公共信息:年龄/体重/身高(复用原有逻辑) -->
<el-form-item label="年龄" prop="age">
<el-input v-model.number="personForm.age" placeholder="输入年龄" />
</el-form-item>
<el-form-item label="体重(kg)" prop="weight">
<el-input v-model.number="personForm.weight" placeholder="输入体重" />
</el-form-item>
<el-form-item label="身高(cm)" prop="height">
<el-input v-model.number="personForm.height" placeholder="输入身高" />
</el-form-item>
</el-form>
</el-card>
<!-- 药材消耗、诊断建议等原有模块(保持不变) -->
<el-card shadow="hover" class="mb-4">
<template #header>
<div class="flex justify-between items-center">
<span>药材消耗</span>
<el-button v-if="isEdit" type="primary" size="small" @click="handleAddHerb"> 添加药材 </el-button>
</div>
</template>
<el-table :data="herbList" border :disabled="!isEdit" empty-text="暂无药材消耗记录">
<el-table-column label="药材名称" required>
<template #default="scope">
<el-select
v-model="scope.row.medicinalId"
placeholder="搜索或选择药材"
filterable
remote
:remote-method="kw => handleHerbSearch(kw, scope.$index)"
@change="val => handleHerbChange(val, scope.$index)"
:disabled="!isEdit"
clearable
>
<el-option v-for="item in scope.row.medicinalOptions" :key="item.id" :label="item.name" :value="item.id" />
<el-option
v-if="scope.row.medicinalOptions.length === 0 && scope.row.keyword"
:key="0"
:label="`新增药材:${scope.row.keyword}`"
:value="0"
/>
</el-select>
<el-input v-if="scope.row.showNewInput" v-model="scope.row.newName" placeholder="输入新药材名称" class="mt-2" :disabled="!isEdit" />
</template>
</el-table-column>
<el-table-column label="消耗(g)" required>
<template #default="scope">
<el-input v-model.number="scope.row.amountG" type="number" min="0.1" placeholder="输入消耗克数" :disabled="!isEdit" />
</template>
</el-table-column>
<el-table-column label="备注">
<template #default="scope">
<el-input v-model="scope.row.note" placeholder="输入备注" :disabled="!isEdit" />
</template>
</el-table-column>
<el-table-column label="操作" v-if="isEdit">
<template #default="scope">
<el-button type="text" text-color="#ff4d4f" @click="handleDeleteHerb(scope.$index)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<div class="flex gap-4 mb-4">
<el-card shadow="hover" class="flex-1">
<template #header><span>诊断结果</span></template>
<el-input v-model="prescriptionForm.diagnosisResult" type="textarea" rows="5" placeholder="输入诊断结果" :disabled="!isEdit" />
</el-card>
<el-card shadow="hover" class="flex-1">
<template #header><span>用药建议</span></template>
<el-input v-model="prescriptionForm.userMedicinaAdvice" type="textarea" rows="5" placeholder="输入用药建议" :disabled="!isEdit" />
</el-card>
</div>
<el-card shadow="hover" class="mb-4">
<el-form-item label="就诊时间" required>
<el-date-picker
v-model="prescriptionForm.visitDate"
type="datetime"
placeholder="选择就诊时间"
value-format="YYYY-MM-DD HH:mm:ss"
:disabled="!isEdit"
/>
</el-form-item>
</el-card>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave" v-if="isEdit">
{{ prescriptionId ? "保存修改" : "新增处方" }}
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed, nextTick } from "vue";
import { ElMessage, ElMessageBox, ElForm, FormItemRule } from "element-plus";
import { prescriptionApi } from "@/api/modules/biz/WeiBingJu/prescription";
// 1. props/emit定义
const props = defineProps({
visible: { type: Boolean, default: false },
prescriptionId: { type: Number, default: 0 },
isEdit: { type: Boolean, default: false }
});
const emit = defineEmits(["update:visible", "close", "refreshList"]);
// 2. 基础变量
const dialogTitle = computed(() => {
if (props.prescriptionId === 0) return "新增就诊处方";
return props.isEdit ? "编辑处方" : "处方详情";
});
const loading = ref(false);
const personFormRef = ref<InstanceType<typeof ElForm>>();
const relationTypes = ref<any[]>([]); // 亲属关系列表
const herbList = ref<any[]>([]); // 药材列表(原有逻辑)
// 3. 处方表单(原有逻辑)
const prescriptionForm = reactive({
diagnosisResult: "",
userMedicinaAdvice: "",
visitDate: new Date().toISOString().slice(0, 16) // 默认当前时间
});
// 4. 人员表单(核心重构:区分员工/家属场景)
const personForm = reactive({
// 基础字段
id: undefined, // 人员ID(编辑时用)
name: "", // 人员姓名(最终提交用)
isEmployee: true, // 是否员工(true=员工,false=家属)
// 员工场景字段
employeeId: undefined, // 已选员工ID
newEmployeeName: "", // 新增员工姓名
// 家属场景字段
relatedEmployeeId: undefined, // 关联员工ID
newRelatedEmployeeName: "", // 新增关联员工姓名
relativeId: undefined, // 已选家属ID
newRelativeName: "", // 新增家属姓名
relationShip: undefined, // 亲属关系ID
// 公共字段
age: undefined,
weight: undefined,
height: undefined
});
// 5. 下拉/新增状态控制(核心:控制显示隐藏)
const state = reactive({
// 员工场景
showEmployeeSelect: true, // 是否显示员工下拉(默认显示)
employeeOptions: [] as any[], // 员工下拉选项
employeeKeyword: "", // 员工搜索关键词
// 家属场景
showRelatedEmployeeSelect: true, // 是否显示关联员工下拉(默认显示)
relatedEmployeeOptions: [] as any[], // 关联员工下拉选项
relatedEmployeeKeyword: "", // 关联员工搜索关键词
showRelativeSelect: true, // 是否显示家属下拉(默认显示)
relativeOptions: [] as any[], // 家属下拉选项
relativeKeyword: "" // 家属搜索关键词
});
// 6. 表单验证规则
const personFormRules = reactive<Record<string, FormItemRule[]>>({
isEmployee: [{ required: true, message: "请选择人员类型", trigger: "change" }],
employeeId: [{ required: true, message: "请选择员工", trigger: "change" }],
newEmployeeName: [{ required: true, message: "请输入员工姓名", trigger: "blur" }],
relatedEmployeeId: [{ required: true, message: "请选择关联员工", trigger: "change" }],
newRelatedEmployeeName: [{ required: true, message: "请输入关联员工姓名", trigger: "blur" }],
relativeId: [{ required: true, message: "请选择家属", trigger: "change" }],
newRelativeName: [{ required: true, message: "请输入家属姓名", trigger: "blur" }],
relationShip: [{ required: true, message: "请选择亲属关系", trigger: "change" }],
age: [{ type: "number", message: "请输入合法年龄", trigger: "blur" }],
weight: [{ type: "number", message: "请输入合法体重", trigger: "blur" }],
height: [{ type: "number", message: "请输入合法身高", trigger: "blur" }]
});
// 7. 初始化逻辑(监听弹窗显示/隐藏)
watch(
() => props.visible,
async val => {
if (val) {
await fetchRelationTypes(); // 加载亲属关系
if (props.prescriptionId) {
await fetchPrescriptionDetail(); // 编辑/详情:加载已有数据
} else {
resetForm(); // 新增:重置表单
}
} else {
resetForm(); // 弹窗关闭:重置表单
emit("close");
}
},
{ immediate: true }
);
// 8. 基础接口请求(原有逻辑优化)
// 加载亲属关系
const fetchRelationTypes = async () => {
try {
const res = await prescriptionApi.getRelationTypes();
relationTypes.value = res || [];
} catch (err) {
relationTypes.value = [];
ElMessage.error("加载亲属关系失败");
}
};
// 加载处方详情(编辑/详情场景)
const fetchPrescriptionDetail = async () => {
loading.value = true;
try {
const res = await prescriptionApi.getDetail({ id: props.prescriptionId });
if (!res) return;
// 填充处方信息
prescriptionForm.diagnosisResult = res.diagnosisResult || "";
prescriptionForm.userMedicinaAdvice = res.userMedicinaAdvice || "";
prescriptionForm.visitDate = res.visitDate || new Date().toISOString().slice(0, 16);
// 填充人员信息
const { person } = res;
if (person) {
personForm.id = person.id;
personForm.name = person.name;
personForm.isEmployee = person.EmployeeId === undefined; // 员工:EmployeeId为空
personForm.age = person.age;
personForm.weight = person.weight;
personForm.height = person.height;
personForm.relationShip = person.RelationShip;
// 员工场景:赋值员工ID
if (personForm.isEmployee) {
personForm.employeeId = person.id;
state.employeeOptions = [{ id: person.id, name: person.name }];
}
// 家属场景:赋值关联员工ID和家属ID
else {
personForm.relatedEmployeeId = person.EmployeeId;
personForm.relativeId = person.id;
// 加载关联员工选项
state.relatedEmployeeOptions = [{ id: person.EmployeeId, name: res.employeeName || "" }];
// 加载家属选项
state.relativeOptions = [{ id: person.id, name: person.name }];
}
}
// 填充药材信息(原有逻辑)
herbList.value = (res.herbs || []).map((h: any) => ({
medicinalId: h.medicinalId,
medicinalOptions: [{ id: h.medicinalId, name: h.medicinalName }],
keyword: h.medicinalName,
showNewInput: false,
newName: "",
amountG: h.amountG,
note: h.note || ""
}));
} catch (err) {
ElMessage.error("加载处方详情失败");
} finally {
loading.value = false;
}
};
// 9. 员工场景交互逻辑
// 员工下拉搜索
const handleEmployeeSearch = async (keyword: string) => {
state.employeeKeyword = keyword.trim();
if (!keyword) {
state.employeeOptions = [];
return;
}
// 调用接口查询员工(personType=1表示员工)
const res = await prescriptionApi.getPersonList({ personType: 1, keyword });
state.employeeOptions = res || [];
};
// 员工下拉选择(含"未找到"逻辑)
const handleEmployeeChange = async (val: number) => {
if (val === 0) {
// 点击"未找到":隐藏下拉,显示新增文本框
state.showEmployeeSelect = false;
personForm.employeeId = undefined; // 清空下拉值
personForm.newEmployeeName = state.employeeKeyword; // 预填搜索关键词
} else {
// 选择已有员工:显示下拉,清空新增文本框
state.showEmployeeSelect = true;
personForm.newEmployeeName = "";
// 赋值人员姓名
const selectedEmp = state.employeeOptions.find(emp => emp.id === val);
if (selectedEmp) personForm.name = selectedEmp.name;
}
};
// 10. 家属场景交互逻辑
// 关联员工下拉搜索
const handleRelatedEmployeeSearch = async (keyword: string) => {
state.relatedEmployeeKeyword = keyword.trim();
if (!keyword) {
state.relatedEmployeeOptions = [];
return;
}
// 调用接口查询员工(personType=1表示员工)
const res = await prescriptionApi.getPersonList({ personType: 1, keyword });
state.relatedEmployeeOptions = res || [];
};
// 关联员工下拉选择(含"未找到"逻辑)
const handleRelatedEmployeeChange = async (val: number) => {
if (val === 0) {
// 点击"未找到":隐藏关联员工下拉,显示新增文本框+家属新增文本框
state.showRelatedEmployeeSelect = false;
state.showRelativeSelect = false; // 隐藏家属下拉
personForm.relatedEmployeeId = undefined;
personForm.newRelatedEmployeeName = state.relatedEmployeeKeyword;
personForm.relativeId = undefined;
} else {
// 选择已有关联员工:显示关联员工下拉,加载家属列表
state.showRelatedEmployeeSelect = true;
state.showRelativeSelect = true; // 显示家属下拉
personForm.newRelatedEmployeeName = "";
personForm.newRelativeName = "";
// 加载该员工的家属(personType=2表示家属,传employeeId)
await handleLoadRelatives(val);
}
};
// 加载关联员工的家属列表
const handleLoadRelatives = async (employeeId: number) => {
try {
const res = await prescriptionApi.getPersonList({
personType: 2, // 假设personType=2表示家属
employeeId
});
state.relativeOptions = res || [];
} catch (err) {
state.relativeOptions = [];
ElMessage.error("加载家属列表失败");
}
};
// 家属下拉搜索
const handleRelativeSearch = async (keyword: string) => {
state.relativeKeyword = keyword.trim();
if (!keyword || !personForm.relatedEmployeeId) {
state.relativeOptions = [];
return;
}
// 调用接口查询家属(传关联员工ID)
const res = await prescriptionApi.getPersonList({
personType: 2,
employeeId: personForm.relatedEmployeeId,
keyword
});
state.relativeOptions = res || [];
};
// 家属下拉选择(含"未找到"逻辑)
const handleRelativeChange = async (val: number) => {
if (val === 0) {
// 点击"未找到":隐藏家属下拉,显示家属新增文本框
state.showRelativeSelect = false;
personForm.relativeId = undefined;
personForm.newRelativeName = state.relativeKeyword;
} else {
// 选择已有家属:显示家属下拉,清空新增文本框
state.showRelativeSelect = true;
personForm.newRelativeName = "";
// 赋值家属姓名
const selectedRel = state.relativeOptions.find(rel => rel.id === val);
if (selectedRel) personForm.name = selectedRel.name;
}
};
// 11. 人员类型切换逻辑(重置状态)
const handlePersonTypeChange = () => {
// 重置所有状态
state.showEmployeeSelect = true;
state.showRelatedEmployeeSelect = true;
state.showRelativeSelect = true;
state.employeeOptions = [];
state.relatedEmployeeOptions = [];
state.relativeOptions = [];
state.employeeKeyword = "";
state.relatedEmployeeKeyword = "";
state.relativeKeyword = "";
// 重置表单字段
personForm.employeeId = undefined;
personForm.newEmployeeName = "";
personForm.relatedEmployeeId = undefined;
personForm.newRelatedEmployeeName = "";
personForm.relativeId = undefined;
personForm.newRelativeName = "";
personForm.relationShip = undefined;
personForm.name = "";
// 重置表单验证
nextTick(() => {
personFormRef.value?.clearValidate();
});
};
// 12. 药材相关逻辑(保持原有)
const handleAddHerb = () => {
herbList.value.push({
medicinalId: undefined,
medicinalOptions: [],
keyword: "",
showNewInput: false,
newName: "",
amountG: undefined,
note: ""
});
};
const handleDeleteHerb = (index: number) => {
herbList.value.splice(index, 1);
};
const handleHerbSearch = async (keyword: string, index: number) => {
const res = await prescriptionApi.searchMedicinals({ keyword });
herbList.value[index].keyword = keyword;
herbList.value[index].medicinalOptions = res || [];
};
const handleHerbChange = (val: number | 0, index: number) => {
if (val === 0) {
herbList.value[index].showNewInput = true;
herbList.value[index].newName = herbList.value[index].keyword;
} else {
herbList.value[index].showNewInput = false;
herbList.value[index].newName = "";
}
};
// 13. 表单提交/取消逻辑(优化参数组装)
const handleSave = async () => {
// 表单验证
if (!personFormRef.value?.validate()) return;
// 药材验证
if (herbList.value.length === 0) {
return ElMessage.warning("请至少添加一条药材消耗记录");
}
// 诊断结果验证
if (!prescriptionForm.diagnosisResult.trim()) {
return ElMessage.warning("请输入诊断结果");
}
loading.value = true;
try {
// 处理新增员工/家属的情况
let newPersonId: number | undefined = personForm.id;
if (!newPersonId) {
if (personForm.isEmployee) {
// 新增员工
if (state.showEmployeeSelect) {
newPersonId = personForm.employeeId; // 选择已有员工
} else {
// 新增员工:调用addPerson接口
newPersonId = await prescriptionApi.addPerson({
name: personForm.newEmployeeName.trim(),
isEmployee: true
});
}
} else {
// 新增家属:先处理关联员工(如有新增)
let relatedEmpId = personForm.relatedEmployeeId;
if (!state.showRelatedEmployeeSelect) {
// 新增关联员工
relatedEmpId = await prescriptionApi.addPerson({
name: personForm.newRelatedEmployeeName.trim(),
isEmployee: true
});
}
// 新增家属
if (state.showRelativeSelect) {
newPersonId = personForm.relativeId; // 选择已有家属
} else {
// 新增家属:调用addPerson接口
newPersonId = await prescriptionApi.addPerson({
name: personForm.newRelativeName.trim(),
isEmployee: false,
EmployeeId: relatedEmpId,
RelationShip: personForm.relationShip
});
}
}
}
// 组装药材参数
const herbParams = herbList.value.map(h => ({
medicinalId: h.medicinalId,
amountG: h.amountG,
note: h.note?.trim()
}));
// 最终提交参数
const submitParams = {
id: props.prescriptionId || undefined,
PersonId: newPersonId, // 人员ID(新增/编辑后的值)
diagnosisResult: prescriptionForm.diagnosisResult.trim(),
userMedicinaAdvice: prescriptionForm.userMedicinaAdvice.trim(),
visitDate: prescriptionForm.visitDate,
herbs: herbParams
};
// 提交保存
await prescriptionApi.save(submitParams);
ElMessage.success(props.prescriptionId ? "修改成功" : "新增成功");
emit("update:visible", false);
emit("refreshList");
} catch (err: any) {
ElMessage.error(err.message || "保存失败");
} finally {
loading.value = false;
}
};
const handleCancel = () => {
if (props.isEdit) {
ElMessageBox.confirm("是否放弃未保存的修改?", "确认取消", { type: "warning" })
.then(() => emit("update:visible", false))
.catch(); // 无逻辑时可省略函数体
} else {
emit("update:visible", false);
}
};
const handleUpdateVisible = (newVisible: boolean) => {
emit("update:visible", newVisible);
};
// 重置表单
const resetForm = () => {
// 重置处方表单
prescriptionForm.diagnosisResult = "";
prescriptionForm.userMedicinaAdvice = "";
prescriptionForm.visitDate = new Date().toISOString().slice(0, 16);
// 重置人员表单
Object.assign(personForm, {
id: undefined,
name: "",
isEmployee: true,
employeeId: undefined,
newEmployeeName: "",
relatedEmployeeId: undefined,
newRelatedEmployeeName: "",
relativeId: undefined,
newRelativeName: "",
relationShip: undefined,
age: undefined,
weight: undefined,
height: undefined
});
// 重置状态
Object.assign(state, {
showEmployeeSelect: true,
employeeOptions: [],
employeeKeyword: "",
showRelatedEmployeeSelect: true,
relatedEmployeeOptions: [],
relatedEmployeeKeyword: "",
showRelativeSelect: true,
relativeOptions: [],
relativeKeyword: ""
});
// 重置药材列表
herbList.value = [];
// 重置表单验证
nextTick(() => {
personFormRef.value?.clearValidate();
});
};
</script>
<style scoped>
.flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
}
.gap-4 {
gap: 16px;
}
.flex-1 {
flex: 1;
}
.el-form-item {
margin-bottom: 20px;
}
</style>
@@ -0,0 +1,103 @@
<template>
<div class="table-box">
<el-card class="mt-4">
<ProTable ref="proTable" title="处方列表" :columns="columns" :request-api="getPrescriptionListApi" :row-key="rowKey">
<template #tableHeader>
<s-button suffix="处方" type="success" @click="handleAddPrescription" />
</template>
<template #operation="scope">
<s-button link :opt="FormOptEnum.VIEW" size="small" @click="handleViewDetail(scope.row.id)"> 查看详情 </s-button>
<s-button link :opt="FormOptEnum.EDIT" size="small" class="ml-2" @click="handleEdit(scope.row.id)" v-if="scope.row.canEdit">
编辑
</s-button>
</template>
</ProTable>
</el-card>
<!-- 弹窗:查看/编辑处方 -->
<prescription-detail-dialog
:visible="showDetailDialog"
:prescription-id="currentPrescriptionId"
:is-edit="isEdit"
@update:visible="val => (showDetailDialog = val)"
@refresh-list="handleRefresh"
/>
</div>
</template>
<script setup lang="ts" name="prescriptionModule">
import { ref } from "vue";
import ProTable from "@/components/ProTable/index.vue";
import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import { prescriptionApi } from "@/api";
import PrescriptionDetailDialog from "./components/PrescriptionDetailDialog.vue";
import { FormOptEnum } from "@/enums";
// ProTable ref
const proTable = ref<ProTableInstance | null>(null);
// 弹窗状态
const showDetailDialog = ref(false);
const currentPrescriptionId = ref<number | null>(null);
const isEdit = ref(false);
// 表格列配置
const columns: ColumnProps<any>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "personName", label: "就诊人", search: { el: "input" } },
{
prop: "personType",
label: "就诊人类型",
formatter: (row: any) => (row.personType === 1 ? "公司员工" : `员工家属(${row.employeeName || "未知"})`)
},
{ prop: "visitDate", label: "就诊时间" },
{ prop: "operation", label: "操作", width: 180, fixed: "right" }
];
// rowKey定义
const rowKey = (row: any) => row.id;
// 请求接口
const getPrescriptionListApi = (params: any) => {
return prescriptionApi.getList(params);
};
// 新增处方
const handleAddPrescription = () => {
currentPrescriptionId.value = null;
isEdit.value = true;
showDetailDialog.value = true;
};
// 查看详情
const handleViewDetail = (id: number) => {
currentPrescriptionId.value = id;
isEdit.value = false;
showDetailDialog.value = true;
};
// 编辑
const handleEdit = (id: number) => {
currentPrescriptionId.value = id;
isEdit.value = true;
showDetailDialog.value = true;
};
// 刷新表格
const handleRefresh = () => {
proTable.value?.refresh?.();
};
</script>
<style scoped lang="scss">
.table-box {
padding-bottom: 24px;
}
.mt-4 {
margin-top: 16px;
}
.ml-2 {
margin-left: 8px;
}
</style>
@@ -0,0 +1,53 @@
<!--
* @Description: 柱状图
* @Author: huguodong
* @Date: 2023-12-15 15:44:12
!-->
<template>
<div id="columnChat" class="h-150px"></div>
</template>
<script setup lang="ts" name="columnChart">
import { OpLog, opLogApi } from "@/api";
import { Column } from "@antv/g2plot";
onMounted(() => {
opLogApi.columnChart().then(res => {
createColumnChat(res.data);
});
});
/**
* 创建折线图,具体文档参考https://g2plot.antv.antgroup.com/api/plots/column
* @param data 数据
*/
function createColumnChat(data: OpLog.ColumnChart[]) {
const column = new Column("columnChat", {
data,
isGroup: true,
xField: "date",
yField: "count",
seriesField: "name",
/** 设置颜色 */
color: ["#409EFF", "#F5222D"],
/** 设置间距 */
// marginRatio: 0.1,
label: {
// 可手动配置 label 数据标签位置
position: "middle", // 'top', 'middle', 'bottom'
// 可配置附加的布局方法
layout: [
// 柱形图数据标签位置自动调整
{ type: "interval-adjust-position" },
// 数据标签防遮挡
{ type: "interval-hide-overlap" },
// 数据标签文颜色自动调整
{ type: "adjust-color" }
]
}
});
column.render(); //渲染
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,99 @@
<!--
* @Description: 详情
* @Author: huguodong
* @Date: 2023-12-15 15:44:16
!-->
<template>
<form-container v-model="visible" title="日志详情" form-size="650px">
<el-descriptions :column="1" border class="mb-2">
<el-descriptions-item label="名称">{{ logInfo.name }}</el-descriptions-item>
<el-descriptions-item label="IP地址">{{ logInfo.opIp }}</el-descriptions-item>
<el-descriptions-item label="地址">{{ logInfo.opAddress }}</el-descriptions-item>
<el-descriptions-item label="请求地址">{{ logInfo.reqUrl }}</el-descriptions-item>
<el-descriptions-item label="操作类">{{ logInfo.className }}</el-descriptions-item>
<el-descriptions-item label="操作方法">{{ logInfo.methodName }}</el-descriptions-item>
<el-descriptions-item label="浏览器">{{ logInfo.opBrowser }}</el-descriptions-item>
<el-descriptions-item label="设备">{{ logInfo.opOs }}</el-descriptions-item>
<el-descriptions-item label="时间">{{ logInfo.opTime }}</el-descriptions-item>
<el-descriptions-item label="用户">{{ logInfo.opUser }}</el-descriptions-item>
<el-descriptions-item label="账号">{{ logInfo.opAccount }}</el-descriptions-item>
</el-descriptions>
<el-space direction="vertical" class="mb-2 w-full">
<span>请求参数:</span>
<code-high-light language="JSON" class="w-600px" :code="paramJson" />
</el-space>
<el-space direction="vertical" class="mb-2" style="width: 100%">
返回结果:
<code-high-light autodetect class="w-600px" :code="resultJson"></code-high-light>
</el-space>
<template #footer>
<el-button type="primary" @click="onClose"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts" name="detail">
import { OpLog, opLogApi } from "@/api";
const visible = ref(false); //是否显示表单
// 表单参数
const logInfo = ref<OpLog.OpLogInfo>({
name: "",
opIp: "",
opAddress: "",
opBrowser: "",
opOs: "",
opTime: "",
opUser: "",
opAccount: "",
id: 0,
category: "",
exeStatus: "",
createTime: "",
exeMessage: null,
className: "",
methodName: "",
reqMethod: "",
reqUrl: "",
paramJson: "",
resultJson: ""
});
const paramJson = ref<string>();
const resultJson = ref<string>();
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(record: OpLog.OpLogInfo) {
logInfo.value = record;
visible.value = true; //显示表单
opLogApi.detail({ id: record.id }).then(res => {
logInfo.value = res.data;
if (res.data.paramJson) paramJson.value = JSON.stringify(JSON.parse(res.data.paramJson), null, 2);
if (res.data.exeMessage) {
resultJson.value = res.data.exeMessage;
} else {
if (res.data.resultJson) resultJson.value = JSON.stringify(JSON.parse(res.data.resultJson), null, 2);
}
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped>
:deep(.el-space__item) {
width: 100% !important;
}
</style>
@@ -0,0 +1,47 @@
<!--
* @Description: 饼状图
* @Author: huguodong
* @Date: 2023-12-15 15:44:22
!-->
<template>
<div id="pieChat" class="h-150px"></div>
</template>
<script setup lang="ts" name="pieChart">
import { OpLog, opLogApi } from "@/api";
import { Pie } from "@antv/g2plot";
onMounted(() => {
opLogApi.pieChart().then(res => {
createPieChat(res.data);
});
});
/**
* 创建折线图,具体文档参考https://g2plot.antv.antgroup.com/api/plots/pie
* @param data 数据
*/
function createPieChat(data: OpLog.PineChart[]) {
const piePlot = new Pie("pieChat", {
appendPadding: 10,
data,
angleField: "value",
colorField: "type",
radius: 0.9,
color: ["#409EFF", "#F5222D"],
label: {
type: "inner",
offset: "-30%",
content: ({ percent }) => `${(percent * 100).toFixed(0)}%`,
style: {
fontSize: 14,
textAlign: "center"
}
},
interactions: [{ type: "element-active" }]
});
piePlot.render(); //渲染
}
</script>
<style scoped lang="scss"></style>
+127
View File
@@ -0,0 +1,127 @@
<!--
* @Description: 操作日志
* @Author: huguodong
* @Date: 2023-12-15 15:44:27
!-->
<template>
<div>
<el-row :gutter="10" class="mb-2">
<el-col :span="16">
<el-card header="周统计" shadow="never">
<column-chat />
</el-card>
</el-col>
<el-col :span="8">
<el-card header="总比例" shadow="never">
<pie-chat />
</el-card>
</el-col>
</el-row>
<el-row>
<div class="table-box min-h-400px">
<ProTable
ref="proTable"
title="系统字典"
class="table"
:tool-button="false"
:columns="columns"
:request-api="opLogApi.page"
:init-param="initParam"
>
<!-- 表格 header 按钮 -->
<template #tableHeader>
<el-space>
<el-radio-group v-model="initParam.category" class="mb-15px">
<el-radio-button v-for="(item, index) in visLogTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button>
</el-radio-group>
<el-input v-model="initParam.searchKey" placeholder="请输入字典名称" class="mb-15px">
<template #append>
<el-button :icon="Search" class="el-input-button" />
</template>
</el-input>
<s-button type="danger" plain :opt="FormOptEnum.DELETE" @click="onDelete('清空当前日志')">清空</s-button>
</el-space>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.VIEW" @click="onOpen(scope.row)"> 详情 </s-button>
</template>
</ProTable>
<!-- 日志详情 -->
<Detail ref="detailRef" />
</div>
</el-row>
</div>
</template>
<script setup lang="ts" name="sysOpLog">
import ColumnChat from "./components/columnChat.vue";
import PieChat from "./components/pieChat.vue";
import Detail from "./components/detail.vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { Search } from "@element-plus/icons-vue";
import { OpLog, opLogApi } from "@/api";
import { FormOptEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
// 左侧表格初始化条件
interface InitParam {
category?: string; //字典分类
searchKey?: string; //关键字
}
// 访问日志类型
let visLogTypeOptions = [
{
label: "操作日志",
value: "OPERATE"
},
{
label: "异常日志",
value: "EXCEPTION"
}
];
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ category: visLogTypeOptions[0].value }); //主表格初始化参数
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<OpLog.OpLogInfo>[] = [
{ prop: "name", label: "名称" },
{ prop: "opIp", label: "IP地址" },
{ prop: "opAddress", label: "地址" },
{ prop: "className", label: "类名称" },
{ prop: "methodName", label: "方法名称" },
{ prop: "opUser", label: "用户" },
{ prop: "opAccount", label: "账号" },
{ prop: "opTime", label: "操作时间" },
{ prop: "operation", label: "操作", fixed: "right", width: 100 }
];
const detailRef = ref<InstanceType<typeof Detail> | null>(null); //详情引用
/**
* 打开表单
* @param record 记录
*/
function onOpen(record: OpLog.OpLogInfo) {
detailRef.value?.onOpen(record);
}
/**
* 清空日志
*/
async function onDelete(msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(opLogApi.delete, initParam.category, msg);
proTable.value?.refresh(); //刷新主表格
}
</script>
<style lang="scss" scoped>
:deep(.el-card__header) {
padding: 9px;
}
</style>
@@ -0,0 +1,65 @@
<!--
* @Description: 详情
* @Author: huguodong
* @Date: 2023-12-15 15:44:38
!-->
<template>
<form-container v-model="visible" title="日志详情" form-size="600px">
<el-descriptions :column="1" border class="mb-2">
<el-descriptions-item label="名称">{{ logInfo.name }}</el-descriptions-item>
<el-descriptions-item label="IP地址">{{ logInfo.opIp }}</el-descriptions-item>
<el-descriptions-item label="地址">{{ logInfo.opAddress }}</el-descriptions-item>
<el-descriptions-item label="浏览器">{{ logInfo.opBrowser }}</el-descriptions-item>
<el-descriptions-item label="设备">{{ logInfo.opOs }}</el-descriptions-item>
<el-descriptions-item label="时间">{{ logInfo.opTime }}</el-descriptions-item>
<el-descriptions-item label="用户">{{ logInfo.opUser }}</el-descriptions-item>
<el-descriptions-item label="账号">{{ logInfo.opAccount }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button type="primary" @click="onClose"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts" name="detail">
import { VisLog } from "@/api";
const visible = ref(false); //是否显示表单
// 表单参数
const logInfo = ref<VisLog.VisLogInfo>({
name: "",
opIp: "",
opAddress: "",
opBrowser: "",
opOs: "",
opTime: "",
opUser: "",
opAccount: "",
id: 0,
category: "",
exeStatus: "",
createTime: ""
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(record: VisLog.VisLogInfo) {
logInfo.value = record;
visible.value = true; //显示表单
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,74 @@
<!--
* @Description: 折线图
* @Author: huguodong
* @Date: 2023-12-15 15:44:42
!-->
<template>
<div id="lineChat" class="h-150px"></div>
</template>
<script setup lang="ts" name="lineChart">
import { VisLog, visLogApi } from "@/api";
import { Line } from "@antv/g2plot";
const seriesField = "series"; //分组字段
const yField = "value"; //图形在 y 方向对应的数据字段名,一般是纵向的坐标轴对应的字段
type alias = "用户登录" | "用户登出"; //别名
// lineMeta 用于处理数据,将数据中的字段名转换为别名
interface LineMeta {
[key: string]: {
alias: alias;
};
}
const lineMeta: LineMeta = {
loginCount: {
alias: "用户登录"
},
logoutCount: {
alias: "用户登出"
}
};
onMounted(() => {
visLogApi.lineChart().then(res => {
createLineChat(res.data);
});
});
/**
* 创建折线图,具体文档参考https://g2plot.antv.antgroup.com/api/plots/line
* @param data 数据
*/
function createLineChat(data: VisLog.LineChart[]) {
const line = new Line("lineChat", {
data: processData(data, lineMeta),
padding: "auto",
xField: "date",
yField: yField,
seriesField: seriesField,
color: ["#409EFF", "rgb(188, 189, 190)"],
appendPadding: [0, 8, 0, 0]
});
line.render(); //渲染
}
/**
* 处理数据,将数据中的字段名转换为别名
* @param data 数据
* @param yFields y轴字段
* @param meta 别名
*/
function processData(data: VisLog.LineChart[], meta: LineMeta) {
const result: any = [];
//将数据中的字段名转换为别名
data.forEach((item: VisLog.LineChart) => {
result.push({ ...item, [seriesField]: meta.loginCount?.alias, [yField]: item.loginCount });
result.push({ ...item, [seriesField]: meta.logoutCount?.alias, [yField]: item.logoutCount });
});
return result;
}
</script>
<style scoped lang="scss"></style>
@@ -0,0 +1,47 @@
<!--
* @Description: 饼状图
* @Author: huguodong
* @Date: 2023-12-15 15:44:47
!-->
<template>
<div id="pieChat" class="h-150px"></div>
</template>
<script setup lang="ts" name="pieChart">
import { VisLog, visLogApi } from "@/api";
import { Pie } from "@antv/g2plot";
onMounted(() => {
visLogApi.pieChart().then(res => {
createPieChat(res.data);
});
});
/**
* 创建折线图,具体文档参考https://g2plot.antv.antgroup.com/api/plots/pie
* @param data 数据
*/
function createPieChat(data: VisLog.PineChart[]) {
const piePlot = new Pie("pieChat", {
appendPadding: 10,
data,
angleField: "value",
colorField: "type",
radius: 0.9,
color: ["#409EFF", "rgb(188, 189, 190)"],
label: {
type: "inner",
offset: "-30%",
content: ({ percent }) => `${(percent * 100).toFixed(0)}%`,
style: {
fontSize: 14,
textAlign: "center"
}
},
interactions: [{ type: "element-active" }]
});
piePlot.render(); //渲染
}
</script>
<style scoped lang="scss"></style>
+127
View File
@@ -0,0 +1,127 @@
<!--
* @Description: 访问日志
* @Author: huguodong
* @Date: 2023-12-15 15:44:33
!-->
<template>
<div>
<el-row :gutter="10" class="mb-2">
<el-col :span="16">
<el-card header="周统计" shadow="never">
<line-chat />
</el-card>
</el-col>
<el-col :span="8">
<el-card header="总比例" shadow="never">
<pie-chat />
</el-card>
</el-col>
</el-row>
<el-row>
<div class="table-box min-h-400px">
<ProTable
ref="proTable"
title="系统字典"
class="table"
:tool-button="false"
:columns="columns"
:request-api="visLogApi.page"
:init-param="initParam"
>
<!-- 表格 header 按钮 -->
<template #tableHeader>
<el-space>
<el-radio-group v-model="initParam.category" class="mb-15px">
<el-radio-button v-for="(item, index) in visLogTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button>
</el-radio-group>
<el-input v-model="initParam.searchKey" placeholder="请输入字典名称" class="mb-15px">
<template #append>
<el-button :icon="Search" class="el-input-button" />
</template>
</el-input>
<s-button type="danger" plain :opt="FormOptEnum.DELETE" @click="onDelete('清空当前日志')">清空</s-button>
</el-space>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.VIEW" @click="onOpen(scope.row)"> 详情 </s-button>
</template>
</ProTable>
<!-- 日志详情 -->
<Detail ref="detailRef" />
</div>
</el-row>
</div>
</template>
<script setup lang="ts" name="sysVisLog">
import LineChat from "./components/lineChat.vue";
import PieChat from "./components/pieChat.vue";
import Detail from "./components/detail.vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { Search } from "@element-plus/icons-vue";
import { VisLog, visLogApi } from "@/api";
import { FormOptEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
// 左侧表格初始化条件
interface InitParam {
category?: string; //字典分类
searchKey?: string; //关键字
}
// 访问日志类型
let visLogTypeOptions = [
{
label: "登录日志",
value: "LOGIN"
},
{
label: "登出日志",
value: "LOGOUT"
}
];
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ category: visLogTypeOptions[0].value }); //主表格初始化参数
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<VisLog.VisLogInfo>[] = [
{ prop: "name", label: "名称" },
{ prop: "opIp", label: "IP地址" },
{ prop: "opAddress", label: "地址" },
{ prop: "opBrowser", label: "浏览器" },
{ prop: "opOs", label: "操作系统" },
{ prop: "opUser", label: "用户" },
{ prop: "opAccount", label: "账号" },
{ prop: "opTime", label: "操作时间" },
{ prop: "operation", label: "操作", fixed: "right", width: 100 }
];
const detailRef = ref<InstanceType<typeof Detail> | null>(null); //详情引用
/**
* 打开表单
* @param record 记录
*/
function onOpen(record: VisLog.VisLogInfo) {
detailRef.value?.onOpen(record);
}
/**
* 清空日志
*/
async function onDelete(msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(visLogApi.delete, initParam.category, msg);
proTable.value?.refresh(); //刷新主表格
}
</script>
<style lang="scss" scoped>
:deep(.el-card__header) {
padding: 9px;
}
</style>
+538
View File
@@ -0,0 +1,538 @@
<!--
* @Description: 基础信息
* @Author: huguodong
* @Date: 2025-01-08 16:05:34
!-->
<template>
<el-form ref="basicFormRef" :rules="rules" :model="basicProps.record" label-width="auto" label-suffix=" :" label-position="top">
<el-card :bordered="true" header="基本设置">
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="选择主表" prop="dbTable">
<s-select v-model="basicProps.record.dbTable" :options="basicOptions.tableOptions" filterable @change="formFieldAssign" />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="表前缀移除" prop="tablePrefix">
<s-radio-group v-model="basicProps.record.tablePrefix" :options="genConst.tablePrefixOptions" @change="tablePrefixChange">
</s-radio-group>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="生成方式" prop="generateType">
<s-radio-group v-model="basicProps.record.generateType" :options="genConst.generateTypeOptions"> </s-radio-group>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="选择生成模板" prop="moduleType" :popover="{ title: '生成模板说明', placement: 'top-start', width: '500' }">
<template #popover>
<p>单表:生成普通单表的增删改查</p>
<p>树表-列表型:生成树型结构的表的增删改查,参考组织管理</p>
<p>树表-树型:生成树型结构的表的增删改查,参考菜单管理</p>
<p>主子表:生成主表关联子表的增删改查,删除主表数据会清除相应的子表数据</p>
</template>
<s-select v-model="basicProps.record.moduleType" :options="genConst.moduleTypeOptions" />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="数据权限" prop="dataPermission">
<s-radio-group v-model="basicProps.record.dataPermission" :options="genConst.dataPermissionOptions"></s-radio-group>
</s-form-item>
</el-col>
<el-col :span="4">
<s-form-item label="功能名" prop="functionName">
<s-input v-model="basicProps.record.functionName" allow-clear />
</s-form-item>
</el-col>
<el-col :span="4">
<s-form-item label="后缀" prop="functionNameSuffix">
<s-input v-model="basicProps.record.functionNameSuffix" allow-clear />
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="功能列表" prop="funcList">
<s-select v-model="basicProps.record.funcList" multiple :options="genConst.funcOptions" />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="作者" prop="authorName">
<s-input v-model="basicProps.record.authorName" allow-clear />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="basicProps.record.sortCode" :max="100" style="width: 100%" />
</s-form-item>
</el-col>
</el-row>
<el-divider v-if="showTree" content-position="left">树表配置</el-divider>
<el-row v-if="showTree" :gutter="24">
<el-col :span="8">
<s-form-item label="树ID字段" prop="treeId">
<s-select v-model="basicProps.record.treeId" :options="basicOptions.columnOptions" filterable />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="树父ID字段" prop="treePid">
<s-select v-model="basicProps.record.treeId" :options="basicOptions.columnOptions" filterable />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="树名称字段" prop="treeName">
<s-select v-model="basicProps.record.treeId" :options="basicOptions.columnOptions" filterable />
</s-form-item>
</el-col>
</el-row>
<el-divider v-if="showMaster" content-position="left">主子表配置</el-divider>
<el-row v-if="showMaster" :gutter="24">
<el-col :span="8">
<s-form-item label="关联子表的表名" prop="childTable">
<s-select v-model="basicProps.record.childTable" :options="basicOptions.childTableList" filterable @change="selectChildTable" />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="树ID字段" prop="childFk">
<s-select v-model="basicProps.record.childFk" :options="basicOptions.childColumnOptions" filterable />
</s-form-item>
</el-col>
</el-row>
</el-card>
<el-card :bordered="true" header="前端设置" class="mt-2">
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="前端文件夹路径" prop="frontedPath">
<s-input v-model="basicProps.record.frontedPath" allow-clear />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="所属模块" prop="module">
<s-select
v-model="basicProps.record.module"
:options="basicOptions.moduleOptions"
filterable
@change="moduleChange(basicProps.record.module as number, false)"
/>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="上级目录" prop="menuPid">
<MenuSelector
v-model:menu-value="basicProps.record.menuPid"
:check-strictly="true"
:menu-tree-data="basicOptions.menuTreeData"
:show-all="true"
:only-catalog="true"
@change="changeMenu"
/>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="表单布局" prop="formLayout">
<s-radio-group v-model="basicProps.record.formLayout" :options="genConst.formLayoutOptions"></s-radio-group>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="使用栅格" prop="gridWhether">
<s-radio-group v-model="basicProps.record.gridWhether" :options="genConst.gridWhetherOptions"></s-radio-group>
</s-form-item>
</el-col>
<el-col :span="8" v-if="basicProps.record.moduleType === genConst.moduleTypeOptions[0].value">
<s-form-item label="左侧树" prop="leftTree">
<s-select v-model="basicProps.record.leftTree" :options="genConst.treeOptions" filterable />
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="路由名(最终路由名是:路由名+业务名)" prop="routeName">
<s-input v-model="basicProps.record.routeName" allow-clear placeholder="根据上级目录自动生成,顶级需手动填写" />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="图标" prop="icon">
<SelectIconPlus v-if="showIcon" v-model:icon-value="basicProps.record.icon" />
</s-form-item>
</el-col>
</el-row>
</el-card>
<el-card :bordered="true" header="后端设置" class="mt-2">
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="业务名(最好和文件夹路径对应)" prop="busName">
<s-input v-model="basicProps.record.busName" allow-clear />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="类名" prop="className">
<s-input v-model="basicProps.record.className" allow-clear />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="服务层代码位置" prop="servicePosition">
<el-input style="max-width: 600px" v-model="basicProps.record.serviceDictionary" placeholder="请输入文件夹">
<template #prepend>
<s-select style="width: 250px" v-model="basicProps.record.servicePosition" :options="basicOptions.servicePositions" filterable />
</template>
</el-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<s-form-item label="接口层代码位置" prop="controllerPosition">
<el-input style="max-width: 600px" v-model="basicProps.record.controllerDictionary" placeholder="请输入文件夹">
<template #prepend>
<s-select style="width: 250px" v-model="basicProps.record.controllerPosition" :options="basicOptions.servicePositions" filterable />
</template>
</el-input>
</s-form-item>
</el-col>
</el-row>
</el-card>
</el-form>
</template>
<script setup lang="ts">
import { GenCode, genBasicApi, userCenterApi } from "@/api";
import { FormOptEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { useUserStore } from "@/stores/modules";
import * as genConst from "../genConst";
import { FormInstance } from "element-plus";
import { MenuCategoryEnum } from "@/enums";
const userStore = useUserStore();
const basicFormRef = ref<FormInstance>();
const showIcon = ref(false);
// 选项接口
interface BasicOptions {
/*模块选项 */
moduleOptions: { label: string; value: string | number }[];
/*服务列表 */
servicePositions: { label: string; value: string | number }[];
/*控制器选项 */
controllerPositions: { label: string; value: string | number }[];
/*数据表列表 */
tableOptions: GenCode.TableList[];
/*字段选项 */
columnOptions: { label: string; value: string }[];
/*子表字段 */
childColumnOptions: { label: string; value: string }[];
/*子表列表 */
childTableList: GenCode.TableList[];
/*菜单选择器数据 */
menuTreeData: any[];
}
// 表单参数
const basicProps = reactive<FormProps.Base<GenCode.GenBasic>>({
opt: FormOptEnum.ADD,
record: {}
});
//显示树表配置
const showTree = computed(() => {
return basicProps.record.moduleType && basicProps.record.moduleType.includes("tree");
});
//显示主子表配置
const showMaster = computed(() => {
return basicProps.record.moduleType && basicProps.record.moduleType.includes("master");
});
//下拉用的数据
const basicOptions: BasicOptions = reactive({
tableOptions: [],
moduleOptions: [],
servicePositions: [],
controllerPositions: [],
columnOptions: [],
childColumnOptions: [],
childTableList: [],
menuTreeData: []
});
// 表单验证规则
const rules = reactive({
tablePrefix: [required("请选择是否移除表前缀")],
dbTable: [required("请选择主表")],
dbTableKey: [required("请选择主表主键")],
generateType: [required("请选择生成方式")],
module: [required("请选择所属模块")],
menuPid: [required("请选择上级目录")],
routeName: [required("请输入路由名")],
icon: [required("请选择图标")],
functionName: [required("请输入功能名")],
busName: [required("请输入业务名")],
className: [required("请输入类名")],
frontedPath: [required("请输入前端路径")],
servicePosition: [required("请输入服务层代码项目")],
serviceDictionary: [required("请输入服务层代码文件夹")],
controllerPosition: [required("请输入接口层代码位置")],
sortCode: [required("请选择排序")],
formLayout: [required("请选择表单布局")],
gridWhether: [required("请选择是否使用栅格")],
authorName: [required("请输入作者名")],
funcList: [required("请选择功能列表")],
moduleType: [required("请输入模版类型")],
dataPermission: [required("请选择数据权限")],
treeId: [required("请选择树Id字段")],
treePid: [required("请选择树父Id字段")],
treeName: [required("请选择树名称字段")],
childTable: [required("请选择关联子表的表名")],
childFk: [required("请选择关联子表的外键")]
});
/**
* 打开表单
* @param record 表单数据
*/
function onOpen(record: GenCode.GenBasic) {
//加载默认的模块
basicOptions.moduleOptions = userStore.moduleListGet.map(item => {
return {
label: item.title,
value: item.id
};
});
//获取程序集
genBasicApi.assemblies().then(res => {
const data = res.data;
basicOptions.servicePositions = data.map(item => {
return {
value: item,
label: item
};
});
basicOptions.controllerPositions = data.map(item => {
return {
value: item,
label: item
};
});
});
// 获取数据库中的所有表
genBasicApi.tables({}).then(res => {
const data = res.data;
basicOptions.tableOptions = data.map(item => {
return {
value: item.tableName,
tableName: item.tableName,
configId: item.configId,
label: `${item.tableDescription}-${item.configId}-${item.entityName}`,
entityName: item.entityName,
tableDescription: item.tableDescription || item.entityName,
tableColumns: item.columns
};
});
if (record && Object.keys(record).length > 0) {
basicProps.record = record;
autoModuleType(record); //自动填写模块类型
moduleChange(record.module, true); //自动填写上级目录
showIcon.value = true;
} else {
basicProps.record = {
sortCode: 99,
authorName: "superAdmin",
tablePrefix: "Y",
servicePosition: "",
serviceDictionary: "Services",
controllerPosition: "",
controllerDictionary: "Controllers/Application",
generateType: "PRO",
functionName: "",
functionNameSuffix: "管理",
formLayout: "vertical",
gridWhether: "N",
leftTree: "null",
moduleType: "single",
dataPermission: "N",
funcList: ["curd"],
icon: ""
};
showIcon.value = true;
}
});
}
/**
* 生成模版字段填充
* @param record 表单数据
*/
function autoModuleType(record) {
//获取表信息
const table = basicOptions.tableOptions.find(item => {
return item.tableName === record.dbTable && item.configId == record.configId;
});
if (table) {
//树ID列表
basicOptions.columnOptions = table.tableColumns.map(item => {
return {
value: item.columnName,
label: `${item.columnName}:${item.columnDescription}`
};
});
//获取子表列表
basicOptions.childTableList = basicOptions.tableOptions.filter(item => item.configId == table.configId);
//获取子表信息
let subTable = basicOptions.childTableList.find(item => {
return item.tableName === record.childTable && item.configId == record.configId;
});
if (subTable) {
//获取子表字段
basicOptions.childColumnOptions = subTable.tableColumns.map(item => {
return {
value: item.columnName,
label: `${item.columnName}:${item.columnDescription}`
};
});
}
}
}
/**
* 表前缀移除
*
*/
function tablePrefixChange() {
const tableName = basicProps.record.dbTable;
if (tableName) {
const tableNameHump = getTableNameToHump(tableName);
basicProps.record.busName = tableNameHump.toLowerCase();
}
}
/**
* 获取表名转驼峰
* @param tableName 表名
*/
function getTableNameToHump(tableName: string) {
if (tableName) {
const arr = tableName.toLowerCase().split("_");
if (basicProps.record.tablePrefix === "Y") {
arr.splice(0, 1);
}
for (let i = 0; i < arr.length; i++) {
// charAt()方法得到第一个字母,slice()得到第二个字母以后的字符串
arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1);
}
return arr.join("");
}
return "";
}
/**
* 获取表名转驼峰
* @param tableName 表名
*/
function getClassName(tableName) {
if (tableName) {
const arr = tableName.toLowerCase().split("_");
for (let i = 0; i < arr.length; i++) {
// charAt()方法得到第一个字母,slice()得到第二个字母以后的字符串
arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1);
}
return arr.join("");
}
return "";
}
/**
* 表单内设置默认的值
* @param value
* @param options
*/
function formFieldAssign(value: string) {
const option = basicOptions.tableOptions.find(item => item.value === value);
if (option) {
const tableNameHump = getTableNameToHump(option.tableName);
basicProps.record.busName = tableNameHump.toLowerCase();
basicProps.record.className = getClassName(option.tableName);
basicProps.record.entityName = option.entityName;
basicProps.record.configId = option.configId;
basicOptions.columnOptions = option.tableColumns.map(item => {
return {
value: item.columnName,
label: `${item.columnName}:${item.columnDescription}`
};
});
basicOptions.childTableList = basicOptions.tableOptions.filter(item => {
return item.configId == option.configId;
});
}
}
/**
* 选择子表
* @param value
*/
function selectChildTable(value: string) {
const option = basicOptions.childTableList.find(item => item.value === value);
if (option) {
basicProps.record.childTable = option.entityName; //子表名实体赋值
basicOptions.childColumnOptions = option.tableColumns.map(item => {
return {
value: item.columnName,
label: `${item["columnName"]}-${item.columnDescription}`
};
});
}
}
// 选择模板的回调
function moduleChange(value: number, assign: boolean) {
if (!assign) {
// 先去掉值
basicProps.record.menuPid = undefined;
}
userCenterApi.getAuthMenuList({ id: value }).then(res => {
console.log("res", res.data);
//去掉单页
const menuTreeData = res.data.filter(item => item.category != MenuCategoryEnum.SPA);
basicOptions.menuTreeData = menuTreeData;
});
}
/**
* 验证并提交数据
*/
function onSubmit(): Promise<GenCode.GenBasic> {
console.log("basic submit");
return new Promise((resolve, reject) => {
basicFormRef.value
?.validate()
.then(() => {
genBasicApi.submitForm(basicProps.record, basicProps.record.id != undefined).then(res => {
resolve(res.data);
});
})
.catch(err => {
reject(err);
});
});
}
/** 选中菜单事件 */
function changeMenu(value: string) {
basicProps.record.routeName = value;
}
// 暴露给父组件的方法
defineExpose({
onOpen,
onSubmit
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,295 @@
<!--
* @Description: 配置步骤
* @Author: huguodong
* @Date: 2025-01-08 16:05:19
!-->
<template>
<el-card :bordered="true">
<ProTable
ref="proTable"
:request-auto="false"
:columns="columns"
:tool-button="false"
:pagination="false"
:request-api="genConfigApi.list"
:init-param="initParam"
:data-callback="dataCallback"
>
<template #fieldRemark="scope">
<s-input v-model="scope.row.fieldRemark" />
</template>
<template #width="scope">
<s-input v-model="scope.row.width" />
</template>
<template #fieldNetType="scope">
<s-select
v-model="scope.row.fieldNetType"
:disabled="toCommonFieldEstimate(scope.row)"
:options="genConst.fieldNetTypeOptions"
filterable
@change="fieldNetTypeChange(scope.row)"
/>
</template>
<template #effectType="scope">
<s-select
v-model="scope.row.effectType"
:disabled="toCommonFieldEstimate(scope.row) || toFieldSelectEstimate(scope.row)"
:options="genConst.effectTypeOptions"
filterable
@change="effectTypeChange(scope.row)"
:class="{ effectType: scope.row.effectType === 'fk' }"
/>
<el-button
@click="fkFormRef?.onOpen(scope.row)"
type="primary"
link
v-if="scope.row.effectType === 'fk'"
class="pl-2 ml-2"
:icon="View"
></el-button>
</template>
<template #dictTypeCode="scope">
<s-select
v-if="scope.row.effectType === 'radio' || scope.row.effectType === 'select' || scope.row.effectType === 'checkbox'"
v-model="scope.row.dictTypeCode"
:options="dictTypeCodeOptions"
filterable
/>
</template>
<template #whetherTable="scope">
<el-checkbox v-model="scope.row.whetherTable" :value="scope.row.whetherTable" />
</template>
<template #whetherRetract="scope">
<el-checkbox v-model="scope.row.whetherRetract" :value="scope.row.whetherRetract" />
</template>
<template #whetherResizable="scope">
<el-checkbox v-model="scope.row.whetherResizable" :value="scope.row.whetherResizable" />
</template>
<template #whetherAddUpdate="scope">
<el-checkbox v-model="scope.row.whetherAddUpdate" :disabled="toFieldEstimate(scope.row)" :value="scope.row.whetherAddUpdate" />
</template>
<template #whetherImportExport="scope">
<el-checkbox v-model="scope.row.whetherImportExport" :disabled="toFieldEstimate(scope.row)" :value="scope.row.whetherAddUpdate" />
</template>
<template #whetherRequired="scope">
<el-checkbox
v-model="scope.row.whetherRequired"
:disabled="toFieldEstimate(scope.row) || !scope.row.whetherAddUpdate"
:value="scope.row.whetherRequired"
/>
</template>
<template #queryWhether="scope">
<el-switch v-model="scope.row.queryWhether" />
</template>
<template #queryType="scope">
<s-select
v-if="scope.row.queryWhether === true && scope.row.effectType !== 'datepicker'"
v-model="scope.row.queryType"
:options="genConst.queryTypeOptions"
filterable
/>
<span v-else-if="scope.row.effectType === 'datepicker' && scope.row.queryWhether === true">时间段</span>
<span v-else>无</span>
</template>
</ProTable>
</el-card>
<FkForm ref="fkFormRef" @successful="proTable?.refresh()" />
</template>
<script setup lang="ts">
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { GenCode, genConfigApi } from "@/api";
import * as genConst from "../genConst";
import { useDictStore } from "@/stores/modules";
import FkForm from "./fkForm.vue";
import { View } from "@element-plus/icons-vue";
const dictStore = useDictStore();
// 初始化参数
const initParam = reactive<{ basicId: string | number }>({ basicId: 0 });
const dictTypeCodeOptions = dictStore.getDictAll();
const fkFormRef = ref<InstanceType<typeof FkForm> | null>(null);
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<GenCode.GenConfig>[] = [
{ type: "sort", label: "排序", width: 60 },
{ prop: "fieldName", label: "字段" },
{ prop: "fieldRemark", label: "注释" },
{ prop: "fieldType", label: "类型", width: 100 },
{ prop: "fieldNetType", label: "实体类型", width: 150 },
{ prop: "effectType", label: "作用类型" },
{ prop: "dictTypeCode", label: "字典", width: 140 },
{ prop: "width", label: "列宽", width: 80 },
{ prop: "whetherTable", label: "列表", width: 60 },
{ prop: "whetherRetract", label: "列省略", width: 60 },
{ prop: "whetherResizable", label: "可伸缩列", width: 60 },
{ prop: "whetherAddUpdate", label: "增改", width: 60 },
{ prop: "whetherImportExport", label: "导入导出", width: 60 },
{ prop: "whetherRequired", label: "必填", width: 60 },
{ prop: "queryWhether", label: "查询", width: 80 },
{ prop: "queryType", label: "查询方式", width: 150, fixed: "right" }
];
/**
* 打开配置步骤
* @param id 基础信息id
*/
function onOpen(id: number | string) {
initParam.basicId = id;
}
// dataCallback 是对于返回的表格数据做处理
function dataCallback(data: any) {
data.forEach(item => {
for (const key in item) {
if (item[key] === "Y") {
item[key] = true;
}
if (item[key] === "N") {
item[key] = false;
}
}
// 去掉删除标识
// 如果是主键,我们不提供主键的配置,也用不到
if (
item.isPrimaryKey ||
item.fieldName.toLowerCase().includes("isdelete") ||
item.fieldName.toLowerCase().includes("createuserid") ||
item.fieldName.toLowerCase().includes("updateuserid")
) {
data = data.filter(table => {
return table.fieldName != item.fieldName;
});
}
// 让默认的变成设置的
fieldNetTypeChange(item);
});
return data;
}
/**
*通用字段是否可选
* @param record
*/
function toCommonFieldEstimate(record: GenCode.GenConfig) {
if (record.fieldName.toLowerCase().includes("createuser") || record.fieldName.toLowerCase().includes("updateuser")) {
return true;
}
return false;
}
/**
*
* @param record 通用字段是否可选
*/
function toFieldSelectEstimate(record) {
if (record.fieldNetType === "DateTime" && record.effectType === "datepicker") {
return true;
}
return false;
}
/**
*
* @param record 改变作用类型
* @param record
*/
function effectTypeChange(record: GenCode.GenConfig) {
console.log("value", record);
if (record.effectType === "fk") {
console.log("value", record);
fkFormRef.value?.onOpen(record);
} else {
record.fkEntityName = "";
record.fkColumnName = "";
record.fkColumnId = "";
}
console.log(record);
}
/**
* 实体类型选择触发
*/
function fieldNetTypeChange(record: GenCode.GenConfig) {
if (record.fieldNetType === "DateTime") {
record.effectType = "datepicker";
}
}
/**
* 通用字段是否可选
* @param data
*/
function toFieldEstimate(data: GenCode.GenConfig) {
if (
data.fieldName.toLowerCase().includes("createuser") ||
data.fieldName.toLowerCase().includes("createtime") ||
data.fieldName.toLowerCase().includes("updateuser") ||
data.fieldName.toLowerCase().includes("updatetime") ||
data.fieldName.toLowerCase().includes("isdelete") ||
data.fieldName.toLowerCase().includes("createuserid") ||
data.fieldName.toLowerCase().includes("updateuserid") ||
data.isPrimaryKey === "true"
) {
return true;
}
return false;
}
/**
* 验证并提交数据
*/
function onSubmit(): Promise<any> {
let data = proTable.value?.tableData as GenCode.GenConfig[];
let error = false;
data.forEach(item => {
// 必填那一项转换
for (const key in item) {
//不转isdeleste
if (key != "isDelete") {
if (item[key] === true) {
item[key] = "Y";
}
if (item[key] === false) {
item[key] = "N";
}
}
}
if (item.queryWhether === "Y" && !item.queryType) {
// 排除掉时间选择
if (item.fieldNetType !== "DateTime" && item.effectType !== "checkbox" && item.fieldNetType !== "Date") {
error = true;
}
}
if ((item.effectType === "select" || item.effectType === "radio" || item.effectType === "checkbox") && !item.dictTypeCode) {
error = true;
}
});
return new Promise((resolve, reject) => {
if (error) {
reject("校验失败,请选择对应的下拉框选项");
return;
}
genConfigApi
.editBatch(data)
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err);
});
});
}
// 暴露方法
defineExpose({ onOpen, onSubmit });
</script>
<style lang="scss" scoped>
.effectType {
width: 75%;
}
</style>
@@ -0,0 +1,124 @@
<!--
* @Description: 外键关系配置
* @Author: huguodong
* @Date: 2025-02-05 11:02:14
!-->
<template>
<form-container v-model="visible" title="外键关系配置" form-size="600px">
<el-form ref="fkFormRef" :model="fkProps.fkInfo" :rules="rules" label-width="auto" label-suffix=" :">
<s-form-item label="选择外键表" prop="fkEntityName">
<s-select v-model="fkProps.fkInfo.fkEntityName" :options="fkProps.tableOptions" filterable @change="formFieldAssign" />
</s-form-item>
<s-form-item label="外键ID" prop="fkColumnId">
<s-select v-model="fkProps.fkInfo.fkColumnId" :options="fkProps.columnOptions" filterable />
</s-form-item>
<s-form-item label="显示字段" prop="fkColumnName">
<s-select v-model="fkProps.fkInfo.fkColumnName" :options="fkProps.columnOptions" filterable />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="onSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { GenCode, genBasicApi } from "@/api";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
const fkFormRef = ref<FormInstance>();
const visible = ref(false); //是否显示表单
// 选项接口
interface FkProps {
/*数据表列表 */
tableOptions: GenCode.TableList[];
/*字段选项 */
columnOptions: { label: string; value: string }[];
/*当前行数据 */
fkInfo: GenCode.GenConfig;
}
// 表单参数
const fkProps: FkProps = reactive({
tableOptions: [],
columnOptions: [],
fkInfo: {} as GenCode.GenConfig
});
// 表单验证规则
const rules = reactive({
fkEntityName: [required("请选择外键表")],
fkColumnId: [required("请选择外键Id")],
fkColumnName: [required("请选择外键字段")]
});
/**
* 打开表单
* @param genConfig 表单参数
*/
function onOpen(genConfig: GenCode.GenConfig) {
visible.value = true; //显示抽屉
console.log("genConfig", genConfig);
fkProps.fkInfo = genConfig;
// 获取数据库中的所有表
genBasicApi.tables({ isAll: true }).then(res => {
const data = res.data;
fkProps.tableOptions = data.map(item => {
return {
value: item.tableName,
tableName: item.tableName,
configId: item.configId,
label: `${item.tableDescription}-${item.configId}-${item.entityName}`,
entityName: item.entityName,
tableDescription: item.tableDescription || item.entityName,
tableColumns: item.columns
};
});
const option = fkProps.tableOptions.find(item => item.value === genConfig.fkEntityName);
if (option) {
fkProps.columnOptions = option.tableColumns.map(item => {
return {
value: item.columnName,
label: `${item.columnName}:${item.columnDescription}`
};
});
}
});
}
/**
* 表单内设置默认的值
* @param value
* @param options
*/
function formFieldAssign(value: string) {
fkProps.fkInfo.fkColumnName = "";
fkProps.fkInfo.fkColumnId = "";
const option = fkProps.tableOptions.find(item => item.value === value);
if (option) {
fkProps.columnOptions = option.tableColumns.map(item => {
return {
value: item.columnName,
label: `${item.columnName}:${item.columnDescription}`
};
});
}
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
/** 提交表单*/
function onSubmit() {
fkFormRef.value?.validate(valid => {
if (!valid) return; //表单验证失败
onClose(); //关闭表单
});
}
defineExpose({ onOpen });
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,114 @@
<!--
* @Description: 预览
* @Author: huguodong
* @Date: 2025-02-10 10:52:26
!-->
<template>
<form-container v-model="visible" title="代码生成预览" form-size="1200px" destroy-on-close>
<el-tabs v-model="previewProps.codeTypeActiveKey" class="-mt-4" @tab-change="onTabChange">
<el-tab-pane
v-for="codeType in previewProps.codeTypeArray"
:key="codeType.codeTypeKey"
:name="codeType.codeTypeKey"
:label="codeType.codeTypeTitle"
>
<div class="tabs-wrapper">
<el-tabs v-model="previewProps.codeListActiveKey" tab-position="left" type="card" style="height: 100%">
<el-tab-pane v-for="pan in codeType.codeTypeList" :key="pan.codeFileName" :name="pan.codeFileName" :label="pan.codeFileName">
<div style="height: calc(100vh - 160px); overflow: auto">
<el-input v-model="pan.codeFileContent" type="textarea" autosize />
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="onClose"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { GenCode, genBasicApi } from "@/api";
import { TabPaneName } from "element-plus";
const visible = ref(false); //是否显示表单
// 选项接口
interface PreviewProps {
/*代码类型选项 */
codeTypeActiveKey: string;
/*代码文件选项 */
codeListActiveKey: string;
/*代码预览数组 */
codeTypeArray: { codeTypeKey: string; codeTypeTitle: string; codeTypeList: GenCode.GenBaseCodeResult[] }[];
}
// 表单参数
const previewProps: PreviewProps = reactive({
codeTypeArray: [],
codeTypeActiveKey: "",
codeListActiveKey: ""
});
/**
* 打开表单
* @param genConfig 表单参数
*/
function onOpen(basic: GenCode.GenBasic) {
visible.value = true; //显示抽屉
console.log("basic", basic);
genBasicApi.previewGen({ id: basic.id }).then(res => {
if (res.data) {
previewProps.codeTypeArray = [
{
codeTypeKey: "frontend",
codeTypeTitle: "前端代码",
codeTypeList: res.data.codeFrontendResults
},
{
codeTypeKey: "backend",
codeTypeTitle: "后端代码",
codeTypeList: res.data.codeBackendResults
},
{
codeTypeKey: "sqlend",
codeTypeTitle: "SQL文件",
codeTypeList: res.data.sqlResults
}
];
previewProps.codeTypeActiveKey = previewProps.codeTypeArray[0].codeTypeKey; //默认选中第一个
previewProps.codeListActiveKey = previewProps.codeTypeArray[0].codeTypeList[0].codeFileName; //默认选中第一个
} else {
ElMessage.warning("预览失败:请检查问题");
}
});
}
/**
* tab切换
* @param key 选中的key
*/
function onTabChange(key: TabPaneName) {
previewProps.codeListActiveKey = previewProps.codeTypeArray.find(f => f.codeTypeKey === key)?.codeTypeList[0]?.codeFileName || "";
}
/**
* 关闭表单
*/
function onClose() {
visible.value = false; //关闭抽屉
}
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped>
.tabs-wrapper {
height: calc(100vh - 260px);
overflow: auto;
}
</style>
+133
View File
@@ -0,0 +1,133 @@
<!--
* @Description: 代码生成步骤
* @Author: huguodong
* @Date: 2025-01-08 15:00:51
!-->
<template>
<div>
<el-card class="steps-card" :bordered="false">
<el-row class="xn-row">
<el-col :span="6"></el-col>
<el-col :span="12">
<el-steps align-center finish-status="success" :active="active">
<el-step v-for="item in steps" :key="item.title" :title="item.title" />
</el-steps>
</el-col>
<el-col :span="6">
<div style="float: right">
<el-button :disabled="active === 0" style="margin-right: 8px" @click="prev"> 上一步 </el-button>
<el-button :disabled="active === 2" type="primary" style="margin-left: 8px" @click="next"> 继续 </el-button>
<el-button type="primary" danger ghost style="margin-left: 8px" @click="$emit('closed')"> 关闭 </el-button>
</div>
</el-col>
</el-row>
</el-card>
<div v-if="active === 0">
<Basic ref="basicRef" />
</div>
<div v-if="active === 1">
<Config ref="configRef" />
</div>
</div>
</template>
<script setup lang="ts">
import { GenCode } from "@/api";
import { FormOptEnum } from "@/enums";
import Basic from "./basic.vue";
import Config from "./config.vue";
const active = ref(0);
const emit = defineEmits(["closed"]);
const recordData = ref({}); //记录数据
// 分布步骤数据
const steps = [
{
title: "基础信息",
content: "基础信息"
},
{
title: "详细配置",
content: "详细配置"
}
];
// 表单参数
reactive<FormProps.Base<GenCode.GenBasic>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 获取组件实例
const basicRef = ref<InstanceType<typeof Basic> | null>(null);
const configRef = ref<InstanceType<typeof Config> | null>(null);
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<GenCode.GenBasic>) {
// console.log("record", props.record);
// Object.assign(genBasicProps, props); //合并参数
basicRef.value?.onOpen(props.record as GenCode.GenBasic);
}
/**下一步**/
function next() {
// active.value++;
console.log("active.value", active.value);
// 判断是哪一步
if (active.value === 0) {
basicRef.value?.onSubmit().then(data => {
recordData.value = data;
active.value++;
nextTick(() => {
configRef.value?.onOpen(data.id);
});
});
// active.value--;
}
if (active.value === 1) {
configRef.value?.onSubmit().then(() => {
active.value++;
nextTick(() => {
// emit("closed");
//关闭页面
emit("closed");
});
});
}
}
/**上一步**/
function prev() {
active.value--;
if (active.value === 0) {
nextTick(() => {
basicRef.value!.onOpen(recordData.value as GenCode.GenBasic);
});
}
if (active.value === 1) {
nextTick(() => {
// configRef.value.onOpen(recordData.value);
});
}
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped>
.steps-card {
padding-top: -10px;
margin: -12px -12px 10px;
}
.gen-row {
margin-top: -10px;
margin-bottom: -10px;
}
</style>
+262
View File
@@ -0,0 +1,262 @@
//生成选项
export const execTypeOptions = [
{
label: "全部",
value: "ALL"
},
{
label: "前端",
value: "FRONTEND"
},
{
label: "后端",
value: "BACKEND"
}
];
//生成方式
export const generateTypeOptions = [
{
label: "项目内",
value: "PRO"
},
{
label: "压缩包",
value: "ZIP"
}
];
//表前缀
export const tablePrefixOptions = [
{
label: "移除",
value: "Y"
},
{
label: "不移除",
value: "N"
}
];
//表单布局
export const formLayoutOptions = [
{
label: "垂直",
value: "vertical"
},
{
label: "水平",
value: "horizontal"
}
];
//使用格栅
export const gridWhetherOptions = [
{
label: "栅格布局",
value: "Y"
},
{
label: "不使用",
value: "N"
}
];
//数据权限
export const dataPermissionOptions = [
{
label: "有",
value: "Y"
},
{
label: "无",
value: "N"
}
];
//生成模版
export const moduleTypeOptions = [
{
label: "单表",
value: "single"
},
{
label: "树表-列表型",
value: "treeList"
},
{
label: "树表-树型",
value: "treeTree"
},
{
label: "主子表",
value: "masterChild"
}
];
//左侧树
export const treeOptions = [
{
label: "不使用",
value: "null"
},
{
label: "组织树",
value: "org"
}
];
//功能列表
export const funcOptions = [
{
label: "增删改查",
value: "curd"
}
// {
// label: "批量删除",
// value: "batchDelete"
// },
// {
// label: "导入",
// value: "import"
// },
// {
// label: "导出",
// value: "export"
// },
// {
// label: "批量编辑",
// value: "batchEdit"
// }
];
//作用类型
export const effectTypeOptions = [
{
label: "输入框",
value: "input"
},
{
label: "文本框",
value: "textarea"
},
{
label: "下拉框",
value: "select"
},
{
label: "单选框",
value: "radio"
},
{
label: "复选框",
value: "checkbox"
},
{
label: "日期选择器",
value: "datepicker"
},
{
label: "时间选择器",
value: "timepicker"
},
{
label: "数字输入框",
value: "inputNumber"
},
{
label: "滑动数字条",
value: "slider"
},
{
label: "图片",
value: "image"
},
{
label: "外键",
value: "fk"
}
];
// 实体类型
export const fieldNetTypeOptions = [
{
label: "Byte",
value: "byte"
},
{
label: "Int",
value: "int"
},
{
label: "Long",
value: "long"
},
{
label: "String",
value: "string"
},
{
label: "Bool",
value: "bool"
},
{
label: "Decimal",
value: "decimal"
},
{
label: "Double",
value: "double"
},
{
label: "Date",
value: "Date"
},
{
label: "DateTime",
value: "DateTime"
},
{
label: "Single",
value: "Single"
},
{
label: "Byte[]",
value: "byte[]"
},
{
label: "Guid",
value: "Guid"
}
];
// 查询方式
export const queryTypeOptions = [
{
label: "模糊包含",
value: "like"
},
{
label: "模糊不包含",
value: "notLike"
},
{
label: "等于",
value: "eq"
},
{
label: "不等于",
value: "ne"
},
{
label: "大于",
value: "gt"
},
{
label: "大于等于",
value: "ge"
},
{
label: "小于",
value: "lt"
},
{
label: "小于等于",
value: "le"
}
];
+195
View File
@@ -0,0 +1,195 @@
<!--
* @Description: 代码生成器
* @Author: huguodong
* @Date: 2025-01-07 16:24:17
!-->
<template>
<div class="table-box">
<ProTable v-if="indexShow" ref="proTable" title="代码生成器列表" :columns="columns" :request-api="genBasicApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选代码生成')"
/>
</template>
<!-- 表格 状态 -->
<template #status="scope">
<el-space wrap>
<el-tag v-if="scope.row.status === MessageStatusDictEnum.ALREADY" type="success">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_STATUS, MessageStatusDictEnum.ALREADY)
}}</el-tag>
<el-tag v-else-if="scope.row.status === MessageStatusDictEnum.READY" type="info">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_STATUS, MessageStatusDictEnum.READY)
}}</el-tag>
</el-space>
</template>
<!-- 表格 类型 -->
<template #category="scope">
<el-space wrap>
<el-tag v-if="scope.row.category === MessageTypeDictEnum.INFORM" type="danger">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_CATEGORY, MessageTypeDictEnum.INFORM)
}}</el-tag>
<el-tag v-else-if="scope.row.category === MessageTypeDictEnum.MESSAGE" type="primary">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_CATEGORY, MessageTypeDictEnum.MESSAGE)
}}</el-tag>
<el-tag v-else-if="scope.row.category === MessageTypeDictEnum.NOTICE" type="warning">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_CATEGORY, MessageTypeDictEnum.NOTICE)
}}</el-tag>
</el-space>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.VIEW" @click="previewRef?.onOpen(scope.row)">预览</s-button>
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)">配置</s-button>
<s-button link :icon="VideoPlay" @click="choseExecGen(scope.row)">生成</s-button>
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.dbTable}】信息`)" />
</template>
</ProTable>
<Steps v-else ref="stepsRef" @closed="closeStep" />
<Preview ref="previewRef" />
<el-dialog v-model="dialogVisible" title="生成选项" width="400">
<s-radio-group v-model="execType" :options="genConst.execTypeOptions" size="large"></s-radio-group>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="execGen">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="tsx" name="genCode">
import { VideoPlay } from "@element-plus/icons-vue";
import { genBasicApi, GenCode } from "@/api";
import { useHandleData } from "@/hooks/useHandleData";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useDictStore } from "@/stores/modules";
import { FormOptEnum, SysDictEnum, MessageStatusDictEnum, MessageTypeDictEnum } from "@/enums";
import * as genConst from "./genConst";
import Steps from "./components/steps.vue";
import Preview from "./components/preview.vue";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const dictStore = useDictStore();
const indexShow = ref(true);
const previewRef = ref<InstanceType<typeof Preview> | null>(null); // 预览表单引用
const dialogVisible = ref(false);
const execType = ref(genConst.execTypeOptions[0].value);
const execRecord = ref(); //要生成的id
// 表格配置项
const columns: ColumnProps<GenCode.GenBasic>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "configId", label: "库名" },
{ prop: "dbTable", label: "表名" },
{ prop: "busName", label: "业务名" },
{ prop: "functionName", label: "功能名" },
{ prop: "className", label: "类名" },
{ prop: "generateType", label: "生成方式", enum: genConst.generateTypeOptions },
{ prop: "moduleType", label: "生成模版", enum: genConst.moduleTypeOptions },
{ prop: "authorName", label: "作者" },
{ prop: "operation", label: "操作", width: 280, fixed: "right" }
];
// 表单引用
const stepsRef = ref<InstanceType<typeof Steps> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | GenCode.GenBasic = {}) {
indexShow.value = false;
nextTick(() => {
stepsRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
});
}
/**
* 关闭步骤
*/
function closeStep() {
indexShow.value = true;
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(genBasicApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
//生成方式
// function generateTypeFilter(text: string) {
// const array = genConst.generateTypeOptions;
// const foundItem = array.find(f => f.value === text);
// if (foundItem) {
// return foundItem.label;
// } else {
// // 返回一个默认值或者抛出一个错误
// return "未知";
// }
// }
//选择生成前端还是后端
function choseExecGen(record: GenCode.GenBasic) {
dialogVisible.value = true;
execRecord.value = record;
}
/**执行代码生成 */
function execGen() {
console.log("execType", execType);
const param = {
id: execRecord.value.id,
execType: execType.value
};
if (execRecord.value.generateType === "PRO") {
genBasicApi.execGenPro(param).then(() => {
RefreshTable();
});
} else {
// 下载压缩包
genBasicApi.execGenZip(param).then(res => {
console.log("[ res ] >", res);
let data = res as any;
const blob = new Blob([data.data], { type: "application/octet-stream;charset=UTF-8" });
const contentDisposition = data.headers["content-disposition"];
const patt = new RegExp("filename=([^;]+\\.[^\\.;]+);*");
const $link = document.createElement("a");
$link.href = URL.createObjectURL(blob);
const match = patt.exec(contentDisposition);
if (match) {
$link.download = decodeURIComponent(match[1]);
}
$link.click();
document.body.appendChild($link);
document.body.removeChild($link); // 下载完成移除元素
window.URL.revokeObjectURL($link.href); // 释放掉blob对象
});
}
execCancel();
}
//取消生成
function execCancel() {
dialogVisible.value = false;
execType.value = genConst.execTypeOptions[0].value;
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,127 @@
<!--
* @Description: 站内信详情
* @Author: huguodong
* @Date: 2024-11-11 09:22:49
!-->
<template>
<form-container v-model="visible" title="站内信详情" form-size="700px">
<h4 class="title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="主题" label-align="left">
{{ messageProps.record.subject }}
</el-descriptions-item>
<el-descriptions-item label="消息类型" label-align="left">
{{ dictStore.dictTranslation(SysDictEnum.MESSAGE_CATEGORY, messageProps.record.category!) }}
</el-descriptions-item>
<el-descriptions-item label="发送方式" label-align="left">
{{ dictStore.dictTranslation(SysDictEnum.MESSAGE_WAY, messageProps.record.sendWay!) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" label-align="left">
{{ messageProps.record.createTime }}
</el-descriptions-item>
<el-descriptions-item label="发送时间" label-align="left">
{{ messageProps.record.sendTime }}
</el-descriptions-item>
<el-descriptions-item label="状态" label-align="left">
{{ dictStore.dictTranslation(SysDictEnum.MESSAGE_STATUS, messageProps.record.status!) }}
</el-descriptions-item>
<!-- <el-descriptions-item label="正文" class-name="my-class" label-align="left" label-class-name="my-label">
{{ messageProps.record.content }}
</el-descriptions-item> -->
</el-descriptions>
<h4 class="title">正文:</h4>
<div v-html="messageProps.record.content"></div>
<h4 class="title">接收详情:</h4>
<ProTable
v-if="getReadData"
ref="proTable"
title="接收列表"
class="table"
:columns="columns"
:pagination="true"
:data="messageProps.record.receiverDetail"
:tool-button="false"
>
<!-- 表格 类型 -->
<template #read="scope">
<el-space wrap>
<el-tag v-if="scope.row.read === true" type="info">是</el-tag>
<el-tag v-else-if="scope.row.read === false" type="danger">否</el-tag>
</el-space>
</template>
</ProTable>
<template #footer>
<el-button type="primary" @click="onClose"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { SysMessage, messageApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { useDictStore } from "@/stores/modules";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore();
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 是否
const yesNoOptions = dictStore.getDictList(SysDictEnum.YES_NO, true);
const getReadData = ref<boolean>(false); //是否获取了接收详情数据
// 表格配置项
const columns: ColumnProps<any>[] = [
{ prop: "name", label: "姓名" },
{ prop: "read", label: "是否已读", enum: yesNoOptions }
];
// 表单参数
const messageProps = reactive<FormProps.Base<SysMessage.SysMessageInfo>>({
opt: FormOptEnum.VIEW,
record: {},
disabled: false
});
/** 关闭表单*/
function onClose() {
visible.value = false;
}
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysMessage.SysMessageInfo>) {
console.log("[ messageProps.record.id ] >", messageProps.record.id);
Object.assign(messageProps, props); //合并参数
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
messageApi.detail({ id: props.record.id, showReceiveInfo: true }).then(res => {
messageProps.record = res.data;
getReadData.value = true;
});
}
}
/**
* 获取按钮分页数据
* 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
* 默认不做操作就直接在 ProTable 组件上绑定 :requestApi="getUserList"
*/
// function getPage(params: any) {
// let newParams = JSON.parse(JSON.stringify(params)); //转换成json字符串再转换成json对象
// newParams.id = messageProps.record.id; //按钮父Id
// console.log("[ initParam ] >", initParam.value);
// return messageApi.readDetail(newParams);
// }
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,242 @@
<!--
* @Description: 单页管理表单页面
* @Author: huguodong
* @Date: 2023-12-15 15:43:59
!-->
<template>
<form-container v-model="visible" :title="`${messageProps.opt}站内信`" form-size="700px">
<el-form
ref="messageFormRef"
:rules="rules"
:disabled="messageProps.disabled"
:model="messageProps.record"
:hide-required-asterisk="messageProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="消息主题" prop="subject">
<s-input v-model="messageProps.record.subject"></s-input>
</s-form-item>
<s-form-item label="消息类型" prop="category">
<s-select v-model="messageProps.record.category" :options="messageTypeOptions" />
</s-form-item>
<s-form-item label="正文内容" prop="content">
<s-input v-model="messageProps.record.content" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" />
</s-form-item>
<s-form-item label="发送方式" prop="sendWay">
<s-radio-group v-model="messageProps.record.sendWay" :options="sendWayOptions" button @change="handleSendWayChange" />
</s-form-item>
<s-form-item v-if="messageProps.record.sendWay === MessageSendWayDictEnum.DELAY" label="延迟时间" prop="delayTime">
<el-input v-model="messageProps.record.delayTime">
<template #append>秒</template>
</el-input>
</s-form-item>
<s-form-item v-if="messageProps.record.sendWay === MessageSendWayDictEnum.SCHEDULE" label="指定时间" prop="sendTime">
<date-picker v-model="messageProps.record.sendTime" :show-shortcuts="false" :disabled-date-before="true" type="datetime" />
</s-form-item>
<s-form-item label="接收人" prop="receiverType">
<!-- 全部和指定 -->
<s-radio-group v-model="messageProps.record.receiverType" :options="receiverTypeOptions" button @change="handleReceiverTypeChange" />
</s-form-item>
<s-form-item v-if="messageProps.record.receiverType !== MessageReceiverTypeDictEnum.ALL" :label="receiverType" prop="receiverInfo">
<el-button link type="primary" @click="showSelector">选择</el-button>
<el-tag v-for="item in messageProps.record.receiverInfo" :key="item.id" class="ml-3px" closable @close="removeTag(item)">{{
item.name
}}</el-tag>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!messageProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
<!-- 用户选择器 -->
<user-selector
ref="userSelectorRef"
multiple
:org-tree-api="sysOrgApi.tree"
:position-tree-api="sysPositionApi.tree"
:role-tree-api="sysRoleApi.tree"
:user-selector-api="sysUserApi.selector"
@successful="handleChooseUser"
/>
<!-- 角色选择器 -->
<role-selector
ref="roleSelectorRef"
multiple
:org-tree-api="sysOrgApi.tree"
:role-selector-api="sysRoleApi.roleSelector"
@successful="handleChooseRole"
></role-selector>
</template>
<script setup lang="ts">
import { SysMessage, messageApi, sysOrgApi, sysPositionApi, sysRoleApi, sysUserApi, SysUser, SysRole } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum, MessageTypeDictEnum, SysDictEnum, MessageReceiverTypeDictEnum, MessageSendWayDictEnum } from "@/enums";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";
import { RoleSelectorInstance } from "@/components/Selectors/RoleSelector/interface";
const dictStore = useDictStore();
const visible = ref(false); //是否显示表单
// 消息类型选项
const messageTypeOptions = dictStore.getDictList(SysDictEnum.MESSAGE_CATEGORY);
// 消息接受者类型选项
const receiverTypeOptions = dictStore.getDictList(SysDictEnum.RECEIVER_TYPE);
// 发送方式选项
const sendWayOptions = dictStore.getDictList(SysDictEnum.MESSAGE_WAY);
//根据接收人类型,计算出显示角色还是用户
const receiverType = computed(() => {
return messageProps.record.receiverType == MessageReceiverTypeDictEnum.ROLE ? "角色" : "用户";
});
// 表单参数
const messageProps = reactive<FormProps.Base<SysMessage.SysMessageInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
subject: [required("请输入消息主题")],
category: [required("请输入消息类型")],
content: [required("请输入正文内容")],
receiverType: [required("请选择接收人")],
sendWay: [required("请选择发送方式")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysMessage.SysMessageInfo>) {
Object.assign(messageProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
messageProps.record.category = MessageTypeDictEnum.INFORM;
messageProps.record.receiverType = MessageReceiverTypeDictEnum.ALL;
messageProps.record.receiverInfo = [] as SysUser.SysUserInfo[] | SysRole.SysRoleInfo[];
messageProps.record.sendWay = MessageSendWayDictEnum.NOW;
messageProps.record.read = false;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
messageApi.detail({ id: props.record.id }).then(res => {
messageProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const messageFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
messageFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//如果接收人类型是指定,但是没有选择接收人,提示用户选择接收人
if (messageProps.record.receiverType != MessageReceiverTypeDictEnum.ALL && messageProps.record.receiverInfo!.length == 0) {
ElMessage("请选择接收人");
return;
}
//如果发送方式是延迟发送,但是没有填写延迟时间,提示用户填写延迟时间
if (messageProps.record.sendWay == MessageSendWayDictEnum.DELAY && messageProps.record.delayTime == null) {
ElMessage("请填写延迟时间");
return;
}
//如果发送方式是定时发送,但是没有填写发送时间,提示用户填写发送时间
if (messageProps.record.sendWay == MessageSendWayDictEnum.SCHEDULE && messageProps.record.sendTime == null) {
ElMessage("请填写发送时间");
return;
}
//提交表单
await messageApi
.submitForm(messageProps.record, messageProps.record.id != undefined)
.then(() => {
messageProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
const userSelectorRef = ref<UserSelectorInstance>(); //用户选择器引用
const roleSelectorRef = ref<RoleSelectorInstance>(); //角色选择器引用
/** 显示用户选择器 */
function showSelector() {
//根据是指定角色还是用户,显示不同的选择器
if (messageProps.record.receiverType == MessageReceiverTypeDictEnum.ROLE) {
roleSelectorRef.value?.showSelector();
} else if (messageProps.record.receiverType == MessageReceiverTypeDictEnum.APPOINT) {
userSelectorRef.value?.showSelector();
}
}
/** 选择用户 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
// 选择用户后,将用户id赋值给orgProps.record.directorId
if (data.length > 0) {
console.log("[ data ] >", data);
//赋值messageProps.record.receiverInfo
messageProps.record.receiverInfo = data;
}
}
/** 选择角色 */
function handleChooseRole(data: SysRole.SysRoleInfo[]) {
// 选择角色后,将角色id赋值给orgProps.record.directorId
if (data.length > 0) {
messageProps.record.receiverInfo = data;
}
}
/** 移除选择的角色或者用户 */
function removeTag(data: SysUser.SysUserInfo | SysRole.SysRoleInfo) {
//移除messageProps.record.receiverInfo中对应Id的数据
if (messageProps.record.receiverInfo) {
const newData = messageProps.record.receiverInfo!.filter(item => item.id != data.id) as SysUser.SysUserInfo[] | SysRole.SysRoleInfo[];
messageProps.record.receiverInfo = newData;
}
}
/** 改变接收人类型 */
function handleReceiverTypeChange() {
//接收人类型改变时,清空接收人信息
messageProps.record.receiverInfo = [];
}
/** 改变发送方式 */
function handleSendWayChange(value: any) {
//发送方式改变时,清空发送方式信息
// messageProps.record.sendType = [];
messageProps.record.sendTime = undefined;
messageProps.record.delayTime = undefined;
messageProps.record.sendWay = value;
return;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped>
:deep(.s-input-group__prepend) {
padding: 0 10px !important;
}
</style>
+143
View File
@@ -0,0 +1,143 @@
<!--
* @Description:站内消息
* @Author: huguodong
* @Date: 2024-11-08 16:16:52
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="消息列表" :columns="columns" :request-api="messageApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="站内信" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="站内信"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选信息')"
/>
</template>
<!-- 表格 状态 -->
<template #status="scope">
<el-space wrap>
<el-tag v-if="scope.row.status === MessageStatusDictEnum.ALREADY" type="success">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_STATUS, MessageStatusDictEnum.ALREADY)
}}</el-tag>
<el-tag v-else-if="scope.row.status === MessageStatusDictEnum.READY" type="info">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_STATUS, MessageStatusDictEnum.READY)
}}</el-tag>
</el-space>
</template>
<!-- 表格 类型 -->
<template #category="scope">
<el-space wrap>
<el-tag v-if="scope.row.category === MessageTypeDictEnum.INFORM" type="danger">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_CATEGORY, MessageTypeDictEnum.INFORM)
}}</el-tag>
<el-tag v-else-if="scope.row.category === MessageTypeDictEnum.MESSAGE" type="primary">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_CATEGORY, MessageTypeDictEnum.MESSAGE)
}}</el-tag>
<el-tag v-else-if="scope.row.category === MessageTypeDictEnum.NOTICE" type="warning">{{
dictStore.dictTranslation(SysDictEnum.MESSAGE_CATEGORY, MessageTypeDictEnum.NOTICE)
}}</el-tag>
</el-space>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button v-if="scope.row.status === MessageStatusDictEnum.READY" link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button v-if="scope.row.status === MessageStatusDictEnum.ALREADY" link :opt="FormOptEnum.VIEW" @click="onOpenDetail(scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.title}】信息`)" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 详情表单 -->
<Detail ref="detailRef" />
</div>
</template>
<script setup lang="tsx" name="sysSpa">
import { messageApi } from "@/api";
import { SysMessage } from "@/api/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useDictStore } from "@/stores/modules";
import { FormOptEnum, SysDictEnum, MessageStatusDictEnum, MessageTypeDictEnum } from "@/enums";
import Form from "./components/form.vue";
import Detail from "./components/detail.vue";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const dictStore = useDictStore();
// 单页类型选项
const messageTypeOptions = dictStore.getDictList(SysDictEnum.MESSAGE_CATEGORY);
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.MESSAGE_STATUS);
// 发送方式选项
const sendWayOptions = dictStore.getDictList(SysDictEnum.MESSAGE_WAY);
// 表格配置项
const columns: ColumnProps<SysMessage.SysMessageInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "searchKey", label: "主题关键字", search: { el: "input" }, isShow: false },
{ prop: "subject", label: "主题" },
{
prop: "category",
label: "消息类型",
enum: messageTypeOptions,
search: { el: "select" }
},
{ prop: "content", label: "正文" },
{ prop: "sendWay", label: "发送方式", enum: sendWayOptions },
{ prop: "createTime", label: "创建时间" },
{ prop: "sendTime", label: "发送时间" },
{ prop: "status", label: "状态", enum: statusOptions, search: { el: "select" } },
{ prop: "operation", label: "操作", width: 200, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysMessage.SysMessageInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(messageApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
// 表单引用
const detailRef = ref<InstanceType<typeof Detail> | null>(null);
/**
* 打开详情
* @param opt 操作类型
* @param record 记录
*/
function onOpenDetail(record: SysMessage.SysMessageInfo) {
detailRef.value?.onOpen({ opt: FormOptEnum.VIEW, record: record, successful: RefreshTable });
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,85 @@
<!--
* @Description: 权限按钮批量表单页面
* @Author: huguodong
* @Date: 2023-12-15 15:42:29
!-->
<template>
<el-dialog v-model="visible" title="批量新增按钮" width="500px">
<el-form ref="buttonFormRef" :rules="rules" :model="buttonProps" label-width="auto">
<s-form-item label="权限简称" prop="title">
<s-input v-model="buttonProps.title"></s-input>
</s-form-item>
<s-form-item label="编码前缀" prop="code">
<s-input v-model="buttonProps.code"></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button primary @click="handleSubmit"> 确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { buttonApi, Button } from "@/api";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus/es/components/form";
const visible = ref(false); //是否显示表单
// 表单参数接口
interface ButtonProps extends Button.Batch {
/** 成功之后的方法 */
successful?: () => void;
}
// 表单参数
const buttonProps = reactive<ButtonProps>({
parentId: 0,
title: "",
code: ""
});
// 表单验证规则
const rules = reactive({
title: [required("请输入权限简称")],
code: [required("请输入编码前缀")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: ButtonProps) {
Object.assign(buttonProps, props); //合并参数
visible.value = true;
}
// 提交数据(新增/编辑)
const buttonFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
buttonFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await buttonApi
.batch(buttonProps)
.then(() => {
buttonProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,104 @@
<!--
* @Description: 权限按钮表单页面
* @Author: huguodong
* @Date: 2023-12-15 15:42:36
!-->
<template>
<el-dialog v-model="visible" :title="`${buttonProps.opt}按钮`" width="500px">
<el-form
ref="buttonFormRef"
:rules="rules"
:disabled="buttonProps.disabled"
:model="buttonProps.record"
:hide-required-asterisk="buttonProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="按钮名称" prop="title">
<s-input v-model="buttonProps.record.title"></s-input>
</s-form-item>
<s-form-item label="按钮编码" prop="code">
<s-input v-model="buttonProps.record.code"></s-input>
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="buttonProps.record.sortCode" show-input :min="1" />
</s-form-item>
<s-form-item label="说明" prop="description">
<s-input v-model="buttonProps.record.description"></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!buttonProps.disabled" primary @click="handleSubmit"> 确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { buttonApi, Button } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum } from "@/enums";
import { FormInstance } from "element-plus/es/components/form";
const visible = ref(false); //是否显示表单
// 表单参数
const buttonProps = reactive<FormProps.Base<Button.ButtonInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
title: [required("请输入单页名称")],
code: [required("请输入按钮编码")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<Button.ButtonInfo>) {
Object.assign(buttonProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
buttonProps.record.sortCode = 99;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
buttonApi.detail({ id: props.record.id }).then(res => {
buttonProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const buttonFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
buttonFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await buttonApi
.submitForm(buttonProps.record, buttonProps.record.id != undefined)
.then(() => {
buttonProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+142
View File
@@ -0,0 +1,142 @@
<!--
* @Description: 权限按钮管理
* @Author: huguodong
* @Date: 2023-12-15 15:42:52
!-->
<template>
<div>
<form-container v-model="visible" title="权限按钮" form-size="60%">
<div class="table-box min-h-300px">
<ProTable ref="proTable" title="按钮列表" class="table" :columns="columns" :request-api="getPage">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="按钮" @click="onOpen(FormOptEnum.ADD)" />
<s-button @click="onOpenBatch()"> 批量新增 </s-button>
<s-button
type="danger"
plain
suffix="按钮"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选按钮')"
/>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.title}】按钮`)" />
</template>
</ProTable>
</div>
<template #footer>
<el-button type="primary" @click="onClose"> 确定 </el-button>
</template>
</form-container>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 批量新增 -->
<Batch ref="batchRef" />
</div>
</template>
<script setup lang="ts">
import { buttonApi, Button } from "@/api";
import { useHandleData } from "@/hooks/useHandleData";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { FormOptEnum } from "@/enums";
import Form from "./components/form.vue";
import Batch from "./components/batch.vue";
const visible = ref(false); //是否显示表单
const parentId = ref<number | string>(0); //按钮父Id
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<Button.ButtonInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "title", label: "名称" },
{ prop: "code", label: "编码" },
{ prop: "sortCode", label: "排序" },
{ prop: "description", label: "说明" },
{ prop: "operation", label: "操作", width: 150, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | Button.ButtonInfo = { parentId: parentId.value }) {
formRef.value?.onOpen({
opt: opt,
record: record,
successful: () => RefreshTable()
});
}
// 批量新增引用
const batchRef = ref<InstanceType<typeof Batch> | null>(null);
/**
* 打开批量新增表单
*/
function onOpenBatch() {
batchRef.value?.onOpen({
parentId: parentId.value,
title: "",
code: "",
successful: RefreshTable
});
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(buttonApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
/**
* 打开表单
* @param props 表单参数
*/
function onOpenTable(menuId: number | string) {
visible.value = true;
parentId.value = menuId;
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
/**
* 获取按钮分页数据
* 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
* 默认不做操作就直接在 ProTable 组件上绑定 :requestApi="getUserList"
*/
function getPage(params: any) {
let newParams = JSON.parse(JSON.stringify(params)); //转换成json字符串再转换成json对象
newParams.parentId = parentId.value; //按钮父Id
return buttonApi.page(newParams);
}
// 暴露给父组件的方法
defineExpose({
onOpenTable
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,94 @@
<!--
* @Description: 更改菜单模块
* @Author: huguodong
* @Date: 2023-12-15 15:42:57
!-->
<template>
<form-container v-model="visible" :title="'更改模块->' + changeModuleProps.title" form-size="450px">
<el-form ref="changeModuleFormRef" :rules="rules" :model="changeModuleProps" class="mt-20px">
<s-form-item label="所属模块" prop="module">
<s-select v-model="changeModuleProps.module" :options="changeModuleProps.moduleOptions"></s-select>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { menuApi } from "@/api";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus/es/components/form";
const visible = ref(false); //是否显示表单
/** 表单参数 */
interface ChangeModuleProps {
/** 菜单id */
id: number | string;
/** 模块id */
module: number | string;
/** 菜单名称 */
title: string;
/** 模块列表 */
moduleOptions: { label: string; value: number | string }[];
/** 表单实例 */
successful?: () => void;
}
/** 表单参数 */
const changeModuleProps = reactive<ChangeModuleProps>({
id: 0,
module: 0,
title: "",
moduleOptions: []
});
// 表单验证规则
const rules = reactive({
module: [required("请选择所属模块")]
});
// 提交数据(新增/编辑)
const changeModuleFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
changeModuleFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await menuApi
.changeModule({
id: changeModuleProps.id,
module: changeModuleProps.module
})
.then(() => {
//提交成功
changeModuleProps.successful?.();
})
.finally(() => {
onClose();
});
});
}
/** 打开表单
* @param title 菜单名称
* @param module 模块id
*/
function onOpen(moduleProps: ChangeModuleProps) {
Object.assign(changeModuleProps, moduleProps); //合并参数
visible.value = true; //显示表单
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,219 @@
<!--
* @Description: 菜单管理表菜单面
* @Author: huguodong
* @Date: 2023-12-15 15:43:02
!-->
<template>
<form-container v-model="visible" :title="`${menuProps.opt}菜单`" form-size="600px">
<el-form
ref="menuFormRef"
:rules="rules"
:disabled="menuProps.disabled"
:model="menuProps.record"
:hide-required-asterisk="menuProps.disabled"
label-width="auto"
label-suffix=" :"
>
<!-- 基本设置 -->
<el-divider content-position="left">基本设置</el-divider>
<s-form-item label="菜单名称" prop="title">
<s-input v-model="menuProps.record.title"></s-input>
</s-form-item>
<s-form-item label="菜单类型" prop="menuType">
<s-radio-group v-model="menuProps.record.menuType" :options="menuTypeOptions" button />
</s-form-item>
<s-form-item label="图标" prop="icon">
<SelectIconPlus v-model:icon-value="menuProps.record.icon!" />
</s-form-item>
<s-form-item label="上级菜单" prop="parentId">
<MenuSelector
v-model:menu-value="menuProps.record.parentId"
show-all
@change="changeMenu"
:menu-tree-api="() => menuApi.menuTreeSelector({ module: module })"
/>
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="menuProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="路由地址" prop="path">
<s-input v-model="menuProps.record.path" placeholder="请填写路由地址,例:/home/index"></s-input>
</s-form-item>
<div v-if="isMenu || isSubSet || isLink">
<s-form-item label="组件名称" prop="name">
<s-input v-model="menuProps.record.name">
<template #prepend>setup name=</template>
</s-input>
</s-form-item>
<s-form-item label="组件地址" prop="component">
<s-input v-model="menuProps.record.component">
<template #prepend>src/views/</template>
</s-input>
</s-form-item>
</div>
<div v-if="isLink">
<s-form-item label="链接地址:" prop="isLink">
<s-input v-model="menuProps.record.isLink" placeholder="请填写链接地址,例:http://www.baidu.com"></s-input>
</s-form-item>
</div>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="menuProps.record.sortCode" show-input :min="1" />
</s-form-item>
<s-form-item label="说明" prop="description">
<s-input v-model="menuProps.record.description"></s-input>
</s-form-item>
<!-- 功能设置 -->
<div v-if="isMenu || isLink">
<el-divider content-position="left">功能设置</el-divider>
<el-row :gutter="24">
<el-col :span="12">
<s-form-item label="设置主页" prop="isHome">
<s-radio-group v-model="menuProps.record.isHome" :disabled="isLink || isSubSet" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="隐藏菜单" prop="isHide">
<s-radio-group v-model="menuProps.record.isHide" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="页面全屏" prop="isFull">
<s-radio-group v-model="menuProps.record.isFull" :disabled="isLink" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="固定标签页" prop="isAffix">
<s-radio-group v-model="menuProps.record.isAffix" :disabled="isLink" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="路由缓存" prop="isKeepAlive">
<s-radio-group v-model="menuProps.record.isKeepAlive" :disabled="isLink" :options="yesOptions" button />
</s-form-item>
</el-col>
</el-row>
</div>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!menuProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { menuApi, Menu } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum, SysDictEnum, MenuTypeDictEnum } from "@/enums";
import { FormInstance } from "element-plus/es/components/form";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 菜单类型选项
const menuTypeOptions = dictStore
.getDictList(SysDictEnum.MENU_TYPE)
.filter((item: { value: MenuTypeDictEnum }) => item.value !== MenuTypeDictEnum.BUTTON);
// 是否选项
const yesOptions = dictStore.getDictList(SysDictEnum.YES_NO, true);
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 模块id
const module = ref<number | string>(0);
// 表单参数
const menuProps = reactive<FormProps.Base<Menu.MenuInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
title: [required("请输入菜单名称")],
parentId: [required("请选择上级菜单")],
menuType: [required("请选择菜单类型")],
path: [required("请输入路由地址")],
name: [required("请输入组件名称")],
component: [required("请输入组件地址")],
sortCode: [required("请输入排序")],
icon: [required("请选择图标")],
status: [required("请选择状态")],
isLink: [required("请输入链接地址")]
});
// const isCatalog = computed(() => menuProps.record.menuType === MenuTypeDictEnum.CATALOG); //是否是目录
const isMenu = computed(() => menuProps.record.menuType === MenuTypeDictEnum.MENU); //是否是菜单
const isLink = computed(() => menuProps.record.menuType === MenuTypeDictEnum.LINK); //是否
const isSubSet = computed(() => menuProps.record.menuType === MenuTypeDictEnum.SUBSET); //是否是子集
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<Menu.MenuInfo>, moduleId: number | string) {
Object.assign(menuProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
menuProps.record.sortCode = 99;
menuProps.record.status = statusOptions[0].value;
menuProps.record.menuType = MenuTypeDictEnum.MENU;
menuProps.record.sortCode = 99;
menuProps.record.isHome = false;
menuProps.record.isHide = false;
menuProps.record.isFull = false;
menuProps.record.isAffix = false;
menuProps.record.isKeepAlive = true;
menuProps.record.module = moduleId;
}
module.value = moduleId; //设置模块id
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
menuApi.detail({ id: props.record.id }).then(res => {
menuProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const menuFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
menuFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await menuApi
.submitForm(menuProps.record, menuProps.record.id != undefined)
.then(() => {
menuProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
/** 菜单选择事件
* @param path 路由地址
*/
function changeMenu(path: string) {
if (isSubSet.value) {
path = path + "/:id";
menuProps.record.activeMenu = path;
}
menuProps.record.path = path;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+220
View File
@@ -0,0 +1,220 @@
<!--
* @Description: 菜单管理
* @Author: huguodong
* @Date: 2023-12-15 15:43:08
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="菜单列表" :indent="20" :columns="columns" :request-api="menuApi.tree" :pagination="false">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="菜单" @click="onOpen(FormOptEnum.ADD)" />
<s-button
suffix="菜单"
:opt="FormOptEnum.DELETE"
plain
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选菜单')"
/>
</template>
<!-- 表格 菜单类型 按钮 -->
<template #menuType="scope">
<el-space>
<el-tag>{{ dictStore.dictTranslation(SysDictEnum.MENU_TYPE, scope.row.menuType) }}</el-tag>
<el-tag v-if="scope.row.isHome === true" type="warning">首页</el-tag>
</el-space>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<el-space>
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.title}】菜单`)" />
<el-dropdown @command="handleCommand">
<el-link type="primary" :underline="false" :icon="ArrowDown"> 更多 </el-link>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="scope.row.parentId === 0" :command="command(scope.row, cmdEnum.ChangeModule)">{{
cmdEnum.ChangeModule
}}</el-dropdown-item>
<el-dropdown-item v-if="isMenu(scope.row.menuType)" :command="command(scope.row, cmdEnum.Button)">{{
cmdEnum.Button
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 更改模块 -->
<ChangeModule ref="changeModuleFormRef" />
<!-- 权限按钮 -->
<Button ref="buttonFormRef" />
</div>
</template>
<script setup lang="tsx" name="sysMenu">
import { menuApi, Menu } from "@/api";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { ArrowDown } from "@element-plus/icons-vue";
import { useUserStore, useDictStore } from "@/stores/modules";
import { SysDictEnum, FormOptEnum, MenuTypeDictEnum, CommonStatusEnum } from "@/enums";
import Form from "./components/form.vue";
import { useHandleData } from "@/hooks/useHandleData";
import ChangeModule from "./components/changeModule.vue";
import Button from "../button/index.vue";
const userStore = useUserStore();
const dictStore = useDictStore();
//遍历模块列表,生成下拉选项
const moduleOptions = userStore.moduleList.map(item => {
return { label: item.title, value: item.id };
});
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<Menu.MenuInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "id", label: "Id", isShow: false },
{
prop: "module",
label: "应用模块",
enum: moduleOptions,
search: {
el: "select",
props: {
clearable: false,
placeholder: "请选择应用模块"
},
defaultValue: moduleOptions[0].value
},
isShow: false
},
{ prop: "title", label: "菜单名称", width: 150, align: "left", search: { el: "input" } },
{
prop: "icon",
label: "菜单图标",
render: scope => {
return <svg-icon icon={scope.row.icon} class="h-6 w-6" />;
}
},
{ prop: "menuType", label: "菜单类型" },
{ prop: "name", label: "组件名称" },
{ prop: "path", label: "路由地址", width: 200, search: { el: "input" } },
{ prop: "component", label: "组件路径", width: 250 },
{ prop: "sortCode", label: "排序", width: 80 },
{ prop: "status", label: "状态" },
{ prop: "description", label: "说明" },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | Menu.MenuInfo = {}) {
formRef.value?.onOpen(
{
opt: opt,
record: record,
successful: () => RefreshTable()
},
proTable.value?.searchParam.module
);
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(menuApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
/**判断是否是菜单 */
const isMenu = computed(() => (menuType: string) => {
return menuType === MenuTypeDictEnum.MENU;
}); //是否是菜单
/** 更多下拉菜单命令枚举 */
enum cmdEnum {
ChangeModule = "更改模块",
Button = "权限按钮"
}
/** 下拉菜单参数接口 */
interface Command {
row: Menu.MenuInfo;
command: cmdEnum;
}
/**配置command的参数 */
function command(row: Menu.MenuInfo, command: cmdEnum): Command {
return {
row: row,
command: command
};
}
// 修改模块表单引用
const changeModuleFormRef = ref<InstanceType<typeof ChangeModule> | null>(null);
// 权限按钮表单引用
const buttonFormRef = ref<InstanceType<typeof Button> | null>(null);
/**
* 更多下拉菜单点击事件
* @param command
*/
function handleCommand(command: Command) {
console.log("[ 12312 ] >", 12312);
switch (command.command) {
case cmdEnum.ChangeModule:
//更改模块
changeModuleFormRef.value?.onOpen({
moduleOptions,
id: command.row.id,
title: command.row.title,
module: command.row.module,
successful: () => RefreshTable()
});
break;
case cmdEnum.Button:
//权限按钮
buttonFormRef.value?.onOpenTable(command.row.id);
break;
}
}
</script>
<style lang="scss" scoped>
.el-dropdown-link {
display: flex;
align-items: center;
color: var(--el-color-primary);
cursor: pointer;
}
:deep(.el-link__inner) {
padding-left: 6px !important;
}
</style>
@@ -0,0 +1,122 @@
<!--
* @Description: 模块管理表单页面
* @Author: huguodong
* @Date: 2023-12-15 15:43:14
!-->
<template>
<form-container v-model="visible" :title="`${moduleProps.opt}模块`" form-size="600px">
<el-form
ref="moduleFormRef"
:rules="rules"
:disabled="moduleProps.disabled"
:model="moduleProps.record"
:hide-required-asterisk="moduleProps.disabled"
label-width="auto"
label-suffix=" :"
class="mt-20px"
>
<s-form-item label="模块名称" prop="title">
<s-input v-model="moduleProps.record.title"></s-input>
</s-form-item>
<s-form-item label="图标" prop="icon">
<SelectIconPlus v-model:icon-value="moduleProps.record.icon!" />
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="moduleProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="moduleProps.record.sortCode" show-input :min="1" />
</s-form-item>
<s-form-item label="说明" prop="description">
<s-input v-model="moduleProps.record.description"></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!moduleProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { moduleApi, Module } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const dictStore = useDictStore(); //字典仓库
const visible = ref(false); //
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const moduleProps = reactive<FormProps.Base<Module.ModuleInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
title: [required("请输入单页名称")],
menuType: [required("请选择单页类型")],
path: [required("请输入路由地址")],
name: [required("请输入组件名称")],
component: [required("请输入组件地址")],
sortCode: [required("请输入排序")],
icon: [required("请选择图标")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<Module.ModuleInfo>) {
Object.assign(moduleProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
moduleProps.record.sortCode = 99;
moduleProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
moduleApi.detail({ id: props.record.id }).then(res => {
moduleProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const moduleFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
moduleFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await moduleApi
.submitForm(moduleProps.record, moduleProps.record.id != undefined)
.then(() => {
moduleProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+103
View File
@@ -0,0 +1,103 @@
<!--
* @Description: 模块管理
* @Author: huguodong
* @Date: 2023-12-15 15:43:19
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="模块列表" :columns="columns" :request-api="moduleApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="模块" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="模块"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选模块')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)"> 编辑 </s-button>
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.title}】页面`)"> 删除 </s-button>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</template>
<script setup lang="tsx" name="sysModule">
import { moduleApi, Module } from "@/api";
import { useHandleData } from "@/hooks/useHandleData";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { FormOptEnum, CommonStatusEnum, SysDictEnum } from "@/enums";
import { useDictStore } from "@/stores/modules";
import Form from "./components/form.vue";
const dictStore = useDictStore(); //字典仓库
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<Module.ModuleInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "title", label: "模块名称", search: { el: "input" } },
{
prop: "icon",
label: "菜单图标",
render: scope => {
return <svg-icon icon={scope.row.icon} class="h-6 w-6" />;
}
},
{ prop: "sortCode", label: "排序" },
{ prop: "description", label: "说明" },
{ prop: "status", label: "状态" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 200, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | Module.ModuleInfo = {}) {
formRef.value?.onOpen({
opt: opt,
record: record,
successful: () => RefreshTable()
});
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(moduleApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,118 @@
<!--
* @Description: 数据范围选择器
* @Author: huguodong
* @Date: 2023-12-15 15:43:27
!-->
<template>
<div>
<el-space>
<s-radio-group v-model="defaultLevel" :options="scopeOptions" value-key="level" label-key="title" @change="changeDataScope">
<template #radio="{ item }">
<el-badge
v-if="
dataScope.level === defineScope?.level && item.scopeCategory === SysRole.DataScopeEnum.SCOPE_ORG_DEFINE && item.scopeDefineOrgIdList
"
:value="dataScope.scopeDefineOrgIdList.length"
type="success"
>
<span class="mb-5">{{ item.title }}</span>
</el-badge>
<span v-else>{{ item.title }}</span>
</template>
</s-radio-group>
<div v-if="dataScope.level === defineScope?.level">
<el-button link type="primary" class="ml-15px" @click="handleDefineOrg">选择组织</el-button>
</div>
</el-space>
<el-dialog v-model="dialogVisible" append-to-body title="选择组织" width="400" destroy-on-close>
<TreeFilter
title="组织列表"
multiple
check-strictly
class="treeFilter"
label="name"
:request-api="sysOrgApi.tree"
:default-value="dataScope.scopeDefineOrgIdList"
@change="changeTreeFilter"
/>
<template #footer>
<el-button @click="dialogCancel">取消</el-button>
<el-button type="primary" @click="chooseDefineOrg"> 确定 </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { SysRole, sysOrgApi } from "@/api";
const dialogVisible = ref(false); //是否显示弹窗
// props
interface DataScopeProps {
modelValue: SysRole.DefaultDataScope | undefined;
}
const emit = defineEmits(["update:modelValue"]); //定义emit
const props = defineProps<DataScopeProps>(); //定义props
// 用户信息
const dataScope = computed({
get: () => props.modelValue ?? scopeOptions[0], // 判断是否有值,没有值则取第一个
set: val => emit("update:modelValue", val)
});
// 数据范围列表
const scopeOptions = SysRole.dataScopeOptions;
// 默认数据范围
const defaultLevel = ref(dataScope.value.level);
// 自定义数据范围
const defineScope = SysRole.dataScopeOptions.find(item => item.scopeCategory === SysRole.DataScopeEnum.SCOPE_ORG_DEFINE);
// 自定义组织id列表
const defineOrgIdList = ref<number[] | string[]>(dataScope.value.scopeDefineOrgIdList);
/** 修改数据权限 */
function changeDataScope(value: any) {
let scope = SysRole.dataScopeOptions.find(item => item.level === value);
if (scope) {
dataScope.value = scope;
}
}
// /** 处理自定义 */
function handleDefineOrg() {
dialogVisible.value = true;
console.log("[ dataScope.value ] >", dataScope.value);
}
/** 组织树数据变化 */
function changeTreeFilter(val: any[]) {
defineOrgIdList.value = val;
}
/** 取消选择组织 */
function dialogCancel() {
// 关闭弹窗
dialogVisible.value = false;
}
/** 选择机构 */
function chooseDefineOrg() {
// 设置默认数据权限中的机构ID列表
// 关闭弹窗
dialogVisible.value = false;
dataScope.value.scopeDefineOrgIdList = defineOrgIdList.value;
console.log("[ dataScope.value ] >", dataScope.value);
}
</script>
<style lang="scss" scoped>
.treeFilter {
width: 100% !important;
}
:deep(.el-badge__content.is-fixed) {
transform: translateY(-30%) translateX(100%);
}
</style>
@@ -0,0 +1,125 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:43:34
!-->
<template>
<form-container v-model="visible" :title="`${sysRoleProps.opt}角色`" form-size="800px" @close="onClose">
<el-form
ref="sysRoleFormRef"
:rules="rules"
:disabled="sysRoleProps.disabled"
:model="sysRoleProps.record"
:hide-required-asterisk="sysRoleProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="角色名称" prop="name">
<s-input v-model="sysRoleProps.record.name"></s-input>
</s-form-item>
<s-form-item label="角色分类" prop="category">
<s-radio-group v-model="sysRoleProps.record.category" :options="roleCategoryOptions" button />
</s-form-item>
<s-form-item v-if="sysRoleProps.record.category === OrgCategoryEnum.ORG" label="所属组织" prop="orgId">
<org-selector v-model:org-value="sysRoleProps.record.orgId!" />
</s-form-item>
<s-form-item label="角色编码" prop="code">
<s-input v-model="sysRoleProps.record.code" placeholder="请填写角色编码,不填则为随机值"></s-input>
</s-form-item>
<s-form-item label="默认数据范围" prop="code">
<DataScopeSelector v-model="sysRoleProps.record.defaultDataScope"></DataScopeSelector>
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="sysRoleProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="sysRoleProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!sysRoleProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { SysRole, sysRoleApi } from "@/api";
import { FormOptEnum, SysDictEnum, OrgCategoryEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import DataScopeSelector from "./dataScopeSelector.vue";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
const roleCategoryOptions = dictStore.getDictList(SysDictEnum.ROLE_CATEGORY); // 角色类型选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS); // 通用状态选项
// 表单参数
const sysRoleProps = reactive<FormProps.Base<SysRole.SysRoleInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
name: [required("请输入角色名称")],
orgId: [required("请选择所属组织")],
category: [required("请选择角色类型")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysRole.SysRoleInfo>) {
Object.assign(sysRoleProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
sysRoleProps.record.sortCode = 99;
sysRoleProps.record.category = roleCategoryOptions[0].value;
sysRoleProps.record.status = statusOptions[0].value;
sysRoleProps.record.defaultDataScope = SysRole.dataScopeOptions[0];
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
sysRoleApi.detail({ id: props.record.id }).then(res => {
sysRoleProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const sysRoleFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
sysRoleFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await sysRoleApi
.submitForm(sysRoleProps.record, sysRoleProps.record.id != undefined)
.then(() => {
sysRoleProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,458 @@
<!--
* @Description: 授权移动端资源表单
* @Author: huguodong
* @Date: 2025-08-18 15:43:49
!-->
<template>
<div>
<form-container v-model="visible" :title="`授权移动端资源->${grantProps.name}`" form-size="1200px" @close="onClose">
<el-alert title="移动端资源授权:配置角色可访问的移动端菜单和功能" type="info" show-icon style="margin-bottom: 10px" />
<ProTable
ref="proTable"
:columns="columns"
:tool-button="false"
:cell-style="{ padding: '0px' }"
:row-style="{ height: '0' }"
:data="grantProps.tableData"
:pagination="false"
:span-method="objectSpanMethod"
>
<!-- 表格 header 按钮 -->
<template #tableHeader>
<s-radio-group
v-model="grantProps.moduleId"
:options="grantProps.echoDatalist"
value-key="id"
label-key="title"
button
class="mb-2"
@change="moduleClick"
>
<template #radio="{ item }">
<el-space>
<svg-icon :icon="item.icon" class="h-4 w-4"></svg-icon>
{{ item.title }}
</el-space>
</template>
</s-radio-group>
</template>
<!-- 一级目录 -->
<template #parentName="scope">
<el-checkbox v-model="scope.row.parentCheck" @change="val => changeParent(scope.row, val)">
{{ scope.row.parentName }}
</el-checkbox>
</template>
<!-- 菜单 -->
<template #title="scope">
<el-checkbox v-model="scope.row.nameCheck" @change="val => changeSub(scope.row, val)">
{{ scope.row.title }}
</el-checkbox>
</template>
<!-- 按钮授权 -->
<template #button="scope">
<template v-if="scope.row.button.length > 0">
<template v-for="(item, index) in scope.row.button" :key="item.id">
<el-checkbox v-model="item.check" style="margin-right: 5px" @change="val => changeChildCheckBox(scope.row, val)">
{{ item.title }}
</el-checkbox>
<br v-if="(index + 1) % 5 === 0" />
</template>
</template>
</template>
</ProTable>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
<el-dialog v-model="dialogVisible" title="请选择数据范围" width="30%" destroy-on-close>
<DataScopeSelector v-model="grantProps.defaultDataScope"></DataScopeSelector>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUserSubmit"> 确定 </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { SysRole, SysUser, mobileMenuApi, MobileMenu } from "@/api";
import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import type { TableColumnCtx } from "element-plus";
import DataScopeSelector from "./dataScopeSelector.vue";
const visible = ref(false); //授权移动端资源表单是否显示
const dialogVisible = ref(false); // 数据范围弹窗显示
/** 授权移动端资源表单参数接口 */
interface GrantMobileResourceFormProps {
/** id */
id: number | string;
/** 名称 */
name: string;
/** 模块id */
moduleId: number | string;
/** 移动端资源树 */
mobileResTree: MobileMenu.MobileResTreeSelector[];
/** 已拥有的移动端资源 */
ownMobileResource: MobileMenu.RoleOwnMobileResource | null;
/** 一级菜单索引 */
firstShowMap: Record<string, number[]>;
/** 回显数据列表 */
echoDatalist: MobileMenu.MobileResTreeSelector[];
/** 表格数据 */
tableData: MobileMenu.RoleGrantMobileResourceMenu[];
//默认数据范围,用户才有
defaultDataScope: SysRole.DefaultDataScope;
}
/** 授权移动端资源表单参数 */
const grantProps = reactive<GrantMobileResourceFormProps>({
id: 0,
name: "",
moduleId: 0,
mobileResTree: [],
ownMobileResource: null,
firstShowMap: {},
echoDatalist: [],
tableData: [],
defaultDataScope: SysRole.dataScopeOptions[0]
});
/** props */
const props = defineProps<{
/** 是否是用户 */
user?: boolean;
}>();
// 获取 ProTable DOM
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<MobileMenu.RoleGrantMobileResourceMenu>[] = [
{ prop: "parentName", label: "一级目录", width: 150, align: "left" },
{ prop: "title", label: "菜单", width: 180, align: "left" },
{ prop: "button", label: "按钮授权", align: "left" }
];
/** 列合并 */
interface SpanMethodProps {
// 行
row: MobileMenu.RoleGrantMobileResourceMenu;
// 列
column: TableColumnCtx<MobileMenu.RoleGrantMobileResourceMenu>;
// 行索引
rowIndex: number;
// 列索引
columnIndex: number;
}
/** 列合并*/
const objectSpanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps) => {
// 如果列索引为0
if (columnIndex == 0) {
// 获取父名称
const parentName = row.parentName;
// 获取父名称的索引数组
const indexArr = grantProps.firstShowMap[parentName];
// 如果行索引等于索引数组的第一个
if (rowIndex === indexArr[0]) {
return { rowspan: indexArr.length, colspan: 1 };
} else {
return { rowspan: 0, colspan: 0 };
}
}
};
/**
* 打开表单
* @param props 表单参数
*/
async function onOpen(props: FormProps.Base<SysRole.SysRoleInfo | SysUser.SysUserInfo>) {
grantProps.id = props.record.id || 0;
grantProps.name = props.record.name || "";
await loadData();
visible.value = true;
}
/** 加载数据 */
async function loadData() {
if (grantProps.echoDatalist.length > 0) {
let data = grantProps.echoDatalist.find(f => f.id === grantProps.moduleId)!.menu;
grantProps.tableData = data;
} else {
// 获取移动端资源授权树
await mobileMenuApi.resourceTreeSelector().then(res => {
grantProps.mobileResTree = res.data;
grantProps.moduleId = res.data[0].id;
});
await getMobileResources(); // 获取移动端资源
// 初始化数据结构
grantProps.mobileResTree.forEach(module => {
if (module.menu) {
module.menu.forEach(menu => {
// 如果 parentId 等于自己的 id,说明是顶级菜单,设置 parentName 为模块名称
if (menu.parentId === menu.id) {
menu.parentName = module.title; // 使用模块标题作为父级名称
}
// 初始化checkbox状态
menu.parentCheck = false;
menu.nameCheck = false;
menu.button.forEach(btn => {
btn.check = false;
});
});
}
});
// 如果有已拥有的移动端资源
if (grantProps.ownMobileResource) {
// 回显模块数据
// 获取echoDatalist
grantProps.echoDatalist = echoModuleData(grantProps.mobileResTree, grantProps.ownMobileResource);
} else {
// 没有已拥有资源时,直接使用资源树
grantProps.echoDatalist = grantProps.mobileResTree;
}
// 获取模块id
grantProps.moduleId = grantProps.mobileResTree[0].id;
// 获取表格数据
grantProps.tableData = grantProps.echoDatalist[0].menu;
// 构建索引映射
grantProps.firstShowMap = {};
grantProps.tableData.forEach((item: MobileMenu.RoleGrantMobileResourceMenu, index) => {
if (grantProps.firstShowMap[item.parentName]) {
grantProps.firstShowMap[item.parentName].push(index);
} else {
grantProps.firstShowMap[item.parentName] = [index];
}
});
}
}
/** 获取移动端资源 */
async function getMobileResources() {
if (props.user) {
// 获取用户拥有移动端资源(这里需要用户API也支持移动端资源)
// 暂时使用mobileMenuApi,实际应该在用户API中添加对应接口
await mobileMenuApi.roleOwnMobileMenu({ id: grantProps.id }).then(res => {
grantProps.ownMobileResource = res.data;
});
} else {
// 获取角色拥有移动端资源
await mobileMenuApi.roleOwnMobileMenu({ id: grantProps.id }).then(res => {
grantProps.ownMobileResource = res.data;
});
}
}
/**
* 回显模块数据
* @param mobileResTree 移动端资源树
* @param ownMobileResource 已拥有的移动端资源
*/
function echoModuleData(mobileResTree: MobileMenu.MobileResTreeSelector[], ownMobileResource: MobileMenu.RoleOwnMobileResource) {
// 通过应用循环
mobileResTree.forEach(module => {
//如果有菜单
if (module.menu) {
module.menu.forEach(menu => {
// 如果 parentId 等于自己的 id,说明是顶级菜单,设置 parentName 为模块名称
if (menu.parentId === menu.id) {
menu.parentName = module.title; // 使用模块标题作为父级名称
}
const menuCheck = ref(0);
// 如果已授权资源信息列表有数据
if (ownMobileResource.grantInfoList.length > 0) {
// 通过已授权资源信息列表循环
ownMobileResource.grantInfoList.forEach(grantInfo => {
// 如果已授权资源信息列表中的菜单id等于当前菜单id
if (menu.id == grantInfo.menuId) {
menuCheck.value++;
// 处理按钮
if (grantInfo.buttonInfo.length > 0) {
// 通过已授权资源信息列表中的按钮循环
grantInfo.buttonInfo.forEach(button => {
menu.button.forEach(itemButton => {
// 如果已授权资源信息列表中的按钮id等于当前按钮id
if (button === itemButton.id) {
itemButton.check = true;
}
});
});
}
}
});
}
// 回显前面的2个
if (menuCheck.value > 0) {
menu.parentCheck = true;
menu.nameCheck = true;
}
});
// 缓存加入索引
module.menu.forEach((item: MobileMenu.RoleGrantMobileResourceMenu, index) => {
// 下面就是用来知道不同的一级菜单里面有几个二级菜单,以及他们所在的索引
if (grantProps.firstShowMap[item.parentName]) {
grantProps.firstShowMap[item.parentName].push(index);
} else {
grantProps.firstShowMap[item.parentName] = [index];
}
});
}
});
return mobileResTree;
}
/** 选择模块 */
function moduleClick() {
// 切换模块时,更新表格数据和索引映射
let data = grantProps.echoDatalist.find(f => f.id === grantProps.moduleId)!.menu;
grantProps.tableData = data;
// 重新构建索引映射
grantProps.firstShowMap = {};
grantProps.tableData.forEach((item: MobileMenu.RoleGrantMobileResourceMenu, index) => {
if (grantProps.firstShowMap[item.parentName]) {
grantProps.firstShowMap[item.parentName].push(index);
} else {
grantProps.firstShowMap[item.parentName] = [index];
}
});
}
/** 一级目录点击 */
function changeParent(row: MobileMenu.RoleGrantMobileResourceMenu, val: any) {
row.parentCheck = val;
// 通过这个应用id,找到应用下的所有菜单
const moduleMenu = grantProps.echoDatalist.find(f => row.module === f.id);
const parentName = row.parentName;
// 获取同一级菜单的所有索引
const indexArr = grantProps.firstShowMap[parentName];
indexArr.forEach(indexItem => {
// 获取同一级菜单的所有行
const row = moduleMenu?.menu[indexItem];
if (row)
// 给这些菜单的索引去勾选
changeSub(row, val);
});
}
/** 二级菜单的点击 */
function changeSub(row: MobileMenu.RoleGrantMobileResourceMenu, val: any) {
// 选中二级菜单
row.nameCheck = val;
row.button.forEach(item => {
// 选中按钮
item.check = val;
});
}
/** 权限按钮点击 */
function changeChildCheckBox(row: MobileMenu.RoleGrantMobileResourceMenu, val: any) {
// 如果val为false,并且row的子菜单没有选中,则取消所有按钮的选中
if (!val && checkAllChildNotChecked(row)) {
// 这里注释掉勾选去掉所有按钮,联动去掉菜单
/*record.nameCheck = false
record.parentCheck = false*/
} else if (val) {
// 如果val为true,则将row的nameCheck和parentCheck属性设置为true
row.nameCheck = val;
row.parentCheck = val;
}
}
/** 检查所有子级是否都没有勾选 */
function checkAllChildNotChecked(record: MobileMenu.RoleGrantMobileResourceMenu) {
// 获取记录的子节点
const child = record.button;
// 判断子节点是否被选中,如果有一个子节点未被选中,则返回false
return child.every(field => !field.check);
}
/** 提交之前转换数据 */
function convertData() {
const params: MobileMenu.GrantMobileResourceReq = {
id: grantProps.id,
GrantInfoList: []
};
// 遍历echoDatalist,获取每一项
grantProps.echoDatalist.forEach(data => {
// 如果data有menu属性
if (data.menu) {
// 遍历data.menu,获取每一项
data.menu.forEach(item => {
// 初始化grantInfo
const grantInfo: MobileMenu.RelationRoleMobileResource = {
menuId: 0,
buttonInfo: []
};
// 如果被选中了
if (item.nameCheck) {
// 获取item的id
grantInfo.menuId = item.id;
// 遍历item.button,获取每一项
item.button.forEach(button => {
// 如果button被选中了
if (button.check) {
// 将button的id添加到grantInfo.buttonInfo中
grantInfo.buttonInfo.push(button.id);
}
});
// 将grantInfo添加到params.GrantInfoList中
params.GrantInfoList.push(grantInfo);
}
});
}
});
// 返回params
return params;
}
/** 提交 */
function handleSubmit() {
if (props.user) {
dialogVisible.value = true;
} else {
let params = convertData();
grantMobileResource(params);
}
}
/** 用户授权移动端资源提交 */
const handleUserSubmit = () => {
const param = convertData();
grantMobileResource(param);
dialogVisible.value = false;
};
/** 授权移动端资源 */
function grantMobileResource(params: MobileMenu.GrantMobileResourceReq) {
// 使用移动端菜单API进行授权
mobileMenuApi.grantRoleMobileResource(params).then(() => {
onClose();
});
}
/** 关闭表单 */
function onClose() {
visible.value = false;
grantProps.echoDatalist = [];
grantProps.moduleId = 0;
grantProps.firstShowMap = {};
grantProps.tableData = [];
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,239 @@
<!--
* @Description: 授权权限表单
* @Author: huguodong
* @Date: 2023-12-15 15:43:40
!-->
<template>
<form-container v-model="visible" :title="`授权权限->${grantProps.name}`" form-size="1200px" @close="onClose">
<el-alert
title="注:此功能界面需要与代码查询条件配合使用,并非所有接口都需设置数据范围,多用于业务模块!"
type="warning"
show-icon
style="margin-bottom: 10px"
/>
<ProTable ref="proTable" :columns="columns" :tool-button="false" :data="grantProps.tableData" row-key="api">
<!-- 自定义api表头 -->
<template #apiHeader>
<el-row class="row-bg" justify="space-between">
<el-col :span="12"><el-checkbox @change="onCheckAllChange"> 接口 </el-checkbox></el-col>
<el-col :span="12"> <el-input v-model="searchValue" @input="searchApi" placeholder="请输入接口名称" /></el-col>
</el-row>
</template>
<!-- 接口 -->
<template #api="scope">
<el-checkbox v-model="scope.row.check" @change="val => changeApi(scope.row, val)">
{{ scope.row.api }}
</el-checkbox>
</template>
<!-- 数据范围 -->
<template #dataScope="scope">
<DataScopeSelector v-if="scope.row.check" v-model="scope.row.dataScope"></DataScopeSelector>
</template>
</ProTable>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { SysRole, sysRoleApi, sysUserApi, SysUser } from "@/api";
import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import DataScopeSelector from "./dataScopeSelector.vue";
const visible = ref(false); //授权权限表单是否显示
const searchValue = ref(""); // 搜索的值
// 数据范围列表
const scopeOptions = SysRole.dataScopeOptions;
/** 授权权限表单参数 */
interface GrantPermissionFormProps {
/** id */
id: number | string;
/** 名称 */
name: string;
/** 表格数据 */
tableData: SysRole.RoleGrantPermission[];
/** 权限数据 */
permissionData: SysRole.RoleGrantPermission[];
}
//表单参数
const grantProps = reactive<GrantPermissionFormProps>({
id: 0,
name: "",
tableData: [],
permissionData: []
});
/** props */
const props = defineProps<{
/** 是否是用户 */
user?: boolean;
}>();
// 获取 ProTable DOM
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysRole.ResTreeSelector>[] = [
// { type: "selection", width: 60 },
{ prop: "api", label: "接口", width: 480, align: "left" },
{ prop: "dataScope", label: "数据范围", align: "left" }
];
/**
* 打开表单
* @param props 表单参数
*/
async function onOpen(props: FormProps.Base<SysRole.SysRoleInfo | SysUser.SysUserInfo>) {
grantProps.id = props.record.id || 0;
grantProps.name = props.record.name || "";
await loadData();
visible.value = true;
}
/** 加载数据 */
async function loadData() {
const params = { id: grantProps.id };
// 获取权限树
const tree = props.user ? await sysUserApi.permissionTreeSelector(params) : await sysRoleApi.permissionTreeSelector(params);
// 获取当前角色拥有的权限
const ownPer = props.user ? await sysUserApi.ownPermission(params) : await sysRoleApi.ownPermission(params);
// 调用echoModuleData方法,将权限树和当前角色拥有的权限进行合并
echoModuleData(tree.data, ownPer.data);
}
/**
* 回显模块数据
* @param apiList 权限列表
* @param ownPermission 已拥有的权限
*/
function echoModuleData(apiList: string[], ownPermission: SysRole.RoleOwnPermission) {
//遍历api列表
apiList.forEach(item => {
let data: SysRole.RoleGrantPermission = {
api: item,
dataScope: SysRole.dataScopeOptions[0],
check: false
};
let api = subStrApi(item); // 截取api串中的中文及括号
//找到对应的Api权限
let grantInfo = ownPermission.grantInfoList.find(grantInfo => grantInfo.apiUrl === api);
if (grantInfo) {
data.check = true;
let dataScope = scopeOptions.find(scope => scope.scopeCategory === grantInfo?.scopeCategory);
// 找到对应的数据范围
data.dataScope = { ...dataScope! }; //合并参数
// 如果scopeCategory为自定义数据范围则赋值数据范围
if (grantInfo.scopeCategory == SysRole.DataScopeEnum.SCOPE_ORG_DEFINE) {
data.dataScope.scopeDefineOrgIdList = grantInfo.scopeDefineOrgIdList;
}
}
grantProps.tableData.push(data);
});
grantProps.permissionData = grantProps.tableData; //保存权限数据
}
/** 搜索api
* @param value 搜索值
*/
function searchApi(value: string) {
console.log("[ value ] >", value);
//如果搜索值为空,就显示全部
if (!value) {
grantProps.tableData = grantProps.permissionData;
return;
}
//过滤api
grantProps.tableData = grantProps.permissionData.filter(item => {
return item.api.indexOf(value) > -1;
});
}
/** 提交之前转换数据 */
function convertData() {
//将tableData的数据合并到permissionData的中
grantProps.permissionData.forEach(item => {
let data = grantProps.tableData.find(data => data.api === item.api);
if (data) {
item.check = data.check;
item.dataScope = data.dataScope;
}
});
//定义参数
const params: SysRole.GrantPermissionReq = {
id: grantProps.id,
grantInfoList: []
};
// 遍历表格数据赋值给参数列表
grantProps.permissionData.forEach(item => {
if (item.check) {
let grantInfo: SysRole.RelationRolePermission = {
apiUrl: subStrApi(item.api),
scopeCategory: item.dataScope.scopeCategory,
scopeDefineOrgIdList: item.dataScope.scopeDefineOrgIdList
};
params.grantInfoList.push(grantInfo);
}
});
return params;
}
/** 截取api串中的中文及括号 */
function subStrApi(api: string) {
return api.substring(0, api.indexOf("[")); // 截取api串中的中文及括号
}
/** 选中api */
function changeApi(row: SysRole.RoleGrantPermission, val: any) {
row.check = val; //设置选中状态
//如果选中了
if (val) {
row.dataScope.check = true;
} else {
//去掉已选中的
row.dataScope.check = false;
if (row.dataScope.scopeCategory == SysRole.DataScopeEnum.SCOPE_ORG_DEFINE) {
row.dataScope.scopeDefineOrgIdList = [];
}
}
}
/** 全选/取消 */
function onCheckAllChange(val: any) {
grantProps.tableData.forEach(item => {
changeApi(item, val);
});
}
/** 提交 */
function handleSubmit() {
let params = convertData();
//判断是否是用户
if (props.user) {
sysUserApi.grantPermission(params).then(() => {
onClose();
});
} else {
sysRoleApi.grantPermission(params).then(() => {
onClose();
});
}
}
/** 关闭表单 */
function onClose() {
visible.value = false;
grantProps.tableData = [];
grantProps.permissionData = [];
searchValue.value = "";
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,414 @@
<!--
* @Description: 授权资源表单
* @Author: huguodong
* @Date: 2023-12-15 15:43:49
!-->
<template>
<div>
<form-container v-model="visible" :title="`授权资源->${grantProps.name}`" form-size="1200px" @close="onClose">
<el-alert title="非超管角色不可被授权系统模块菜单资源" type="warning" show-icon style="margin-bottom: 10px" />
<ProTable
ref="proTable"
:columns="columns"
:tool-button="false"
:cell-style="{ padding: '0px' }"
:row-style="{ height: '0' }"
:data="grantProps.tableData"
:pagination="false"
:span-method="objectSpanMethod"
>
<!-- 表格 header 按钮 -->
<template #tableHeader>
<s-radio-group
v-model="grantProps.moduleId"
:options="grantProps.echoDatalist"
value-key="id"
label-key="title"
button
class="mb-2"
@change="moduleClick"
>
<template #radio="{ item }">
<el-space>
<svg-icon :icon="item.icon" class="h-4 w-4"></svg-icon>
{{ item.title }}
</el-space>
</template>
</s-radio-group>
</template>
<!-- 一级目录 -->
<template #parentName="scope">
<el-checkbox v-model="scope.row.parentCheck" @change="val => changeParent(scope.row, val)">
{{ scope.row.parentName }}
</el-checkbox>
</template>
<!-- 菜单 -->
<template #title="scope">
<el-checkbox v-model="scope.row.nameCheck" @change="val => changeSub(scope.row, val)">
{{ scope.row.title }}
</el-checkbox>
</template>
<!-- 按钮授权 -->
<template #button="scope">
<template v-if="scope.row.button.length > 0">
<template v-for="(item, index) in scope.row.button" :key="item.id">
<el-checkbox v-model="item.check" style="margin-right: 5px" @change="val => changeChildCheckBox(scope.row, val)">
{{ item.title }}
</el-checkbox>
<br v-if="(index + 1) % 5 === 0" />
</template>
</template>
</template>
</ProTable>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
<el-dialog v-model="dialogVisible" title="请选择数据范围" width="30%" destroy-on-close>
<DataScopeSelector v-model="grantProps.defaultDataScope"></DataScopeSelector>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleUserSubmit"> 确定 </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { SysRole, SysUser, sysRoleApi, sysUserApi } from "@/api";
import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import type { TableColumnCtx } from "element-plus";
import DataScopeSelector from "./dataScopeSelector.vue";
const visible = ref(false); //授权资源表单是否显示
const dialogVisible = ref(false); // 数据范围弹窗显示
/** 授权资源表单参数接口 */
interface GrantResourceFormProps {
/** id */
id: number | string;
/** 名称 */
name: string;
/** 模块id */
moduleId: number | string;
/** 资源树 */
resTree: SysRole.ResTreeSelector[];
/** 已拥有的资源 */
ownResource: SysRole.RoleOwnResource | null;
/** 一级菜单索引 */
firstShowMap: Record<string, number[]>;
/** 回显数据列表 */
echoDatalist: SysRole.ResTreeSelector[];
/** 表格数据 */
tableData: SysRole.RoleGrantResourceMenu[];
//默认数据范围,用户才有
defaultDataScope: SysRole.DefaultDataScope;
}
/** 授权资源表单参数 */
const grantProps = reactive<GrantResourceFormProps>({
id: 0,
name: "",
moduleId: 0,
resTree: [],
ownResource: null,
firstShowMap: {},
echoDatalist: [],
tableData: [],
defaultDataScope: SysRole.dataScopeOptions[0]
});
/** props */
const props = defineProps<{
/** 是否是用户 */
user?: boolean;
}>();
// 获取 ProTable DOM
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysRole.ResTreeSelector>[] = [
// { type: "selection", width: 80 },
{ prop: "parentName", label: "一级目录", width: 150, align: "left" },
{ prop: "title", label: "菜单", width: 180, align: "left" },
{ prop: "button", label: "按钮授权", align: "left" }
];
/** 列合并 */
interface SpanMethodProps {
// 行
row: SysRole.RoleGrantResourceMenu;
// 列
column: TableColumnCtx<SysRole.RoleGrantResourceMenu>;
// 行索引
rowIndex: number;
// 列索引
columnIndex: number;
}
/** 列合并*/
const objectSpanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps) => {
// 如果列索引为0
if (columnIndex == 0) {
// 获取父名称
const parentName = row.parentName;
// 获取父名称的索引数组
const indexArr = grantProps.firstShowMap[parentName];
// 如果行索引等于索引数组的第一个
if (rowIndex === indexArr[0]) {
return { rowspan: indexArr.length, colspan: 1 };
} else {
return { rowspan: 0, colspan: 0 };
}
}
};
/**
* 打开表单
* @param props 表单参数
*/
async function onOpen(props: FormProps.Base<SysRole.SysRoleInfo | SysUser.SysUserInfo>) {
grantProps.id = props.record.id || 0;
grantProps.name = props.record.name || "";
await loadData();
visible.value = true;
}
/** 加载数据 */
async function loadData() {
if (grantProps.echoDatalist.length > 0) {
let data = grantProps.echoDatalist.find(f => f.id === grantProps.moduleId)!.menu;
grantProps.tableData = data;
} else {
// 获取角色资源树
await sysRoleApi.resourceTreeSelector().then(res => {
grantProps.resTree = res.data;
grantProps.moduleId = res.data[0].id;
});
await getResources(); // 获取资源
// 如果有已拥有的资源
if (grantProps.ownResource) {
// 回显模块数据
// 获取echoDatalist
grantProps.echoDatalist = echoModuleData(grantProps.resTree, grantProps.ownResource);
// 获取模块id
grantProps.moduleId = grantProps.resTree[0].id;
// 获取表格数据
grantProps.tableData = grantProps.echoDatalist[0].menu;
}
}
}
/** 获取资源 */
async function getResources() {
if (props.user) {
// 获取用户拥有资源
await sysUserApi.ownResource({ id: grantProps.id }).then(res => {
grantProps.ownResource = res.data;
});
} else {
// 获取角色拥有资源
await sysRoleApi.ownResource({ id: grantProps.id }).then(res => {
grantProps.ownResource = res.data;
});
}
}
/**
* 回显模块数据
* @param resTree 资源树
* @param ownResource 已拥有的资源
*/
function echoModuleData(resTree: SysRole.ResTreeSelector[], ownResource: SysRole.RoleOwnResource) {
// 通过应用循环
resTree.forEach(module => {
//如果有菜单
if (module.menu) {
module.menu.forEach(menu => {
const menuCheck = ref(0);
// 如果已授权资源信息列表有数据
if (ownResource.grantInfoList.length > 0) {
// 通过已授权资源信息列表循环
ownResource.grantInfoList.forEach(grantInfo => {
// 如果已授权资源信息列表中的菜单id等于当前菜单id
if (menu.id == grantInfo.menuId) {
menuCheck.value++;
// 处理按钮
if (grantInfo.buttonInfo.length > 0) {
// 通过已授权资源信息列表中的按钮循环
grantInfo.buttonInfo.forEach(button => {
menu.button.forEach(itemButton => {
// 如果已授权资源信息列表中的按钮id等于当前按钮id
if (button === itemButton.id) {
itemButton.check = true;
}
});
});
}
}
});
}
// 回显前面的2个
if (menuCheck.value > 0) {
menu.parentCheck = true;
menu.nameCheck = true;
}
});
// 缓存加入索引
module.menu.forEach((item: SysRole.RoleGrantResourceMenu, index) => {
// 下面就是用来知道不同的一级菜单里面有几个二级菜单,以及他们所在的索引
if (grantProps.firstShowMap[item.parentName]) {
grantProps.firstShowMap[item.parentName].push(index);
} else {
grantProps.firstShowMap[item.parentName] = [index];
}
});
}
});
return resTree;
}
/** 选择模块 */
function moduleClick() {
loadData();
}
/** 一级目录点击 */
function changeParent(row: SysRole.RoleGrantResourceMenu, val: any) {
row.parentCheck = val;
// 通过这个应用id,找到应用下的所有菜单
const moduleMenu = grantProps.echoDatalist.find(f => row.module === f.id);
const parentName = row.parentName;
// 获取同一级菜单的所有索引
const indexArr = grantProps.firstShowMap[parentName];
indexArr.forEach(indexItem => {
// 获取同一级菜单的所有行
const row = moduleMenu?.menu[indexItem];
if (row)
// 给这些菜单的索引去勾选
changeSub(row, val);
});
}
/** 二级菜单的点击 */
function changeSub(row: SysRole.RoleGrantResourceMenu, val: any) {
// 选中二级菜单
row.nameCheck = val;
row.button.forEach(item => {
// 选中按钮
item.check = val;
});
}
/** 权限按钮点击 */
function changeChildCheckBox(row: SysRole.RoleGrantResourceMenu, val: any) {
// 如果val为false,并且row的子菜单没有选中,则取消所有按钮的选中
if (!val && checkAllChildNotChecked(row)) {
// 这里注释掉勾选去掉所有按钮,联动去掉菜单
/*record.nameCheck = false
record.parentCheck = false*/
} else if (val) {
// 如果val为true,则将row的nameCheck和parentCheck属性设置为true
row.nameCheck = val;
row.parentCheck = val;
}
}
/** 检查所有子级是否都没有勾选 */
function checkAllChildNotChecked(record: SysRole.RoleGrantResourceMenu) {
// 获取记录的子节点
const child = record.button;
// 判断子节点是否被选中,如果有一个子节点未被选中,则返回false
return child.every(field => !field.check);
}
/** 提交之前转换数据 */
function convertData() {
const params: SysRole.GrantResourceReq = {
id: grantProps.id,
grantInfoList: []
};
// 遍历echoDatalist,获取每一项
grantProps.echoDatalist.forEach(data => {
// 如果data有menu属性
if (data.menu) {
// 遍历data.menu,获取每一项
data.menu.forEach(item => {
// 初始化grantInfo
const grantInfo: SysRole.RelationRoleResource = {
menuId: 0,
buttonInfo: []
};
// 如果被选中了
if (item.nameCheck) {
// 获取item的id
grantInfo.menuId = item.id;
// 遍历item.button,获取每一项
item.button.forEach(button => {
// 如果button被选中了
if (button.check) {
// 将button的id添加到grantInfo.buttonInfo中
grantInfo.buttonInfo.push(button.id as never);
}
});
// 将grantInfo添加到params.grantInfoList中
params.grantInfoList.push(grantInfo);
}
});
}
});
// 返回params
return params;
}
/** 提交 */
function handleSubmit() {
if (props.user) {
dialogVisible.value = true;
} else {
let params = convertData();
grantResource(params);
}
}
/** 用户授权资源提交 */
const handleUserSubmit = () => {
const param = convertData();
param.defaultDataScope = grantProps.defaultDataScope;
grantResource(param);
dialogVisible.value = false;
};
/** 授权资源 */
function grantResource(params: SysRole.GrantResourceReq) {
// 根据用户判断走哪个方法
if (props.user) {
sysUserApi.grantResource(params).then(() => {
onClose();
});
} else {
sysRoleApi.grantResource(params).then(() => {
onClose();
});
}
}
/** 关闭表单 */
function onClose() {
visible.value = false;
grantProps.echoDatalist = [];
grantProps.moduleId = 0;
grantProps.firstShowMap = {};
grantProps.tableData = [];
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+242
View File
@@ -0,0 +1,242 @@
<!--
* @Description: 角色管理
* @Author: huguodong
* @Date: 2023-12-15 15:43:53
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="组织列表"
:request-api="sysOrgApi.tree"
:default-value="initParam.orgId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" :columns="columns" :request-api="sysRoleApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="角色" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="角色"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选角色')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<el-space>
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.name}】角色`)" />
<el-dropdown @command="handleCommand">
<el-link type="primary" :underline="false" :icon="ArrowDown"> 授权 </el-link>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(item, index) in cmdEnum" :key="index" :command="command(scope.row, item)">{{ item }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 授权资源组件 -->
<GrantResource ref="grantResourceRef" />
<!-- 授权移动端资源组件 -->
<GrantMobileResource ref="grantMobileResourceRef" />
<!-- 授权权限组件 -->
<GrantPermission ref="grantPermissionRef" />
<!-- 用户选择器 -->
<user-selector
ref="userSelectorRef"
:org-tree-api="sysOrgApi.tree"
:position-tree-api="sysPositionApi.tree"
:role-tree-api="sysRoleApi.tree"
:user-selector-api="sysUserApi.selector"
multiple
@successful="handleChooseUser"
/>
</div>
</div>
</template>
<script setup lang="ts" name="sysRole">
import { sysOrgApi, sysRoleApi, SysRole, SysUser, sysUserApi, sysPositionApi } from "@/api";
import { ArrowDown } from "@element-plus/icons-vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form.vue";
import GrantResource from "./components/grantResource.vue";
import GrantMobileResource from "./components/grantMobileResource.vue";
import GrantPermission from "./components/grantPermission.vue";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";
const dictStore = useDictStore(); //字典仓库
// 角色类型选项
const roleCategoryOptions = dictStore.getDictList(SysDictEnum.ROLE_CATEGORY);
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 组织列表组件
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
interface InitParam {
orgId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ orgId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysRole.SysRoleInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "name", label: "角色名称", search: { el: "input" } },
{
prop: "category",
label: "角色分类",
enum: roleCategoryOptions,
search: { el: "select" }
},
{ prop: "code", label: "角色编码", search: { el: "input" } },
{ prop: "status", label: "状态", enum: statusOptions, search: { el: "select" } },
{ prop: "sortCode", label: "排序" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysRole.SysRoleInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(sysRoleApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.orgId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.orgId = 0;
}
}
/** 更多下拉菜单命令枚举 */
enum cmdEnum {
GrantResource = "授权资源",
// GrantMobileResource = "授权移动端资源",
GrantPermission = "授权权限",
GrantUser = "授权用户"
}
/** 下拉菜单参数接口 */
interface Command {
row: SysRole.SysRoleInfo;
command: cmdEnum;
}
/**配置command的参数 */
function command(row: SysRole.SysRoleInfo, command: cmdEnum): Command {
return {
row: row,
command: command
};
}
const grantResourceRef = ref<InstanceType<typeof GrantResource> | null>(null); //授权资源组件引用
const grantMobileResourceRef = ref<InstanceType<typeof GrantMobileResource> | null>(null); //授权移动端资源组件引用
const grantPermissionRef = ref<InstanceType<typeof GrantPermission> | null>(null); //授权权限组件引用
const userSelectorRef = ref<UserSelectorInstance>(); //用户选择器引用
const roleId = ref<number | string>(0); //角色id
/**
* 更多下拉菜单点击事件
* @param command
*/
function handleCommand(command: Command) {
switch (command.command) {
case cmdEnum.GrantUser:
roleId.value = command.row.id; //获取角色id
sysRoleApi.ownUser({ id: command.row.id }).then(res => {
userSelectorRef.value?.showSelector(res.data); //显示用户选择器
});
break;
case cmdEnum.GrantResource:
// 打开授权资源组件
grantResourceRef.value?.onOpen({
opt: FormOptEnum.EDIT,
record: command.row,
successful: RefreshTable
});
break;
case cmdEnum.GrantMobileResource:
// 打开授权移动端资源组件
grantMobileResourceRef.value?.onOpen({
opt: FormOptEnum.EDIT,
record: command.row,
successful: RefreshTable
});
break;
case cmdEnum.GrantPermission:
// 打开授权权限组件
grantPermissionRef.value?.onOpen({
opt: FormOptEnum.EDIT,
record: command.row,
successful: RefreshTable
});
break;
}
}
/** 选择用户 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
//组装参数
const grantUser: SysRole.GrantUserReq = {
id: roleId.value,
grantInfoList: data.map(item => item.id) as number[] | string[]
};
sysRoleApi.grantUser(grantUser);
}
</script>
<style lang="scss" scoped></style>
+183
View File
@@ -0,0 +1,183 @@
<!--
* @Description: 单页管理表单页面
* @Author: huguodong
* @Date: 2023-12-15 15:43:59
!-->
<template>
<form-container v-model="visible" :title="`${spaProps.opt}单页`" form-size="600px">
<el-form
ref="spaFormRef"
:rules="rules"
:disabled="spaProps.disabled"
:model="spaProps.record"
:hide-required-asterisk="spaProps.disabled"
label-width="auto"
label-suffix=" :"
>
<!-- 基本设置 -->
<el-divider content-position="left">基本设置</el-divider>
<s-form-item label="单页名称" prop="title">
<s-input v-model="spaProps.record.title"></s-input>
</s-form-item>
<s-form-item label="单页类型" prop="menuType">
<s-radio-group v-model="spaProps.record.menuType" :options="spaTypeOptions" button />
</s-form-item>
<s-form-item label="图标" prop="icon">
<SelectIconPlus v-model:icon-value="spaProps.record.icon!" />
</s-form-item>
<div v-if="spaProps.record.menuType === MenuTypeDictEnum.MENU">
<s-form-item label="路由地址" prop="path">
<s-input v-model="spaProps.record.path" placeholder="请填写路由地址,例:/home/index"></s-input>
</s-form-item>
<s-form-item label="组件名称" prop="name">
<s-input v-model="spaProps.record.name">
<template #prepend>setup name=</template>
</s-input>
</s-form-item>
<s-form-item label="组件地址" prop="component">
<s-input v-model="spaProps.record.component">
<template #prepend>src/views/</template>
</s-input>
</s-form-item>
</div>
<div v-else>
<s-form-item label="链接地址:" prop="path">
<s-input v-model="spaProps.record.path" placeholder="请填写链接地址,例:http://www.baidu.com"></s-input>
</s-form-item>
</div>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="spaProps.record.sortCode" show-input :min="1" />
</s-form-item>
<s-form-item label="说明" prop="description">
<s-input v-model="spaProps.record.description"></s-input>
</s-form-item>
<!-- 功能设置 -->
<el-divider content-position="left">功能设置</el-divider>
<el-row :gutter="24">
<el-col :span="12">
<s-form-item label="设置主页" prop="isHome">
<s-radio-group v-model="spaProps.record.isHome" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="隐藏页面" prop="isHide">
<s-radio-group v-model="spaProps.record.isHide" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="页面全屏" prop="isFull">
<s-radio-group v-model="spaProps.record.isFull" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="固定标签页" prop="isAffix">
<s-radio-group v-model="spaProps.record.isAffix" :options="yesOptions" button />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="路由缓存" prop="isKeepAlive">
<s-radio-group v-model="spaProps.record.isKeepAlive" :options="yesOptions" button />
</s-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!spaProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { Spa, spaApi } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum, SysDictEnum, MenuTypeDictEnum } from "@/enums";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 单页类型选项
const spaTypeOptions = dictStore
.getDictList(SysDictEnum.MENU_TYPE)
.filter((item: { value: MenuTypeDictEnum }) => item.value == MenuTypeDictEnum.MENU || item.value == MenuTypeDictEnum.LINK);
// 是否选项
const yesOptions = dictStore.getDictList(SysDictEnum.YES_NO, true);
// 表单参数
const spaProps = reactive<FormProps.Base<Spa.SpaInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
title: [required("请输入单页名称")],
menuType: [required("请选择单页类型")],
path: [required("请输入路由地址")],
name: [required("请输入组件名称")],
component: [required("请输入组件地址")],
sortCode: [required("请输入排序")],
icon: [required("请选择图标")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<Spa.SpaInfo>) {
Object.assign(spaProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
spaProps.record.menuType = MenuTypeDictEnum.MENU;
spaProps.record.sortCode = 99;
spaProps.record.isHome = true;
spaProps.record.isHide = true;
spaProps.record.isFull = true;
spaProps.record.isAffix = true;
spaProps.record.isKeepAlive = true;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
spaApi.detail({ id: props.record.id }).then(res => {
spaProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const spaFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
spaFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await spaApi
.submitForm(spaProps.record, spaProps.record.id != undefined)
.then(() => {
spaProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped>
:deep(.s-input-group__prepend) {
padding: 0 10px !important;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
<!--
* @Description: 单页管理
* @Author: huguodong
* @Date: 2023-12-15 15:44:05
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="单页列表" :columns="columns" :request-api="spaApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="单页" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="单页"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选页面')"
/>
</template>
<!-- 表格 菜单类型 按钮 -->
<template #menuType="scope">
<el-space wrap>
<el-tag v-if="scope.row.menuType === MenuTypeDictEnum.MENU" type="success">{{
dictStore.dictTranslation(SysDictEnum.MENU_TYPE, MenuTypeDictEnum.MENU)
}}</el-tag>
<el-tag v-else-if="scope.row.menuType === MenuTypeDictEnum.LINK" type="warning">{{
dictStore.dictTranslation(SysDictEnum.MENU_TYPE, MenuTypeDictEnum.LINK)
}}</el-tag>
<el-tag v-else type="info">{{ dictStore.dictTranslation(SysDictEnum.MENU_TYPE, scope.row.menuType) }}</el-tag>
<el-tag v-if="scope.row.isHome === true" type="danger">首页</el-tag>
</el-space>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.title}】页面`)" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</template>
<script setup lang="tsx" name="sysSpa">
import { spaApi } from "@/api";
import { Spa } from "@/api/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useDictStore } from "@/stores/modules";
import { FormOptEnum, SysDictEnum, MenuTypeDictEnum } from "@/enums";
import Form from "./components/form.vue";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const dictStore = useDictStore();
// 单页类型选项
const spaTypeOptions = dictStore
.getDictList(SysDictEnum.MENU_TYPE)
.filter((item: { value: MenuTypeDictEnum }) => item.value == MenuTypeDictEnum.MENU || item.value == MenuTypeDictEnum.LINK);
// 表格配置项
const columns: ColumnProps<Spa.SpaInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "searchKey", label: "关键字", search: { el: "input" }, isShow: false },
{ prop: "title", label: "单页名称" },
{
prop: "icon",
label: "菜单图标",
render: scope => {
return <svg-icon icon={scope.row.icon} class="h-6 w-6" />;
}
},
{
prop: "menuType",
label: "单页类型",
enum: spaTypeOptions,
search: { el: "select" }
},
{ prop: "path", label: "路由地址" },
{ prop: "component", label: "组件路径" },
{ prop: "sortCode", label: "排序" },
{ prop: "description", label: "说明" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 200, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | Spa.SpaInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(spaApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,85 @@
<!--
* @Description: 权限按钮批量表单页面
* @Author: huguodong
* @Date: 2023-12-15 15:42:29
!-->
<template>
<el-dialog v-model="visible" title="批量新增按钮" width="500px">
<el-form ref="buttonFormRef" :rules="rules" :model="buttonProps" label-width="auto">
<s-form-item label="权限简称" prop="title">
<s-input v-model="buttonProps.title"></s-input>
</s-form-item>
<s-form-item label="编码前缀" prop="code">
<s-input v-model="buttonProps.code"></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button primary @click="handleSubmit"> 确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { mobileButtonApi, Button } from "@/api";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus/es/components/form";
const visible = ref(false); //是否显示表单
// 表单参数接口
interface ButtonProps extends Button.Batch {
/** 成功之后的方法 */
successful?: () => void;
}
// 表单参数
const buttonProps = reactive<ButtonProps>({
parentId: 0,
title: "",
code: ""
});
// 表单验证规则
const rules = reactive({
title: [required("请输入权限简称")],
code: [required("请输入编码前缀")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: ButtonProps) {
Object.assign(buttonProps, props); //合并参数
visible.value = true;
}
// 提交数据(新增/编辑)
const buttonFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
buttonFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await mobileButtonApi
.batch(buttonProps)
.then(() => {
buttonProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,104 @@
<!--
* @Description: 权限按钮表单页面
* @Author: huguodong
* @Date: 2023-12-15 15:42:36
!-->
<template>
<el-dialog v-model="visible" :title="`${buttonProps.opt}按钮`" width="500px">
<el-form
ref="buttonFormRef"
:rules="rules"
:disabled="buttonProps.disabled"
:model="buttonProps.record"
:hide-required-asterisk="buttonProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="按钮名称" prop="title">
<s-input v-model="buttonProps.record.title"></s-input>
</s-form-item>
<s-form-item label="按钮编码" prop="code">
<s-input v-model="buttonProps.record.code"></s-input>
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="buttonProps.record.sortCode" show-input :min="1" />
</s-form-item>
<s-form-item label="说明" prop="description">
<s-input v-model="buttonProps.record.description"></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!buttonProps.disabled" primary @click="handleSubmit"> 确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { mobileButtonApi, Button } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum } from "@/enums";
import { FormInstance } from "element-plus/es/components/form";
const visible = ref(false); //是否显示表单
// 表单参数
const buttonProps = reactive<FormProps.Base<Button.ButtonInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
title: [required("请输入单页名称")],
code: [required("请输入按钮编码")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<Button.ButtonInfo>) {
Object.assign(buttonProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
buttonProps.record.sortCode = 99;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
mobileButtonApi.detail({ id: props.record.id }).then(res => {
buttonProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const buttonFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
buttonFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await mobileButtonApi
.submitForm(buttonProps.record, buttonProps.record.id != undefined)
.then(() => {
buttonProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+142
View File
@@ -0,0 +1,142 @@
<!--
* @Description: 权限按钮管理
* @Author: huguodong
* @Date: 2023-12-15 15:42:52
!-->
<template>
<div>
<form-container v-model="visible" title="权限按钮" form-size="60%">
<div class="table-box min-h-300px">
<ProTable ref="proTable" title="按钮列表" class="table" :columns="columns" :request-api="getPage">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="按钮" @click="onOpen(FormOptEnum.ADD)" />
<s-button @click="onOpenBatch()"> 批量新增 </s-button>
<s-button
type="danger"
plain
suffix="按钮"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选按钮')"
/>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.title}】按钮`)" />
</template>
</ProTable>
</div>
<template #footer>
<el-button type="primary" @click="onClose"> 确定 </el-button>
</template>
</form-container>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 批量新增 -->
<Batch ref="batchRef" />
</div>
</template>
<script setup lang="ts">
import { mobileButtonApi, Button } from "@/api";
import { useHandleData } from "@/hooks/useHandleData";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { FormOptEnum } from "@/enums";
import Form from "./components/form.vue";
import Batch from "./components/batch.vue";
const visible = ref(false); //是否显示表单
const parentId = ref<number | string>(0); //按钮父Id
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<Button.ButtonInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "title", label: "名称" },
{ prop: "code", label: "编码" },
{ prop: "sortCode", label: "排序" },
{ prop: "description", label: "说明" },
{ prop: "operation", label: "操作", width: 150, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | Button.ButtonInfo = { parentId: parentId.value }) {
formRef.value?.onOpen({
opt: opt,
record: record,
successful: () => RefreshTable()
});
}
// 批量新增引用
const batchRef = ref<InstanceType<typeof Batch> | null>(null);
/**
* 打开批量新增表单
*/
function onOpenBatch() {
batchRef.value?.onOpen({
parentId: parentId.value,
title: "",
code: "",
successful: RefreshTable
});
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(mobileButtonApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
/**
* 打开表单
* @param props 表单参数
*/
function onOpenTable(menuId: number | string) {
visible.value = true;
parentId.value = menuId;
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
/**
* 获取按钮分页数据
* 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
* 默认不做操作就直接在 ProTable 组件上绑定 :requestApi="getUserList"
*/
function getPage(params: any) {
let newParams = JSON.parse(JSON.stringify(params)); //转换成json字符串再转换成json对象
newParams.parentId = parentId.value; //按钮父Id
return mobileButtonApi.page(newParams);
}
// 暴露给父组件的方法
defineExpose({
onOpenTable
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,94 @@
<!--
* @Description: 更改菜单模块
* @Author: huguodong
* @Date: 2025-07-02 13:52:09
!-->
<template>
<form-container v-model="visible" :title="'更改模块->' + changeModuleProps.title" form-size="450px">
<el-form ref="changeModuleFormRef" :rules="rules" :model="changeModuleProps" class="mt-20px">
<s-form-item label="所属模块" prop="module">
<s-select v-model="changeModuleProps.module" :options="changeModuleProps.moduleOptions"></s-select>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { mobileMenuApi } from "@/api";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus/es/components/form";
const visible = ref(false); //是否显示表单
/** 表单参数 */
interface ChangeModuleProps {
/** 菜单id */
id: number | string;
/** 模块id */
module: number | string;
/** 菜单名称 */
title: string;
/** 模块列表 */
moduleOptions: { label: string; value: number | string }[];
/** 表单实例 */
successful?: () => void;
}
/** 表单参数 */
const changeModuleProps = reactive<ChangeModuleProps>({
id: 0,
module: 0,
title: "",
moduleOptions: []
});
// 表单验证规则
const rules = reactive({
module: [required("请选择所属模块")]
});
// 提交数据(新增/编辑)
const changeModuleFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
changeModuleFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await mobileMenuApi
.changeModule({
id: changeModuleProps.id,
module: changeModuleProps.module
})
.then(() => {
//提交成功
changeModuleProps.successful?.();
})
.finally(() => {
onClose();
});
});
}
/** 打开表单
* @param title 菜单名称
* @param module 模块id
*/
function onOpen(moduleProps: ChangeModuleProps) {
Object.assign(changeModuleProps, moduleProps); //合并参数
visible.value = true; //显示表单
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,161 @@
<!--
* @Description: 菜单管理表单页面
* @Author: superAdmin
* @Date: 2025-06-26 16:41:59
!-->
<template>
<form-container v-model="visible" :title="`${menuProps.opt}菜单`" form-size="600px">
<el-form
ref="menuFormRef"
:rules="rules"
:disabled="menuProps.disabled"
:model="menuProps.record"
:hide-required-asterisk="menuProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="菜单名称" prop="title">
<s-input v-model="menuProps.record.title"></s-input>
</s-form-item>
<s-form-item label="菜单类型" prop="menuType">
<s-radio-group v-model="menuProps.record.menuType" :options="menuTypeOptions" button />
</s-form-item>
<s-form-item label="上级菜单" prop="parentId">
<MenuSelector
v-model:menu-value="menuProps.record.parentId"
show-all
@change="changeMenu"
:menu-tree-api="() => mobileMenuApi.menuTreeSelector({ module: module })"
/>
</s-form-item>
<s-form-item label="路径" prop="path" v-if="isMenu">
<s-input v-model="menuProps.record.path"></s-input>
</s-form-item>
<s-form-item label="图标" prop="icon">
<SelectIconPlus v-model:icon-value="menuProps.record.icon!" />
</s-form-item>
<s-form-item label="颜色" prop="color">
<color-picker v-model:value="menuProps.record.color" />
</s-form-item>
<s-form-item label="排序码" prop="sortCode">
<el-slider v-model="menuProps.record.sortCode" show-input :min="1" />
</s-form-item>
<s-form-item label="正规则" prop="regType">
<s-radio-group v-model="menuProps.record.regType" :options="regTypeOptions" button />
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="menuProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="描述" prop="description">
<s-input v-model="menuProps.record.description"></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!menuProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { mobileMenuApi, MobileModule } from "@/api";
import { FormOptEnum, MenuTypeDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore();
const menuTypeOptions = dictStore
.getDictList("MENU_TYPE")
.filter((item: { value: MenuTypeDictEnum }) => item.value == MenuTypeDictEnum.MENU || item.value == MenuTypeDictEnum.CATALOG); //菜单类型选项
const regTypeOptions = dictStore.getDictList("YES_NO"); //正规则选项
const statusOptions = dictStore.getDictList("COMMON_STATUS"); //状态选项
const isMenu = computed(() => menuProps.record.menuType === MenuTypeDictEnum.MENU); //是否是菜单
// 模块id
const module = ref<number | string>(0);
// 表单参数
const menuProps = reactive<FormProps.Base<MobileModule.MobileModuleInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
title: [required("请输入菜单名称")],
parentId: [required("请选择上级菜单")],
menuType: [required("请选择菜单类型")],
path: [required("请输入路径")],
regType: [required("请选择正规则")],
color: [required("请选择颜色")],
sortCode: [required("请输入排序")],
icon: [required("请选择图标")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<MobileModule.MobileModuleInfo>, moduleId: number | string) {
Object.assign(menuProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
menuProps.record.menuType = menuTypeOptions[0].value;
//如果是新增,设置默认值
//如果是新增,设置默认值
menuProps.record.sortCode = 99;
menuProps.record.status = statusOptions[0].value;
menuProps.record.menuType = MenuTypeDictEnum.MENU;
menuProps.record.sortCode = 99;
menuProps.record.module = moduleId;
menuProps.record.regType = regTypeOptions[0].value;
menuProps.record.color = "#1890ff"; //默认颜色
}
module.value = moduleId; //设置模块id
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
mobileMenuApi.detail({ id: props.record.id }).then(res => {
menuProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const menuFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
menuFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await mobileMenuApi
.submitForm(menuProps.record, menuProps.record.id != undefined)
.then(() => {
menuProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
/** 菜单选择事件
* @param path 路由地址
*/
function changeMenu(path: string) {
menuProps.record.path = path;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
+228
View File
@@ -0,0 +1,228 @@
<!--
* @Description: 菜单管理
* @Author: superAdmin
* @Date: 2025-06-26 16:41:59
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="菜单列表" :columns="columns" :request-api="getTableList" :request-auto="false" :pagination="false">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="菜单" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="菜单"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选菜单')"
/>
</template>
<!-- 表格 菜单类型 按钮 -->
<template #menuType="scope">
<el-space>
<!-- <el-tag>{{ dictStore.dictTranslation(SysDictEnum.MENU_TYPE, scope.row.menuType) }}</el-tag> -->
<el-tag v-if="scope.row.menuType === MenuTypeDictEnum.MENU" type="success">{{
dictStore.dictTranslation(SysDictEnum.MENU_TYPE, scope.row.menuType)
}}</el-tag>
<el-tag v-else type="info">{{ dictStore.dictTranslation(SysDictEnum.MENU_TYPE, scope.row.menuType) }}</el-tag>
</el-space>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 图标 -->
<template #icon="scope">
<svg-icon :icon="scope.row.icon" :color="scope.row.color" class="h-6 w-8" />
</template>
<!-- 颜色 -->
<template #color="scope">
<el-tag :color="scope.row.color" effect="dark" :style="{ borderColor: scope.row.color }" size="large">{{ scope.row.color }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<el-space>
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.title}】菜单`)" />
<el-dropdown @command="handleCommand">
<el-link type="primary" :underline="false" :icon="ArrowDown"> 更多 </el-link>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-if="scope.row.parentId === 0" :command="command(scope.row, cmdEnum.ChangeModule)">{{
cmdEnum.ChangeModule
}}</el-dropdown-item>
<el-dropdown-item v-if="isMenu(scope.row.menuType)" :command="command(scope.row, cmdEnum.Button)">{{
cmdEnum.Button
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 更改模块 -->
<ChangeModule ref="changeModuleFormRef" />
<!-- 权限按钮 -->
<Button ref="buttonFormRef" />
</div>
</template>
<script setup lang="ts" name="mobileMenu">
import { MobileMenu, mobileMenuApi } from "@/api";
import { ArrowDown } from "@element-plus/icons-vue";
import { FormOptEnum, CommonStatusEnum, SysDictEnum, MenuTypeDictEnum } from "@/enums";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import Form from "./components/form.vue";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import ChangeModule from "./components/changeModule.vue";
import Button from "../button/index.vue";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const dictStore = useDictStore();
const menuTypeOptions = dictStore.getDictList("MENU_TYPE"); //菜单类型选项
const regTypeOptions = dictStore.getDictList("YES_NO"); //正规则选项
const statusOptions = dictStore.getDictList("COMMON_STATUS"); //状态选项
const moduleOptions = ref<{ label: string; value: string | number }[]>([]);
const columns: ColumnProps<MobileMenu.MobileMenuInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "id", label: "Id", isShow: false },
{
prop: "module",
label: "应用模块",
enum: moduleOptions,
search: {
el: "select",
props: {
clearable: false,
placeholder: "请选择应用模块"
}
},
isShow: false
},
{ prop: "title", label: "菜单名称", width: 150, align: "left", search: { el: "input" } },
{ prop: "menuType", label: "菜单类型", enum: menuTypeOptions, search: { el: "select" } },
{ prop: "path", label: "路径" },
{ prop: "icon", label: "图标" },
{ prop: "color", label: "颜色" },
{ prop: "sortCode", label: "排序码" },
{ prop: "regType", label: "正规则", enum: regTypeOptions },
{ prop: "status", label: "状态", enum: statusOptions },
{ prop: "description", label: "描述" },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
onMounted(() => {
// 获取应用模块下拉选项
mobileMenuApi.moduleSelector().then(res => {
moduleOptions.value = res.data.map(item => ({ label: item.title, value: item.id }));
// 设置默认值(取第一个模块)
if (moduleOptions.value.length > 0) {
// 1. 赋值初始化参数
proTable.value!.searchInitParam["module"] = moduleOptions.value[0].value;
// 2. 重置参数(使新参数生效)
proTable.value?.resetParam();
proTable.value?.getTableList();
}
});
});
// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
// 默认不做操作就直接在 ProTable 组件上绑定 :requestApi="getUserList"
const getTableList = (params: any) => {
let newParams = JSON.parse(JSON.stringify(params));
return mobileMenuApi.tree(newParams);
};
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | MobileMenu.MobileMenuInfo = {}) {
console.log("[ proTable.value?.searchParam.module ] >", proTable.value?.searchParam.module);
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable }, proTable.value?.searchParam.module);
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(mobileMenuApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
/**判断是否是菜单 */
const isMenu = computed(() => (menuType: string) => {
return menuType === MenuTypeDictEnum.MENU;
});
/** 更多下拉菜单命令枚举 */
enum cmdEnum {
ChangeModule = "更改模块",
Button = "权限按钮"
}
/** 下拉菜单参数接口 */
interface Command {
row: MobileMenu.MobileMenuInfo;
command: cmdEnum;
}
/**配置command的参数 */
function command(row: MobileMenu.MobileMenuInfo, command: cmdEnum): Command {
return {
row: row,
command: command
};
}
// 修改模块表单引用
const changeModuleFormRef = ref<InstanceType<typeof ChangeModule> | null>(null);
// 权限按钮表单引用
const buttonFormRef = ref<InstanceType<typeof Button> | null>(null);
/**
* 更多下拉菜单点击事件
* @param command
*/
function handleCommand(command: Command) {
switch (command.command) {
case cmdEnum.ChangeModule:
//更改模块
changeModuleFormRef.value?.onOpen({
moduleOptions: moduleOptions.value,
id: command.row.id,
title: command.row.title,
module: command.row.module,
successful: () => RefreshTable()
});
break;
case cmdEnum.Button:
//权限按钮
buttonFormRef.value?.onOpenTable(command.row.id);
break;
}
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,119 @@
<!--
* @Description: 模块管理表单页面
* @Author: superAdmin
* @Date: 2025-06-25 14:37:52
!-->
<template>
<form-container v-model="visible" :title="`${moduleProps.opt}模块`" form-size="600px">
<el-form
ref="moduleFormRef"
:rules="rules"
:disabled="moduleProps.disabled"
:model="moduleProps.record"
:hide-required-asterisk="moduleProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="名称" prop="title">
<s-input v-model="moduleProps.record.title"></s-input>
</s-form-item>
<s-form-item label="图标" prop="icon">
<SelectIconPlus v-model:icon-value="moduleProps.record.icon!" />
</s-form-item>
<s-form-item label="颜色" prop="color">
<color-picker v-model:value="moduleProps.record.color" />
</s-form-item>
<s-form-item label="描述" prop="description">
<s-input v-model="moduleProps.record.description"></s-input>
</s-form-item>
<s-form-item label="排序码" prop="sortCode">
<el-slider v-model="moduleProps.record.sortCode" show-input :min="1" />
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="moduleProps.record.status" :options="statusOptions" button />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!moduleProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { mobileModuleApi, MobileModule } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const dictStore = useDictStore(); //字典仓库
const visible = ref(false); //
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const moduleProps = reactive<FormProps.Base<MobileModule.MobileModuleInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
title: [required("请输入显示名称")],
sortCode: [required("请输入排序")],
icon: [required("请选择图标")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<MobileModule.MobileModuleInfo>) {
Object.assign(moduleProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
moduleProps.record.sortCode = 99;
moduleProps.record.status = statusOptions[0].value;
moduleProps.record.color = "#1890ff"; //默认颜色
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
mobileModuleApi.detail({ id: props.record.id }).then(res => {
moduleProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const moduleFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
moduleFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await mobileModuleApi
.submitForm(moduleProps.record, moduleProps.record.id != undefined)
.then(() => {
moduleProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
+106
View File
@@ -0,0 +1,106 @@
<!--
* @Description: 模块管理
* @Author: superAdmin
* @Date: 2025-06-25 14:37:52
!-->
<template>
<div class="table-box">
<ProTable ref="proTable" title="模块列表" :columns="columns" :request-api="mobileModuleApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="模块" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="模块"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选模块')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 图标 -->
<template #icon="scope">
<svg-icon :icon="scope.row.icon" :color="scope.row.color" class="h-6 w-8" />
</template>
<!-- 颜色 -->
<template #color="scope">
<el-tag :color="scope.row.color" effect="dark" :style="{ borderColor: scope.row.color }" size="large">{{ scope.row.color }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除所选模块`)" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</template>
<script setup lang="tsx" name="mobileModule">
import { mobileModuleApi, MobileModule } from "@/api";
import { FormOptEnum, CommonStatusEnum, SysDictEnum } from "@/enums";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import Form from "./components/form.vue";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
const dictStore = useDictStore(); //字典仓库
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const columns: ColumnProps<MobileModule.MobileModuleInfo>[] = [
{ type: "selection", fixed: "left", width: 80 },
{ prop: "title", label: "显示名称", search: { el: "input" } },
{ prop: "description", label: "描述" },
{
prop: "icon",
label: "菜单图标"
},
{
prop: "color",
label: "颜色"
},
{ prop: "sortCode", label: "排序码" },
{ prop: "status", label: "状态" },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | MobileModule.MobileModuleInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(mobileModuleApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,93 @@
<!--
* @Description:连接配置
* @Author: huguodong
* @Date: 2024-11-07 13:55:56
!-->
<template>
<el-form ref="baseFormRef" :rules="rules" :model="baseFormProps" label-width="auto" label-suffix=" :" label-position="top">
<s-form-item label="MQTT服务端地址" prop="MQTT_PARAM_URL">
<s-input v-model="baseFormProps.MQTT_PARAM_URL"></s-input>
</s-form-item>
<s-form-item label="MQTT用户名" prop="MQTT_PARAM_USERNAME">
<s-input v-model="baseFormProps.MQTT_PARAM_USERNAME"></s-input>
</s-form-item>
<s-form-item label="MQTT密码" prop="MQTT_PARAM_PASSWORD">
<s-input v-model="baseFormProps.MQTT_PARAM_PASSWORD"></s-input>
</s-form-item>
<el-form-item>
<el-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</el-button>
<el-button style="margin-left: 10px" @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { SysConfig, sysConfigApi } from "@/api";
import { FormInstance } from "element-plus";
const submitLoading = ref(false); //提交按钮loading
const baseFormRef = ref<FormInstance>();
import { SysConfigTypeEnum } from "@/enums";
/**
* @description: 连接配置参数
*/
const baseFormProps = reactive<SysConfig.MqttPolicyConfig>({
MQTT_PARAM_URL: "",
MQTT_PARAM_USERNAME: "",
MQTT_PARAM_PASSWORD: ""
});
//props定义
const props = defineProps({
baseConfig: {
type: Array as PropType<SysConfig.ConfigInfo[]>,
required: true,
default: () => []
}
});
//监听loginPolicy变化
watch(
() => props.baseConfig,
(newVal: SysConfig.ConfigInfo[]) => {
//重新赋值
newVal.forEach((item: SysConfig.ConfigInfo) => {
(baseFormProps[item.configKey as keyof SysConfig.MqttPolicyConfig] as string) = item.configValue;
});
}
);
// 表单验证规则
const rules = reactive({
MQTT_PARAM_URL: [{ required: true, message: "请输入MQTT服务端地址", trigger: "blur" }],
MQTT_PARAM_USERNAME: [{ required: true, message: "请输入MQTT用户名", trigger: "blur" }],
MQTT_PARAM_PASSWORD: [{ required: true, message: "请输入MQTT密码", trigger: "blur" }]
});
/** 提交表单 */
function onSubmit() {
baseFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
submitLoading.value = true;
//组装参数
const param: SysConfig.ConfigInfo[] = Object.entries(baseFormProps).map(item => {
return {
id: 0,
category: SysConfigTypeEnum.MQTT_POLICY,
configKey: item[0],
configValue: typeof item[1] === "object" ? JSON.stringify(item[1]) : String(item[1])
};
});
//提交数据
sysConfigApi.configEditForm(param).finally(() => {
submitLoading.value = false;
});
});
}
/** 重置表单 */
function resetForm() {
baseFormRef.value?.resetFields();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,46 @@
<!--
* @Description: 安全配置
* @Author: huguodong
* @Date: 2024-01-31 15:33:50
!-->
<template>
<el-tabs v-model="activeName" class="demo-tabs" tab-position="left">
<el-tab-pane label="连接配置" name="base" class="pl-5"><BaseForm :base-config="baseConfig" /></el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts">
import { SysConfig } from "@/api";
import BaseForm from "./baseForm.vue";
import { SysConfigTypeEnum } from "@/enums";
const activeName = ref("base");
const baseConfig = ref<SysConfig.ConfigInfo[]>([]);
//props定义
const props = defineProps({
sysConfigs: {
type: Array as PropType<SysConfig.ConfigInfo[]>,
required: true,
default: () => []
}
});
//监听sysConfigs变化
watch(
() => props.sysConfigs,
(newVal: SysConfig.ConfigInfo[]) => {
// 配置赋值
baseConfig.value = newVal.filter(item => item.category == SysConfigTypeEnum.MQTT_POLICY);
}
);
</script>
<style lang="scss" scoped>
.demo-tabs > .el-tabs__content {
padding: 32px;
font-size: 132px;
font-weight: 600;
color: #6b778c;
}
</style>
@@ -0,0 +1,104 @@
<!--
* @Description: 配置表单
* @Author: huguodong
* @Date: 2024-02-02 10:49:28
!-->
<template>
<form-container v-model="visible" :title="`${configProps.opt}配置`" form-size="600px">
<el-form
ref="configFormRef"
:rules="rules"
:disabled="configProps.disabled"
:model="configProps.record"
:hide-required-asterisk="configProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="配置键" prop="configKey">
<s-input v-model="configProps.record.configKey"></s-input>
</s-form-item>
<s-form-item label="配置值" prop="configValue">
<s-input v-model="configProps.record.configValue"></s-input>
</s-form-item>
<s-form-item label="备注" prop="remark">
<s-input v-model="configProps.record.remark"></s-input>
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="configProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!configProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { SysConfig, sysConfigApi } from "@/api";
import { required } from "@/utils/formRules";
import { FormOptEnum } from "@/enums";
import { FormInstance } from "element-plus";
const visible = ref(false); //是否显示表单
// 表单参数接口
interface ConfigProps extends FormProps.Base<SysConfig.ConfigInfo> {}
// 表单参数
const configProps = reactive<ConfigProps>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
configKey: [required("请输入配置键")],
configValue: [required("请输入配置值")],
sortCode: [required("请输入排序")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: ConfigProps) {
Object.assign(configProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
configProps.record.sortCode = 99;
}
visible.value = true; //显示表单
}
// 提交数据(新增/编辑)
const configFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
configFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await sysConfigApi
.submitForm(configProps.record, configProps.record.id != undefined)
.then(() => {
configProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,80 @@
<!--
* @Description: 其他配置
* @Author: huguodong
* @Date: 2024-02-02 08:43:44
!-->
<template>
<div class="table-box min-h-300px">
<ProTable ref="proTable" class="table" :columns="columns" :request-api="sysConfigApi.page">
<!-- 表格 header 按钮 -->
<template #tableHeader>
<s-button :suffix="title" @click="onOpen(FormOptEnum.ADD)" />
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete(scope.row.id, `删除【${scope.row.configKey}】配置`)" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</template>
<script setup lang="ts">
import { sysConfigApi, SysConfig } from "@/api";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { FormOptEnum } from "@/enums";
import Form from "./form.vue";
const title = "配置";
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysConfig.ConfigInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "configKey", label: "配置键" },
{ prop: "configValue", label: "配置值" },
{ prop: "remark", label: "备注" },
{ prop: "sortCode", label: "排序", width: 80 },
{ prop: "operation", label: "操作", fixed: "right", width: 150 }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysConfig.ConfigInfo = {}) {
formRef.value?.onOpen({
opt: opt,
record: record,
successful: RefreshTable //刷新子表格
});
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(id: number | string, msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(sysConfigApi.delete, { id }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新主表格
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,53 @@
<!--
* @Description: 安全配置
* @Author: huguodong
* @Date: 2024-01-31 15:33:50
!-->
<template>
<el-tabs v-model="activeName" class="demo-tabs" tab-position="left">
<el-tab-pane label="登录策略" name="login" class="pl-5"><LoginForm :login-policy="loginPolicy" /></el-tab-pane>
<el-tab-pane label="密码策略" name="pwd" class="pl-5"><PwdForm :pwd-policy="pwdPolicy" /></el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts">
import { SysConfig } from "@/api";
import LoginForm from "./loginForm.vue";
import PwdForm from "./pwdForm.vue";
import { SysConfigTypeEnum } from "@/enums";
const activeName = ref("login");
const loginPolicy = ref<SysConfig.ConfigInfo[]>([]);
const pwdPolicy = ref<SysConfig.ConfigInfo[]>([]);
//props定义
const props = defineProps({
sysConfigs: {
type: Array as PropType<SysConfig.ConfigInfo[]>,
required: true,
default: () => []
}
});
//监听sysConfigs变化
watch(
() => props.sysConfigs,
(newVal: SysConfig.ConfigInfo[]) => {
// 配置赋值
loginPolicy.value = newVal.filter(item => item.category == SysConfigTypeEnum.LOGIN_POLICY);
pwdPolicy.value = newVal.filter(item => item.category == SysConfigTypeEnum.PWD_POLICY);
}
);
</script>
<style lang="scss" scoped>
.demo-tabs > .el-tabs__content {
padding: 32px;
font-size: 132px;
font-weight: 600;
color: #6b778c;
}
:deep(.el-tabs__nav-scroll) {
height: 500px;
}
</style>
@@ -0,0 +1,119 @@
<!--
* @Description: 登录策略
* @Author: huguodong
* @Date: 2024-01-31 16:25:48
!-->
<template>
<el-form ref="loginFormRef" :rules="rules" :model="loginFormProps" label-width="auto" label-suffix=" :" label-position="top">
<s-form-item label="单用户登录开关" prop="LOGIN_SINGLE_OPEN">
<s-radio-group v-model="loginFormProps.LOGIN_SINGLE_OPEN" :options="yesNoOptions" />
</s-form-item>
<s-form-item label="允许登录错误次数" prop="LOGIN_ERROR_COUNT">
<el-input-number v-model.number="loginFormProps.LOGIN_ERROR_COUNT" :min="1" :max="999" />
</s-form-item>
<s-form-item label="登录错误重置时间(分)" prop="LOGIN_ERROR_RESET_TIME">
<el-input-number v-model.number="loginFormProps.LOGIN_ERROR_RESET_TIME" :min="1" :max="999" />
</s-form-item>
<s-form-item label="登录错误次数过多账号锁定时长:(分)" prop="LOGIN_ERROR_LOCK">
<el-input-number v-model.number="loginFormProps.LOGIN_ERROR_LOCK" :min="1" :max="999" />
</s-form-item>
<s-form-item label="登录验证码开关" prop="LOGIN_CAPTCHA_OPEN">
<s-radio-group v-model="loginFormProps.LOGIN_CAPTCHA_OPEN" :options="yesNoOptions" />
</s-form-item>
<s-form-item label="登录验证码类型" prop="LOGIN_CAPTCHA_TYPE">
<s-radio-group v-model="loginFormProps.LOGIN_CAPTCHA_TYPE" :options="captchaOptions" />
</s-form-item>
<el-form-item>
<el-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</el-button>
<el-button style="margin-left: 10px" @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { SysConfig, sysConfigApi } from "@/api";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import { SysConfigTypeEnum, SysDictEnum } from "@/enums";
const dictStore = useDictStore(); //字典仓库
// 字典类型选项
const yesNoOptions = dictStore.getDictList(SysDictEnum.YES_NO);
const captchaOptions = dictStore.getDictList(SysDictEnum.CAPTCHA_TYPE);
const submitLoading = ref(false); //提交按钮loading
const loginFormRef = ref<FormInstance>();
/** 登录策略参数 */
const loginFormProps = reactive<SysConfig.LoginPolicyConfig>({
LOGIN_SINGLE_OPEN: yesNoOptions[0].value,
LOGIN_CAPTCHA_OPEN: yesNoOptions[0].value,
LOGIN_CAPTCHA_TYPE: captchaOptions[0].value,
LOGIN_ERROR_COUNT: 5,
LOGIN_ERROR_RESET_TIME: 5,
LOGIN_ERROR_LOCK: 30
});
//props定义
const props = defineProps({
loginPolicy: {
type: Array as PropType<SysConfig.ConfigInfo[]>,
required: true,
default: () => []
}
});
//监听loginPolicy变化
watch(
() => props.loginPolicy,
(newVal: SysConfig.ConfigInfo[]) => {
//重新赋值
newVal.forEach((item: SysConfig.ConfigInfo) => {
let prop = loginFormProps[item.configKey as keyof SysConfig.LoginPolicyConfig];
//如果是number类型就转为number
if (typeof prop === "number") {
(loginFormProps[item.configKey as keyof SysConfig.LoginPolicyConfig] as number) = Number(item.configValue);
} else {
(loginFormProps[item.configKey as keyof SysConfig.LoginPolicyConfig] as string) = item.configValue;
}
});
}
);
// 表单验证规则
const rules = reactive({
LOGIN_ERROR_COUNT: [{ required: true, message: "请输入允许登录错误次数", trigger: "blur" }],
LOGIN_ERROR_RESET_TIME: [{ required: true, message: "请输入登录错误重置时间", trigger: "blur" }],
LOGIN_ERROR_LOCK: [{ required: true, message: "请输入登录错误次数过多账号锁定时长", trigger: "blur" }],
LOGIN_CAPTCHA_TYPE: [{ required: true, message: "请选择登录验证码类型", trigger: "blur" }],
LOGIN_SINGLE_OPEN: [{ required: true, message: "请选择单用户登录开关", trigger: "blur" }],
LOGIN_CAPTCHA_OPEN: [{ required: true, message: "请选择登录验证码开关", trigger: "blur" }]
});
/** 提交表单 */
function onSubmit() {
loginFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
submitLoading.value = true;
//组装参数
const param: SysConfig.ConfigInfo[] = Object.entries(loginFormProps).map(item => {
return {
id: 0,
category: SysConfigTypeEnum.LOGIN_POLICY,
configKey: item[0],
configValue: typeof item[1] === "object" ? JSON.stringify(item[1]) : String(item[1])
};
});
//提交数据
sysConfigApi.configEditForm(param).finally(() => {
submitLoading.value = false;
});
});
}
/** 重置表单 */
function resetForm() {
loginFormRef.value?.resetFields();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,143 @@
<!--
* @Description: 密码策略
* @Author: huguodong
* @Date: 2024-01-31 16:25:33
!-->
<template>
<el-form ref="pwdFormRef" :rules="rules" :model="pwdFormProps" label-width="auto" label-suffix=" :" label-position="top">
<s-form-item label="系统默认密码" prop="PWD_DEFAULT_PASSWORD" class="w-50">
<s-input v-model="pwdFormProps.PWD_DEFAULT_PASSWORD"></s-input>
</s-form-item>
<s-form-item label="密码定期提醒更新" prop="PWD_REMIND">
<s-radio-group v-model="pwdFormProps.PWD_REMIND" :options="yesNoOptions" />
</s-form-item>
<s-form-item label="密码定期提醒更新时间(天)" prop="LOGIN_ERROR_LOCK">
<el-input-number v-model.number="pwdFormProps.PWD_REMIND_DAY" :min="1" :max="999" />
</s-form-item>
<s-form-item label="修改初始密码提醒" prop="PWD_UPDATE_DEFAULT">
<s-radio-group v-model="pwdFormProps.PWD_UPDATE_DEFAULT" :options="yesNoOptions" />
</s-form-item>
<s-form-item label="密码最小长度" prop="PWD_MIN_LENGTH">
<el-input-number v-model.number="pwdFormProps.PWD_MIN_LENGTH" :min="6" :max="20" />
</s-form-item>
<el-row :gutter="16">
<el-col :span="6">
<s-form-item label="是否含数字" prop="PWD_CONTAIN_NUM">
<s-radio-group v-model="pwdFormProps.PWD_CONTAIN_NUM" :options="yesNoOptions" />
</s-form-item>
</el-col>
<el-col :span="6">
<s-form-item label="是否包含小写字母" prop="PWD_CONTAIN_LOWER">
<s-radio-group v-model="pwdFormProps.PWD_CONTAIN_LOWER" :options="yesNoOptions" />
</s-form-item>
</el-col>
<el-col :span="6">
<s-form-item label="是否包含大写字母" prop="PWD_CONTAIN_UPPER">
<s-radio-group v-model="pwdFormProps.PWD_CONTAIN_UPPER" :options="yesNoOptions" />
</s-form-item>
</el-col>
<el-col :span="6">
<s-form-item label="是否包含特殊字符" prop="PWD_CONTAIN_CHARACTER">
<s-radio-group v-model="pwdFormProps.PWD_CONTAIN_CHARACTER" :options="yesNoOptions" />
</s-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</el-button>
<el-button style="margin-left: 10px" @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { SysConfig, sysConfigApi } from "@/api";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import { SysConfigTypeEnum, SysDictEnum } from "@/enums";
const dictStore = useDictStore(); //字典仓库
// 字典类型选项
const yesNoOptions = dictStore.getDictList(SysDictEnum.YES_NO);
const submitLoading = ref(false); //提交按钮loading
const pwdFormRef = ref<FormInstance>();
/** 登录策略参数 */
const pwdFormProps = reactive<SysConfig.PwdPolicyConfig>({
PWD_DEFAULT_PASSWORD: "",
PWD_REMIND: yesNoOptions[0].value,
PWD_REMIND_DAY: 30,
PWD_UPDATE_DEFAULT: yesNoOptions[0].value,
PWD_MIN_LENGTH: 6,
PWD_CONTAIN_NUM: yesNoOptions[0].value,
PWD_CONTAIN_LOWER: yesNoOptions[0].value,
PWD_CONTAIN_UPPER: yesNoOptions[0].value,
PWD_CONTAIN_CHARACTER: yesNoOptions[0].value
});
//props定义
const props = defineProps({
pwdPolicy: {
type: Array as PropType<SysConfig.ConfigInfo[]>,
required: true,
default: () => []
}
});
//监听pwdPolicy变化
watch(
() => props.pwdPolicy,
(newVal: SysConfig.ConfigInfo[]) => {
//重新赋值
newVal.forEach((item: SysConfig.ConfigInfo) => {
let prop = pwdFormProps[item.configKey as keyof SysConfig.PwdPolicyConfig];
//如果是number类型就转为number
if (typeof prop === "number") {
(pwdFormProps[item.configKey as keyof SysConfig.PwdPolicyConfig] as number) = Number(item.configValue);
} else {
(pwdFormProps[item.configKey as keyof SysConfig.PwdPolicyConfig] as string) = item.configValue;
}
});
}
);
// 表单验证规则
const rules = reactive({
PWD_DEFAULT_PASSWORD: [{ required: true, message: "请输入系统默认密码", trigger: "blur" }],
PWD_REMIND: [{ required: true, message: "请选择密码定期提醒更新", trigger: "blur" }],
PWD_REMIND_DAY: [{ required: true, message: "请输入密码定期提醒更新时间", trigger: "blur" }],
PWD_UPDATE_DEFAULT: [{ required: true, message: "请选择修改初始密码提醒", trigger: "blur" }],
PWD_MIN_LENGTH: [{ required: true, message: "请输入密码最小长度", trigger: "blur" }],
PWD_CONTAIN_NUM: [{ required: true, message: "请选择是否含数字", trigger: "blur" }],
PWD_CONTAIN_LOWER: [{ required: true, message: "请选择是否包含小写字母", trigger: "blur" }],
PWD_CONTAIN_UPPER: [{ required: true, message: "请选择是否包含大写字母", trigger: "blur" }],
PWD_CONTAIN_CHARACTER: [{ required: true, message: "请选择是否包含特殊字符", trigger: "blur" }]
});
/** 提交表单 */
function onSubmit() {
pwdFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
submitLoading.value = true;
//组装参数
const param: SysConfig.ConfigInfo[] = Object.entries(pwdFormProps).map(item => {
return {
id: 0,
category: SysConfigTypeEnum.LOGIN_POLICY,
configKey: item[0],
configValue: typeof item[1] === "object" ? JSON.stringify(item[1]) : String(item[1])
};
});
//提交数据
sysConfigApi.configEditForm(param).finally(() => {
submitLoading.value = false;
});
});
}
/** 重置表单 */
function resetForm() {
pwdFormRef.value?.resetFields();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,307 @@
<!--
* @Description: 系统配置
* @Author: huguodong
* @Date: 2024-01-22 16:44:07
!-->
<template>
<div>
<el-form ref="sysConfigFormRef" :rules="rules" :model="sysBaseProps" label-width="auto" label-suffix=" :" label-position="top">
<el-row :gutter="16">
<el-col :span="8">
<s-form-item label="系统LOGO" prop="SYS_LOGO">
<UploadImg v-model:image-url="sysBaseProps.SYS_LOGO" width="135px" height="135px" :file-size="3" :auto-upload="false">
<template #empty>
<el-icon><Avatar /></el-icon>
<span>请上传LOGO</span>
</template>
</UploadImg>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="系统ico文件" prop="SYS_ICO">
<UploadImg
v-model:image-url="sysBaseProps.SYS_ICO!"
width="135px"
height="135px"
:file-size="1"
:file-type="icoType"
:auto-upload="false"
>
<template #empty>
<el-icon><Picture /></el-icon>
<span>请上传.ICO文件</span>
</template>
</UploadImg>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<s-form-item label="系统名称" prop="SYS_NAME">
<s-input v-model="sysBaseProps.SYS_NAME"></s-input>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="系统版本" prop="SYS_VERSION">
<s-input v-model="sysBaseProps.SYS_VERSION"></s-input>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="默认快捷方式" prop="SYS_DEFAULT_WORKBENCH_DATA">
<MenuSelector
v-model:menu-value="sysBaseProps.SYS_DEFAULT_WORKBENCH_DATA!.shortcut"
multiple
:check-strictly="false"
:menu-tree-api="menuApi.menuTreeSelector"
/>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<s-form-item label="多租户配置" prop="SYS_TENANT_OPTIONS">
<s-radio-group v-model="sysBaseProps.SYS_TENANT_OPTIONS" :options="tenantOptions" button />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="版权信息" prop="SYS_COPYRIGHT">
<s-input v-model="sysBaseProps.SYS_COPYRIGHT"></s-input>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="版权链接URL" prop="SYS_COPYRIGHT_URL">
<s-input v-model="sysBaseProps.SYS_COPYRIGHT_URL"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<s-form-item label="底部超链接" prop="SYS_FOOTER_LINKS">
<el-tag
v-for="tag in sysBaseProps.SYS_FOOTER_LINKS"
:key="tag.name"
class="mx-1"
closable
:disable-transitions="false"
@close="handleClose(tag.name)"
@click="linkDetail(tag)"
>
{{ tag.name }}
</el-tag>
<el-button class="ml-1" size="small" @click="showLinkModel()">添加</el-button>
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="网站开启访问(仅业务)" prop="SYS_WEB_STATUS">
<s-radio-group v-model="sysBaseProps.SYS_WEB_STATUS" :options="statusOptions" button />
</s-form-item>
</el-col>
<el-col :span="8">
<s-form-item label="网站关闭提示" prop="SYS_WEB_CLOSE_PROMPT">
<s-input v-model="sysBaseProps.SYS_WEB_CLOSE_PROMPT" :rows="3" type="textarea"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item>
<el-button type="primary" :loading="submitLoading" @click="onSubmit()">保存</el-button>
<el-button style="margin-left: 10px" @click="resetForm">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 超链接弹窗 -->
<el-dialog v-model="dialogVisible" title="超链接信息" width="30%" show-close destroy-on-close>
<el-form ref="linkFormRef" :model="linkForm" :rules="linkRules" label-width="auto" label-suffix=" :" label-position="top">
<s-form-item label="超链接名称" prop="name">
<s-input v-model="linkForm.name"></s-input>
</s-form-item>
<s-form-item label="超链接地址" prop="url">
<s-input v-model="linkForm.url"></s-input>
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="linkForm.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveLink">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { SysConfig, menuApi, sysConfigApi } from "@/api";
import { required } from "@/utils/formRules";
import { useDictStore } from "@/stores/modules";
import { SysDictEnum, SysConfigTypeEnum, SysBaseEnum, TenantEnum } from "@/enums";
import { FormInstance } from "element-plus";
const dictStore = useDictStore(); //字典仓库
// 字典类型选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
//多租户选项
const tenantOptions = dictStore.getDictList(SysDictEnum.TENANT_OPTIONS);
const icoType: File.ImageMimeType[] = ["image/x-icon"]; //ico文件类型
const submitLoading = ref(false); //提交按钮loading
const sysConfigFormRef = ref<FormInstance>(); //表单实例
/** 系统配置 */
const sysBaseProps = reactive<SysConfig.SysBaseConfig>({
SYS_NAME: "",
SYS_LOGO: "",
SYS_ICO: "",
SYS_VERSION: "",
SYS_COPYRIGHT: "",
SYS_COPYRIGHT_URL: "",
SYS_DEFAULT_WORKBENCH_DATA: { shortcut: [] },
SYS_WEB_STATUS: statusOptions[0].value,
SYS_WEB_CLOSE_PROMPT: "",
SYS_TENANT_OPTIONS: TenantEnum.CLOSE,
SYS_FOOTER_LINKS: []
});
//定义props
const props = defineProps({
sysConfigs: {
type: Array as PropType<SysConfig.ConfigInfo[]>,
required: true,
default: () => []
}
});
//监听sysConfigs变化
watch(
() => props.sysConfigs,
(newVal: SysConfig.ConfigInfo[]) => {
//重新赋值
newVal.forEach((item: SysConfig.ConfigInfo) => {
//如果是对象类型的属性就转成对象
if (item.configKey == SysBaseEnum.SYS_DEFAULT_WORKBENCH_DATA) {
const workBenchData: SysConfig.WorkBenchData = JSON.parse(item.configValue);
sysBaseProps.SYS_DEFAULT_WORKBENCH_DATA = workBenchData;
} else if (item.configKey == SysBaseEnum.SYS_FOOTER_LINKS) {
const footerLinks: SysConfig.FooterLinkProps[] = JSON.parse(item.configValue);
sysBaseProps.SYS_FOOTER_LINKS = footerLinks;
} else {
// 其他属性直接赋值
(sysBaseProps[item.configKey as keyof SysConfig.SysBaseConfig] as string) = item.configValue;
}
});
}
);
// 表单验证规则
const rules = reactive({
SYS_NAME: [required("请输入系统名称")],
SYS_LOGO: [required("请上传系统LOGO")],
SYS_ICO: [required("请上传系统ICO")],
SYS_VERSION: [required("请输入系统版本")],
SYS_COPYRIGHT: [required("请输入系统版权")],
SYS_COPYRIGHT_URL: [required("请输入系统版权链接")],
SYS_DEFAULT_WORKBENCH_DATA: [required("请输入系统默认工作台数据")],
SYS_WEB_STATUS: [required("请选择网站开启访问状态")],
SYS_WEB_CLOSE_PROMPT: [required("请输入网站关闭提示")]
});
const dialogVisible = ref(false); // 是否显示弹窗
// 超链接临时对象
const linkTmp = ref<SysConfig.FooterLinkProps>({
name: "",
url: "",
sortCode: 99
});
// 是否是添加超链接
const linkFormRef = ref<FormInstance>(); // 超链接表单实例
// 超链接表单参数
const linkForm = reactive<SysConfig.FooterLinkProps>({
name: "",
url: "",
sortCode: 99
});
// 表单验证规则
const linkRules = reactive({
name: [required("请输入超链接名称")],
url: [required("请输入超链接地址")],
sortCode: [required("请输入排序")]
});
/** 删除超链接 */
function handleClose(tag: string) {
sysBaseProps.SYS_FOOTER_LINKS = sysBaseProps.SYS_FOOTER_LINKS.filter(item => item.name != tag);
}
/** 打开超链接详情 */
function linkDetail(value: SysConfig.FooterLinkProps) {
linkTmp.value = value;
linkForm.name = value.name;
linkForm.url = value.url;
linkForm.sortCode = value.sortCode;
dialogVisible.value = true;
}
/** 显示超链接表单 */
function showLinkModel() {
dialogVisible.value = true;
}
/** 保存超链接 */
function saveLink() {
linkFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//判断是编辑还是添加
if (linkTmp.value.name) {
//删除旧的值,添加新的值
sysBaseProps.SYS_FOOTER_LINKS = sysBaseProps.SYS_FOOTER_LINKS.filter(item => item.name != linkTmp.value.name);
}
sysBaseProps.SYS_FOOTER_LINKS.push({ ...linkForm });
//根据sortCode排序,sortCode越小越靠前
sysBaseProps.SYS_FOOTER_LINKS.sort((a, b) => a.sortCode - b.sortCode);
//关闭弹窗,恢复默认值
dialogVisible.value = false;
linkTmp.value.name = "";
linkTmp.value.url = "";
linkTmp.value.sortCode = 99;
linkFormRef.value?.resetFields();
});
}
/** 提交表单 */
function onSubmit() {
sysConfigFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
submitLoading.value = true;
//组装参数
const param: SysConfig.ConfigInfo[] = Object.entries(sysBaseProps).map(item => {
return {
id: 0,
category: SysConfigTypeEnum.SYS_BASE,
configKey: item[0],
//configValue 如果是对象就转成json字符串
configValue: typeof item[1] === "object" ? JSON.stringify(item[1]) : String(item[1])
};
});
//提交数据
sysConfigApi.configEditForm(param).finally(() => {
submitLoading.value = false;
});
});
}
/** 重置表单 */
function resetForm() {
sysConfigFormRef.value?.resetFields();
linkFormRef.value?.resetFields();
}
</script>
<style lang="scss" scoped></style>
+50
View File
@@ -0,0 +1,50 @@
<!--
* @Description:
* @Author: huguodong
* @Date: 2023-12-22 16:59:28
!-->
<template>
<div class="card">
<el-tabs v-model="activeName" class="-mt-4">
<el-tab-pane label="系统配置" name="sysConfig"><SysBaseConfig :sys-configs="sysBase" /></el-tab-pane>
<el-tab-pane label="安全配置" name="safety"><SafetyConfig :sys-configs="safetyConfig" /></el-tab-pane>
<el-tab-pane label="MQTT配置" name="mqtt"><MqttConfig :sys-configs="mqttConfig" /></el-tab-pane>
<el-tab-pane label="文件配置" name="file">开发中......</el-tab-pane>
<el-tab-pane label="其他配置" name="other"><OtherConfig v-if="activeName === 'other'" /></el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import SysBaseConfig from "./components/sysBaseConfig.vue";
import SafetyConfig from "./components/safetyConfig/index.vue";
import OtherConfig from "./components/otherConfig/index.vue";
import MqttConfig from "./components/mqttConfig/index.vue";
import { SysConfig, sysConfigApi } from "@/api";
import { SysConfigTypeEnum } from "@/enums";
const activeName = ref("sysConfig");
const sysBase = ref<SysConfig.ConfigInfo[]>([]);
const safetyConfig = ref<SysConfig.ConfigInfo[]>([]);
const mqttConfig = ref<SysConfig.ConfigInfo[]>([]);
onMounted(async () => {
// 获取系统配置
await sysConfigApi.list().then(res => {
if (res.data) {
//sysBase赋值
sysBase.value = res.data.filter(item => item.category == SysConfigTypeEnum.SYS_BASE);
//safetyBase赋值
safetyConfig.value = res.data.filter(item => item.category == SysConfigTypeEnum.LOGIN_POLICY || item.category == SysConfigTypeEnum.PWD_POLICY);
//mqttConfig赋值
mqttConfig.value = res.data.filter(item => item.category == SysConfigTypeEnum.MQTT_POLICY);
}
});
});
</script>
<style lang="scss" scoped>
:deep(.el-tabs__item) {
font-size: large;
}
</style>
+117
View File
@@ -0,0 +1,117 @@
<!--
* @Description: 字典表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:05
!-->
<template>
<form-container v-model="visible" :title="`${dictProps.opt}字典`" form-size="600px">
<el-form
ref="dictFormRef"
:rules="rules"
:disabled="dictProps.disabled"
:model="dictProps.record"
:hide-required-asterisk="dictProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="字典名称" prop="dictLabel">
<s-input v-model="dictProps.record.dictLabel"></s-input>
</s-form-item>
<s-form-item label="字典值" prop="dictValue">
<s-input v-model="dictProps.record.dictValue"></s-input>
</s-form-item>
<s-form-item label="字典状态" prop="status">
<s-radio-group v-model="dictProps.record.status" :options="statusOptions" />
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="dictProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!dictProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { SysDict, sysDictApi } from "@/api";
import { required } from "@/utils/formRules";
import { useDictStore } from "@/stores/modules";
import { FormOptEnum, CommonStatusEnum, SysDictEnum, DictCategoryEnum } from "@/enums";
import { FormInstance } from "element-plus";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 字典类型选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数接口
interface DictProps extends FormProps.Base<SysDict.DictInfo> {
category: DictCategoryEnum; //字典分类
parentId?: number | string; //父Id
}
// 表单参数
const dictProps = reactive<DictProps>({
opt: FormOptEnum.ADD,
record: {},
disabled: false,
category: DictCategoryEnum.FRM
});
// 表单验证规则
const rules = reactive({
dictLabel: [required("请输入字典名称")],
dictValue: [required("请输入字典值")],
status: [required("请选择状态")],
sortCode: [required("请输入排序")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: DictProps) {
Object.assign(dictProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
dictProps.record.status = CommonStatusEnum.ENABLE;
dictProps.record.sortCode = 99;
dictProps.record.category = props.category;
dictProps.record.parentId = props.parentId;
}
visible.value = true; //显示表单
}
// 提交数据(新增/编辑)
const dictFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
dictFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await sysDictApi
.submitForm(dictProps.record, dictProps.record.id != undefined)
.then(() => {
dictProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+216
View File
@@ -0,0 +1,216 @@
<!--
* @Description: 系统字典
* @Author: huguodong
* @Date: 2023-12-15 15:45:10
!-->
<template>
<div class="main-box gap-10px">
<div class="w-7/12">
<ProTable
ref="proTable"
:columns="columns"
:tool-button="false"
:request-api="getDictPage"
:init-param="initParam"
@current-change="handleCurrentChange"
>
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<el-space>
<el-radio-group v-model="initParam.category" class="mb-15px">
<el-radio-button v-for="(item, index) in dctTypeOptions" :key="index" :label="item.value">{{ item.label }}</el-radio-button>
</el-radio-group>
<el-input v-model="initParam.searchKey" placeholder="请输入字典名称" class="mb-15px">
<template #append>
<el-button :icon="Search" class="el-input-button" @click="RefreshTable" />
</template>
</el-input>
<s-button :suffix="title" @click="onOpen(FormOptEnum.ADD, initParam.category as DictCategoryEnum)" />
<s-button
type="danger"
plain
:suffix="title"
:disabled="!scope.isSelected"
:opt="FormOptEnum.DELETE"
@click="onDelete(scope.selectedListIds, '删除所选字典')"
/>
</el-space>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button
link
:opt="FormOptEnum.EDIT"
@click="onOpen(FormOptEnum.EDIT, initParam.category as DictCategoryEnum, scope.row.parentId, scope.row)"
/>
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.dictLabel}】字典`)" />
</template>
</ProTable>
</div>
<ProTable
ref="proTableChild"
:columns="columns"
:tool-button="false"
:request-api="sysDictApi.page"
:request-auto="false"
:init-param="childInitParam"
>
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<el-space>
<el-input v-model="childInitParam.searchKey" placeholder="请输入字典名称" class="mb-15px">
<template #append>
<el-button :icon="Search" class="el-input-button" />
</template>
</el-input>
<s-button :suffix="title" @click="onOpen(FormOptEnum.ADD, childInitParam.category as DictCategoryEnum, childInitParam.parentId)" />
<s-button
type="danger"
:suffix="title"
plain
:disabled="!scope.isSelected"
:opt="FormOptEnum.DELETE"
@click="onDelete(scope.selectedListIds, '删除所选字典')"
/>
</el-space>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button
link
:opt="FormOptEnum.EDIT"
@click="onOpen(FormOptEnum.EDIT, childInitParam.category as DictCategoryEnum, scope.row.parentId, scope.row)"
/>
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.dictLabel}字典`)" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</template>
<script setup lang="ts" name="sysDict">
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { Search } from "@element-plus/icons-vue";
import { SysDict, sysDictApi } from "@/api";
import { useHandleData } from "@/hooks/useHandleData";
import { SysDictEnum, FormOptEnum, CommonStatusEnum, DictCategoryEnum } from "@/enums";
import { useDictStore } from "@/stores/modules";
import Form from "./components/form.vue";
import { ElMessage } from "element-plus";
const title = "字典"; //功能名称
const dictStore = useDictStore(); //字典仓库
// 左侧表格初始化条件
interface InitParam {
category?: string; //字典分类
searchKey?: string; //关键字
}
//右侧表格列初始化条件
interface ChildInitParam extends InitParam {
parentId: number | string; //父Id
}
const defaultParentId: string | number = -1; //默认父Id
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ category: DictCategoryEnum.FRM }); //主表格初始化参数
const childInitParam = reactive<ChildInitParam>({ parentId: defaultParentId }); //子表格初始化参数
// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
// 默认不做操作就直接在 ProTable 组件上绑定 :request="getUserList"
const getDictPage = (params: any) => {
childInitParam.parentId = defaultParentId;
return sysDictApi.page(params);
};
// 字典类型选项
const dctTypeOptions = dictStore.getDictList(SysDictEnum.DICT_CATEGORY);
// 获取 ProTable 元素,调用其获取刷新数据方法(还能获取到当前查询参数,方便导出携带参数)
const proTable = ref<ProTableInstance>();
const proTableChild = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysDict.DictInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "dictLabel", label: "字典名称" },
{ prop: "dictValue", label: "字典值" },
{ prop: "status", label: "状态", width: 100 },
{ prop: "sortCode", label: "排序", width: 80 },
{ prop: "operation", label: "操作", fixed: "right", width: 150 }
];
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, category: DictCategoryEnum, parentId: number | string = 0, record: {} | SysDict.DictInfo = {}) {
//如果parentId是-1代表没有选择任何字典,此时不打开表单
if (parentId === defaultParentId) {
ElMessage("请先选择字典");
} else {
formRef.value?.onOpen({
opt: opt,
record: record,
category: category,
parentId: parentId,
successful: () => proTableChild.value?.refresh() //刷新子表格
});
}
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(sysDictApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新主表格
proTableChild.value?.refresh(); //刷新子表格
}
/**
* 表格当前行变化
* @param val 选中的值
*/
function handleCurrentChange(val: SysDict.DictInfo | undefined) {
// 如果val存在,则将parentId和category赋值给childInitParam
if (val) {
childInitParam.parentId = val.id;
childInitParam.category = val.category;
}
}
</script>
<style lang="scss" scoped>
// 表格按钮样式
.el-space__item > .el-button {
margin-top: -13px;
}
</style>
@@ -0,0 +1,105 @@
<!--
* @Description: 复制
* @Author: huguodong
* @Date: 2023-12-15 15:45:25
!-->
<template>
<form-container v-model="visible" title="复制组织" form-size="600px">
<el-form ref="orgFormRef" :rules="rules" :model="copyProps" label-width="auto">
<s-form-item label="目标组织" prop="targetId">
<org-selector v-model:org-value="copyProps.targetId" />
</s-form-item>
<s-form-item label="包含子集" prop="containsChild" :tooltip="{ content: '将组织下的子机构也复制一份到目标组织' }">
<s-radio-group v-model="copyProps.containsChild" :options="yesOptions" button />
</s-form-item>
<s-form-item label="包含职位" prop="containsPosition" :tooltip="{ content: '将组织下的子机构也复制一份到目标组织' }">
<s-radio-group v-model="copyProps.containsPosition" :options="yesOptions" button />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { sysOrgApi } from "@/api";
import { SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 是否选项
const yesOptions = dictStore.getDictList(SysDictEnum.YES_NO);
// 表单参数
interface copyProps {
/** 目标组织id */
targetId: string | number;
/** 选择的组织id */
ids: string[] | number[];
/** 是否包含子集 */
containsChild: boolean;
/** 是否包含职位 */
containsPosition: boolean;
/** 成功后的回调 */
successful?: () => void;
}
// 表单参数
const copyProps = reactive<copyProps>({
targetId: 0,
ids: [],
containsChild: true,
containsPosition: true
});
// 表单验证规则
const rules = reactive({
targetId: [required("请选择目标组织")],
containsChild: [required("请选择是否包含子集")],
containsPosition: [required("请选择是否包含职位")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(ids: string[] | number[], successful: () => void) {
visible.value = true; //显示表单
copyProps.ids = ids;
copyProps.successful = successful;
}
// 提交数据(新增/编辑)
const orgFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
orgFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await sysOrgApi
.copy(copyProps)
.then(() => {
copyProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,161 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:28
!-->
<template>
<div>
<form-container v-model="visible" :title="`${orgProps.opt}组织`" form-size="600px">
<el-form
ref="orgFormRef"
:rules="rules"
:disabled="orgProps.disabled"
:model="orgProps.record"
:hide-required-asterisk="orgProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="上级组织" prop="parentId">
<org-selector v-model:org-value="orgProps.record.parentId!" />
</s-form-item>
<s-form-item label="组织名称" prop="name">
<s-input v-model="orgProps.record.name"></s-input>
</s-form-item>
<s-form-item label="组织编码" prop="code">
<s-input v-model="orgProps.record.code" placeholder="请填写组织编码,不填则为随机值" clearable></s-input>
</s-form-item>
<s-form-item label="组织分类" prop="category">
<s-radio-group v-model="orgProps.record.category" :options="orgCategoryOptions" button />
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="orgProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="指定主管" prop="directorId">
<el-button link type="primary" @click="showSelector">选择</el-button>
<el-tag v-if="orgProps.record.directorId" class="ml-3px" type="warning" closable @close="removeDirector">{{
orgProps.record.directorInfo?.name
}}</el-tag>
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="orgProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!orgProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
<user-selector
ref="userSelectorRef"
:org-tree-api="sysOrgApi.tree"
:position-tree-api="sysPositionApi.tree"
:role-tree-api="sysRoleApi.tree"
:user-selector-api="sysUserApi.selector"
@successful="handleChooseUser"
/>
</div>
</template>
<script setup lang="ts">
import { SysOrg, SysUser, sysOrgApi, sysPositionApi, sysRoleApi, sysUserApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 组织类型选项
const orgCategoryOptions = dictStore.getDictList(SysDictEnum.ORG_CATEGORY);
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const orgProps = reactive<FormProps.Base<SysOrg.SysOrgInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
name: [required("请输入组织名称")],
parentId: [required("请选择上级组织")],
category: [required("请选择组织类型")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysOrg.SysOrgInfo>) {
Object.assign(orgProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
orgProps.record.sortCode = 99;
orgProps.record.category = orgCategoryOptions[0].value;
orgProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
sysOrgApi.detail({ id: props.record.id }).then(res => {
orgProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const orgFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
orgFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await sysOrgApi
.submitForm(orgProps.record, orgProps.record.id != undefined)
.then(() => {
orgProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
const userSelectorRef = ref<UserSelectorInstance>(); //用户选择器引用
/** 显示用户选择器 */
function showSelector() {
//将orgProps.record.directorInfo转为 SysUser.SysUserInfo[]类型
const directorInfo = orgProps.record.directorInfo ? [orgProps.record.directorInfo] : [];
userSelectorRef.value?.showSelector(directorInfo);
}
/** 选择用户 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
// 选择用户后,将用户id赋值给orgProps.record.directorId
if (data.length > 0) {
orgProps.record.directorId = data[0].id;
orgProps.record.directorInfo = data[0];
}
}
/** 移除主管 */
function removeDirector() {
orgProps.record.directorId = null;
orgProps.record.directorInfo = null;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+152
View File
@@ -0,0 +1,152 @@
<!--
* @Description: 组织管理
* @Author: huguodong
* @Date: 2023-12-15 15:45:32
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="组织列表"
:request-api="sysOrgApi.tree"
:default-value="initParam.parentId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" title="组织列表" :columns="columns" :request-api="sysOrgApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="组织" @click="onOpen(FormOptEnum.ADD)" />
<s-button :disabled="!scope.isSelected" :icon="CopyDocument" @click="onOpenCopy(scope.selectedListIds)">复制组织</s-button>
<s-button
type="danger"
plain
suffix="组织"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选组织')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.name}】组织`)" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 复制表单 -->
<Copy ref="copyRef" />
</div>
</div>
</template>
<script setup lang="ts" name="sysOrg">
import { sysOrgApi, SysOrg } from "@/api";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { CopyDocument } from "@element-plus/icons-vue";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form.vue";
import Copy from "./components/copy.vue";
const dictStore = useDictStore(); //字典仓库
// 组织类型选项
const orgCategoryOptions = dictStore.getDictList(SysDictEnum.ORG_CATEGORY);
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
interface InitParam {
parentId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ parentId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysOrg.SysOrgInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "name", label: "组织名称", search: { el: "input" } },
{ prop: "names", label: "组织全称" },
{
prop: "category",
label: "分类",
enum: orgCategoryOptions,
search: { el: "select" }
},
{ prop: "code", label: "编码", search: { el: "input" } },
{ prop: "status", label: "状态", enum: statusOptions, search: { el: "select" } },
{ prop: "sortCode", label: "排序" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 150, fixed: "right" }
];
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.parentId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.parentId = 0;
}
}
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysOrg.SysOrgInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
// 表单引用
const copyRef = ref<InstanceType<typeof Copy> | null>(null);
/**
* 打开复制表单
* @param opt 操作类型
* @param record 记录
*/
function onOpenCopy(ids: string[] | number[]) {
copyRef.value?.onOpen(ids, RefreshTable);
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(sysOrgApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,125 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:37
!-->
<template>
<div>
<form-container v-model="visible" :title="`${positionProps.opt}职位`" form-size="600px">
<el-form
ref="positionFormRef"
:rules="rules"
:disabled="positionProps.disabled"
:model="positionProps.record"
:hide-required-asterisk="positionProps.disabled"
label-width="auto"
label-suffix=" :"
>
<s-form-item label="所属组织" prop="orgId">
<org-selector v-model:org-value="positionProps.record.orgId!" :show-all="false" />
</s-form-item>
<s-form-item label="职位名称" prop="name">
<s-input v-model="positionProps.record.name"></s-input>
</s-form-item>
<s-form-item label="职位编码" prop="code">
<s-input v-model="positionProps.record.code" placeholder="请填写职位编码,不填则为随机值"></s-input>
</s-form-item>
<s-form-item label="职位分类" prop="category">
<s-radio-group v-model="positionProps.record.category" :options="posCategoryOptions" button />
</s-form-item>
<s-form-item label="状态" prop="status">
<s-radio-group v-model="positionProps.record.status" :options="statusOptions" button />
</s-form-item>
<s-form-item label="排序" prop="sortCode">
<el-slider v-model="positionProps.record.sortCode" show-input :min="1" />
</s-form-item>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!positionProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>
<script setup lang="ts">
import { SysPosition, sysPositionApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
const visible = ref(false); //是否显示表单
const dictStore = useDictStore(); //字典仓库
// 职位类型选项
const posCategoryOptions = dictStore.getDictList(SysDictEnum.POSITION_CATEGORY);
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const positionProps = reactive<FormProps.Base<SysPosition.SysPositionInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
parentId: [required("请选择所属组织")],
name: [required("请输入职位名称")],
orgId: [required("请选择所属职位")],
category: [required("请选择职位类型")],
status: [required("请选择状态")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysPosition.SysPositionInfo>) {
Object.assign(positionProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
positionProps.record.sortCode = 99;
positionProps.record.category = posCategoryOptions[0].value;
positionProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
sysPositionApi.detail({ id: props.record.id }).then(res => {
positionProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const positionFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
positionFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await sysPositionApi
.submitForm(positionProps.record, positionProps.record.id != undefined)
.then(() => {
positionProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,134 @@
<!--
* @Description: 职位管理
* @Author: huguodong
* @Date: 2023-12-15 15:45:43
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="组织列表"
:request-api="sysOrgApi.tree"
:default-value="initParam.orgId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" title="职位列表" :columns="columns" :request-api="sysPositionApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="职位" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="职位"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选职位')"
/>
</template>
<!-- 状态 -->
<template #status="scope">
<el-tag v-if="scope.row.status === CommonStatusEnum.ENABLE" type="success">{{
dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status)
}}</el-tag>
<el-tag v-else type="danger">{{ dictStore.dictTranslation(SysDictEnum.COMMON_STATUS, scope.row.status) }}</el-tag>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.name}】职位`)" />
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
</div>
</div>
</template>
<script setup lang="ts" name="sysPosition">
import { sysOrgApi, sysPositionApi, SysPosition } from "@/api";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form.vue";
const dictStore = useDictStore(); //字典仓库
// 职位类型选项
const posCategoryOptions = dictStore.getDictList(SysDictEnum.POSITION_CATEGORY);
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
interface InitParam {
orgId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ orgId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysPosition.SysPositionInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "name", label: "职位名称", search: { el: "input" } },
{
prop: "category",
label: "职位分类",
enum: posCategoryOptions,
search: { el: "select" }
},
{ prop: "code", label: "职位编码", search: { el: "input" } },
{ prop: "status", label: "状态", enum: statusOptions, search: { el: "select" } },
{ prop: "sortCode", label: "排序" },
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 150, fixed: "right" }
];
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.orgId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.orgId = 0;
}
}
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysPosition.SysPositionInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(sysPositionApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,167 @@
<!--
* @Description: 基本信息
* @Author: huguodong
* @Date: 2023-12-15 15:45:50
!-->
<template>
<div>
<el-tab-pane label="基础信息" name="basic">
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="账号" prop="account">
<s-input v-model="userInfo.account"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="姓名" prop="name">
<s-input v-model="userInfo.name"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="性别" prop="gender">
<s-radio-group v-model="userInfo.gender" :options="genderOptions" />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="昵称" prop="nickname">
<s-input v-model="userInfo.nickname"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="手机号" prop="phone">
<s-input v-model="userInfo.phone"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="邮箱" prop="email">
<s-input v-model="userInfo.email"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="出生日期" prop="birthday">
<date-picker v-model="userInfo.birthday"></date-picker>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="入职日期" prop="entryDate">
<date-picker v-model="userInfo.entryDate"></date-picker>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="员工编号" prop="empNo">
<s-input v-model="userInfo.empNo"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="职级" prop="positionLevel">
<s-input v-model="userInfo.positionLevel" clearable></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="部门和职位" prop="positionId">
<position-selector v-model="userInfo.orgAndPosIdList" @change="handleChange" />
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="主管" prop="directorId">
<el-button link type="primary" @click="showSelector">选择</el-button>
<el-tag v-if="userInfo.directorId" class="ml-3px" type="warning" closable @close="removeDirector">{{
userInfo.directorInfo?.name
}}</el-tag>
</s-form-item>
</el-col>
</el-row>
</el-tab-pane>
<user-selector
ref="userSelectorRef"
:org-tree-api="sysOrgApi.tree"
:position-tree-api="sysPositionApi.tree"
:role-tree-api="sysRoleApi.tree"
:user-selector-api="sysUserApi.selector"
@successful="handleChooseUser"
/>
</div>
</template>
<script setup lang="ts">
import { SysUser, sysOrgApi, sysPositionApi, sysRoleApi, sysUserApi } from "@/api";
import { useDictStore } from "@/stores/modules";
import { SysDictEnum } from "@/enums";
import { UserSelectorInstance } from "@/components/Selectors/UserSelector/interface";
const dictStore = useDictStore(); //字典仓库
// props
interface FormProps {
modelValue: Partial<SysUser.SysUserInfo>;
}
const emit = defineEmits(["update:modelValue"]); //定义emit
const props = defineProps<FormProps>(); //定义props
// 用户信息
const userInfo = computed({
get: () => props.modelValue,
set: val => emit("update:modelValue", val)
});
const genderOptions = dictStore.getDictList(SysDictEnum.GENDER); //性别选项
onMounted(() => {
// 初始化
userInfo.value.gender = userInfo.value.gender ? userInfo.value.gender : genderOptions[0].value;
});
/** 用于处理职位选择器改变值的情况 */
function handleChange(value: any) {
if (!value) {
return;
}
// 获取改变后的值中最后一位的值,即positionId
let positionId = value[value.length - 1];
// 获取改变后的值中最后两位的值,即orgId
let orgId = value[value.length - 2];
// 将orgId赋值给userInfo的value的orgId
userInfo.value.orgId = orgId;
// 将positionId赋值给userInfo的value的positionId
userInfo.value.positionId = positionId;
}
const userSelectorRef = ref<UserSelectorInstance>(); //用户选择器ref
/** 显示用户选择器 */
function showSelector() {
//将userInfo.value.directorInfo转为 SysUser.SysUserInfo[]类型
const directorInfo = userInfo.value.directorInfo ? [userInfo.value.directorInfo] : [];
userSelectorRef.value?.showSelector(directorInfo);
}
/** 选择用户 */
function handleChooseUser(data: SysUser.SysUserInfo[]) {
// 选择用户后,将用户id赋值给userInfo.value.directorId
if (data.length > 0) {
userInfo.value.directorId = data[0].id;
userInfo.value.directorInfo = data[0];
}
}
/** 移除主管 */
function removeDirector() {
userInfo.value.directorId = null;
userInfo.value.directorInfo = null;
}
</script>
<style lang="scss" scoped>
:deep(.el-input__wrapper) {
width: 100% !important;
}
:deep(.el-date-editor.el-input) {
width: 92% !important;
}
</style>
@@ -0,0 +1,145 @@
<!--
* @Description: 更多信息
* @Author: huguodong
* @Date: 2023-12-15 15:45:55
!-->
<template>
<el-tab-pane label="更多信息" name="more">
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="民族" prop="nation">
<s-select v-model="userInfo.nation" :options="nationOptions"></s-select>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="籍贯" prop="nativePlace">
<s-input v-model="userInfo.nativePlace"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="家庭住址" prop="homeAddress">
<s-input v-model="userInfo.homeAddress" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="通信地址" prop="mailingAddress">
<s-input v-model="userInfo.mailingAddress" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="证件类型" prop="nation">
<s-select v-model="userInfo.idCardType" :options="cardTypeOptions"></s-select>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="证件号码" prop="idCardNumber">
<s-input v-model="userInfo.idCardNumber"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="文化程度" prop="cultureLevel">
<s-select v-model="userInfo.cultureLevel" :options="cultureLevelOptions"></s-select>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="政治面貌" prop="politicalOutlook">
<s-input v-model="userInfo.politicalOutlook"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="毕业学校" prop="college">
<s-input v-model="userInfo.college"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="学历" prop="education">
<s-input v-model="userInfo.education"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="学制" prop="eduLength">
<s-input v-model="userInfo.eduLength"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="学位" prop="degree">
<s-input v-model="userInfo.degree"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="家庭电话" prop="homeTel">
<s-input v-model="userInfo.homeTel"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="办公电话" prop="officeTel">
<s-input v-model="userInfo.officeTel"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="紧急联系人" prop="emergencyContact">
<s-input v-model="userInfo.emergencyContact"></s-input>
</s-form-item>
</el-col>
<el-col :span="12">
<s-form-item label="紧急联系电话" prop="emergencyPhone">
<s-input v-model="userInfo.emergencyPhone"></s-input>
</s-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<s-form-item label="紧急联系人地址" prop="emergencyAddress">
<s-input v-model="userInfo.emergencyAddress"></s-input>
</s-form-item>
</el-col>
</el-row>
</el-tab-pane>
</template>
<script setup lang="ts">
import { SysUser } from "@/api";
import { useDictStore } from "@/stores/modules";
import { SysDictEnum } from "@/enums";
const dictStore = useDictStore(); //字典仓库
// props
interface FormProps {
modelValue: Partial<SysUser.SysUserInfo>;
}
const emit = defineEmits(["update:modelValue"]); //定义emit
const props = defineProps<FormProps>(); //定义props
// 用户信息
const userInfo = computed({
get: () => props.modelValue,
set: val => emit("update:modelValue", val)
});
// 通用状态选项
const nationOptions = dictStore.getDictList(SysDictEnum.NATION);
// 证件类型选项
const cardTypeOptions = dictStore.getDictList(SysDictEnum.IDCARD_TYPE);
//文化程度选项
const cultureLevelOptions = dictStore.getDictList(SysDictEnum.CULTURE_LEVEL);
onMounted(() => {
// 初始化默认值
// userInfo.value.nation = userInfo.value.nation ? userInfo.value.nation : nationOptions[0].value;
userInfo.value.idCardType = userInfo.value.idCardType ? userInfo.value.idCardType : cardTypeOptions[0].value;
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,111 @@
<!--
* @Description: 表单
* @Author: huguodong
* @Date: 2023-12-15 15:45:59
!-->
<template>
<div>
<form-container v-model="visible" :title="`${sysUserProps.opt}用户`" form-size="800px" @close="onClose">
<el-form
ref="sysUserFormRef"
:rules="rules"
:disabled="sysUserProps.disabled"
:model="sysUserProps.record"
:hide-required-asterisk="sysUserProps.disabled"
label-width="auto"
label-suffix=" :"
>
<el-tabs v-model="activeName">
<Basic v-model="sysUserProps.record"></Basic>
<More v-model="sysUserProps.record"></More>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="onClose"> 取消 </el-button>
<el-button v-show="!sysUserProps.disabled" type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</div>
</template>
<script setup lang="ts" name="sysUserForm">
import { SysUser, sysUserApi } from "@/api";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { required } from "@/utils/formRules";
import { FormInstance } from "element-plus";
import { useDictStore } from "@/stores/modules";
import Basic from "./form_basic.vue";
import More from "./form_more.vue";
const dictStore = useDictStore(); //字典仓库
const visible = ref(false); //是否显示表单
const activeName = ref("basic");
// 通用状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
// 表单参数
const sysUserProps = reactive<FormProps.Base<SysUser.SysUserInfo>>({
opt: FormOptEnum.ADD,
record: {},
disabled: false
});
// 表单验证规则
const rules = reactive({
account: [required("请输入账号")],
name: [required("请输入姓名")],
gender: [required("请选择性别")],
positionId: [required("请选择部门和职位")]
});
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysUser.SysUserInfo>) {
Object.assign(sysUserProps, props); //合并参数
if (props.opt == FormOptEnum.ADD) {
//如果是新增,设置默认值
sysUserProps.record.sortCode = 99;
sysUserProps.record.status = statusOptions[0].value;
}
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
sysUserApi.detail({ id: props.record.id }).then(res => {
sysUserProps.record = res.data;
});
}
}
// 提交数据(新增/编辑)
const sysUserFormRef = ref<FormInstance>();
/** 提交表单 */
async function handleSubmit() {
sysUserFormRef.value?.validate(async valid => {
if (!valid) return; //表单验证失败
//提交表单
await sysUserApi
.submitForm(sysUserProps.record, sysUserProps.record.id != undefined)
.then(() => {
sysUserProps.successful!(); //调用父组件的successful方法
})
.finally(() => {
onClose();
});
});
}
/** 关闭表单*/
function onClose() {
visible.value = false;
activeName.value = "basic";
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+251
View File
@@ -0,0 +1,251 @@
<!--
* @Description: 用户管理
* @Author: huguodong
* @Date: 2023-12-15 15:46:03
!-->
<template>
<div class="main-box">
<TreeFilter
ref="treeFilter"
label="name"
title="组织列表"
:request-api="sysOrgApi.tree"
:default-value="initParam.orgId"
@change="changeTreeFilter"
/>
<div class="table-box">
<ProTable ref="proTable" title="用户列表" :columns="columns" :request-api="sysUserApi.page" :init-param="initParam">
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<s-button suffix="用户" @click="onOpen(FormOptEnum.ADD)" />
<s-button
type="danger"
plain
suffix="用户"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="onDelete(scope.selectedListIds, '删除所选用户')"
/>
</template>
<!-- 头像 -->
<template #avatar="scope">
<el-avatar :src="scope.row.avatar" :size="30" />
<!-- <span>{{ scope.row.avatar }}}</span> -->
</template>
<!-- 状态 -->
<template #status="scope">
<el-switch :model-value="scope.row.status === CommonStatusEnum.ENABLE" :loading="switchLoading" @change="editStatus(scope.row)" />
</template>
<!-- 操作 -->
<template #operation="scope">
<el-space>
<s-button link :opt="FormOptEnum.EDIT" @click="onOpen(FormOptEnum.EDIT, scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete([scope.row.id], `删除【${scope.row.account}】用户`)" />
<el-dropdown @command="handleCommand">
<el-link type="primary" :underline="false" :icon="ArrowDown"> 更多 </el-link>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(item, index) in cmdEnum" :key="index" :command="command(scope.row, item)">{{ item }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-space>
</template>
</ProTable>
<!-- 新增/编辑表单 -->
<Form ref="formRef" />
<!-- 角色选择器组件 -->
<role-selector
ref="roleSelectorRef"
multiple
:org-tree-api="sysOrgApi.tree"
:role-selector-api="sysRoleApi.roleSelector"
@successful="handleChooseRole"
></role-selector>
<!-- 授权资源组件 -->
<GrantResource user ref="grantResourceRef" />
<!-- 授权权限组件 -->
<GrantPermission user ref="grantPermissionRef" />
</div>
</div>
</template>
<script setup lang="ts" name="SysUser">
import { sysOrgApi, sysUserApi, SysUser, SysRole, sysRoleApi } from "@/api";
import { ArrowDown } from "@element-plus/icons-vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { SysDictEnum, FormOptEnum, CommonStatusEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { useDictStore } from "@/stores/modules";
import TreeFilter from "@/components/TreeFilter/index.vue";
import Form from "./components/form/index.vue";
import { ElMessage } from "element-plus";
import { RoleSelectorInstance } from "@/components/Selectors/RoleSelector/interface";
import GrantResource from "../../limit/role/components/grantResource.vue";
import GrantPermission from "../../limit/role/components/grantPermission.vue";
const dictStore = useDictStore(); //字典仓库
// 状态选项
const statusOptions = dictStore.getDictList(SysDictEnum.COMMON_STATUS);
const treeFilter = ref<InstanceType<typeof TreeFilter> | null>(null);
//状态开关loading
const switchLoading = ref(false);
interface InitParam {
orgId: number | string;
}
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<InitParam>({ orgId: 0 });
// 获取 ProTable 元素
const proTable = ref<ProTableInstance>();
// 表格配置项
const columns: ColumnProps<SysUser.SysUserInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "searchKey", label: "姓名或账号", search: { el: "input" }, isShow: false },
{ prop: "avatar", label: "头像", width: 80 },
{ prop: "account", label: "账号" },
{ prop: "name", label: "姓名" },
{ prop: "gender", label: "性别", width: 80 },
{ prop: "phone", label: "手机号" },
{ prop: "orgName", label: "组织" },
{ prop: "positionName", label: "职位" },
{
prop: "status",
label: "状态",
width: 90,
enum: statusOptions,
search: { el: "select" }
},
{ prop: "createTime", label: "创建时间" },
{ prop: "operation", label: "操作", width: 230, fixed: "right" }
];
/** 部门切换 */
function changeTreeFilter(val: number | string) {
proTable.value!.pageable.pageNum = 1;
if (val != "") {
// 如果传入的val不为空,则将val赋值给initParam.parentId
initParam.orgId = val;
} else {
// 否则将initParam.parentId赋值为0
initParam.orgId = 0;
}
}
// 表单引用
const formRef = ref<InstanceType<typeof Form> | null>(null);
/**
* 打开表单
* @param opt 操作类型
* @param record 记录
*/
function onOpen(opt: FormOptEnum, record: {} | SysUser.SysUserInfo = {}) {
formRef.value?.onOpen({ opt: opt, record: record, successful: RefreshTable });
}
/**
* 删除
* @param ids id数组
*/
async function onDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(sysUserApi.delete, { ids }, msg);
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh(); //刷新表格
treeFilter.value?.refresh(); //刷新树形筛选器
}
/** 更多下拉菜单命令枚举 */
enum cmdEnum {
ResetPassword = "重置密码",
GrantRole = "授权角色",
GrantResource = "授权资源",
GrantPermission = "授权权限"
}
/** 下拉菜单参数接口 */
interface Command {
row: SysUser.SysUserInfo;
command: cmdEnum;
}
/**配置command的参数 */
function command(row: SysUser.SysUserInfo, command: cmdEnum): Command {
return {
row: row,
command: command
};
}
const roleSelectorRef = ref<RoleSelectorInstance>(); //角色选择器引用
const userId = ref<number | string>(0); //用户id
const grantResourceRef = ref<InstanceType<typeof GrantResource> | null>(null); //授权资源组件引用
const grantPermissionRef = ref<InstanceType<typeof GrantPermission> | null>(null); //授权权限组件引用
/**
* 更多下拉菜单点击事件
* @param command
*/
function handleCommand(command: Command) {
switch (command.command) {
case cmdEnum.ResetPassword:
useHandleData(sysUserApi.resetPassword, { id: command.row.id }, cmdEnum.ResetPassword);
break;
case cmdEnum.GrantRole:
userId.value = command.row.id; //获取用户id
sysUserApi.ownRole({ id: command.row.id }).then(res => {
roleSelectorRef.value?.showSelector(res.data); //显示用户选择器
});
break;
case cmdEnum.GrantResource:
// 打开授权资源组件
grantResourceRef.value?.onOpen({
opt: FormOptEnum.EDIT,
record: command.row,
successful: RefreshTable
});
break;
case cmdEnum.GrantPermission:
// 打开授权权限组件
grantPermissionRef.value?.onOpen({
opt: FormOptEnum.EDIT,
record: command.row,
successful: RefreshTable
});
break;
}
}
/** 选择角色 */
function handleChooseRole(data: SysRole.SysRoleInfo[]) {
//组装参数
const grantUser: SysUser.GrantRoleReq = {
id: userId.value,
roleIdList: data.map(item => item.id) as number[] | string[]
};
sysUserApi.grantRole(grantUser); //授权角色
}
/**
* 修改状态
* @param row 当前行数据
*/
function editStatus(row: SysUser.SysUserInfo) {
switchLoading.value = true;
const status = row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE; // 状态取反
sysUserApi.updateStatus({ id: row.id }, status).then(() => {
switchLoading.value = false;
ElMessage.success("修改成功");
row.status = status;
});
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,94 @@
<!--
* @Description: 基本信息
* @Author: huguodong
* @Date: 2024-03-06 10:20:46
!-->
<template>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="right"
label-width="170px"
class="pt-4 w-800px"
label-suffix=" :"
size="large"
>
<s-form-item label="账号" prop="account">
{{ formData.account }}
</s-form-item>
<s-form-item label="姓名" prop="name">
<s-input v-model="formData.name"></s-input>
</s-form-item>
<s-form-item label="手机" prop="phone">
<s-input v-model="formData.phone"></s-input>
</s-form-item>
<s-form-item label="昵称" prop="nickname">
<s-input v-model="formData.nickname"></s-input>
</s-form-item>
<s-form-item label="性别" prop="gender">
<s-radio-group v-model="formData.gender" :options="genderOptions" />
</s-form-item>
<s-form-item label="出生日期" prop="birthday">
<date-picker v-model="formData.birthday"></date-picker>
</s-form-item>
<s-form-item label="邮箱" prop="email">
<s-input v-model="formData.email"></s-input>
</s-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmit"> 保存基本信息 </el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { Login, userCenterApi } from "@/api";
import { SysDictEnum } from "@/enums";
import { useUserStore, useDictStore } from "@/stores/modules";
import { FormInstance } from "element-plus";
const userStore = useUserStore(); // 用户仓库
const dictStore = useDictStore(); // 字典仓库
const userInfo = userStore.userInfoGet; // 用户信息
const loading = ref(false); // 加载状态
const genderOptions = dictStore.getDictList(SysDictEnum.GENDER); // 性别选项
// 表单数据
const formData = ref<Login.LoginUserInfo>({
...(userInfo as Login.LoginUserInfo)
});
const formRules = {
name: [{ required: true, message: "请输入姓名", trigger: "blur" }] // 表单验证规则
};
const formRef = ref<FormInstance>();
/** 更新基本信息 */
function handleSubmit() {
formRef.value?.validate(async valid => {
if (!valid) return; // 表单验证失败
loading.value = true; // 加载中
// 提交表单
userCenterApi
.updateUserInfo(formData.value)
.then(() => {
// 更新前端缓存
userStore.setUserInfo(formData.value);
})
.finally(() => {
loading.value = false; // 加载完成
});
});
}
</script>
<style lang="scss" scoped>
/* 自定义CSS样式以控制标签和包装器的布局 */
.label-col {
flex-basis: 30%; /* 或者你想要的宽度 */
text-align: right;
}
.wrapper-col {
flex-grow: 1;
}
</style>
@@ -0,0 +1,138 @@
<!--
* @Description: 账号相关
* @Author: huguodong
* @Date: 2024-03-06 14:22:43
!-->
<template>
<div>
<el-row class="list-container">
<el-col :span="24" class="list-item" v-for="(item, index) in data" :key="index">
<!-- 可选的图标或头像部分 -->
<div v-if="item.avatar">
<svg-icon :icon="item.avatar" class="bind-icon" :style="{ color: item.color }"></svg-icon>
</div>
<!-- 标题和描述部分 -->
<div class="item-content">
<h3>{{ item.title }}</h3>
<p></p>
<p>
{{ item.description }}<span v-if="item.value"> : {{ item.value }}</span>
</p>
</div>
<!-- 操作部分 -->
<div class="item-action">
<el-button link @click="bindAction(item.bindType)">{{ item.value ? "修改" : "去绑定" }}</el-button>
</div>
</el-col>
</el-row>
<update-password ref="updatePasswordRef" />
</div>
</template>
<script setup lang="ts">
//导入修改密码组件
import UpdatePassword from "./updatePassword.vue";
const updatePasswordRef = ref<InstanceType<typeof UpdatePassword> | null>(null);
// 绑定数据接口
interface BindData {
title: string; // 标题
description: string; // 描述
value: string; // 值
bindType: BindType; // 类型
bindStatus: number; // 绑定状态
avatar?: string; // 头像
color?: string; // 颜色
}
// 绑定类型
enum BindType {
Password = "password",
QQ = "qq",
WeChat = "weChat",
AliPay = "alipay",
Gitee = "gitee"
}
// 获取绑定的情况
const data: BindData[] = [
{ title: "密码强度", description: "当前密码强度", value: "弱", bindType: BindType.Password, bindStatus: 0 },
/*{ title: '密保手机', description: '已绑定手机', value: '138****8293', type: 'phone', bindStatus: 1 },
{ title: '密保邮箱', description: '未绑定邮箱', value: '', type: 'email', bindStatus: 0 },
{ title: '实名状态', description: '未实名', value: '', type: 'userReal', bindStatus: 0 },*/
{ avatar: "ant-design:qq-outlined", title: "绑定QQ", description: "未绑定", value: "", bindType: BindType.QQ, bindStatus: 0, color: "1890FF" },
{
avatar: "ant-design:wechat-outlined",
title: "绑定微信",
description: "未绑定",
value: "",
bindType: BindType.WeChat,
bindStatus: 0,
color: "1AAD19"
},
{
avatar: "ant-design:alipay-circle-outlined",
title: "绑定支付宝",
description: "未绑定",
value: "",
bindType: BindType.AliPay,
bindStatus: 0,
color: "178bf5"
},
{ avatar: "simple-icons:gitee", title: "绑定Gitee", description: "未绑定", value: "", bindType: BindType.Gitee, bindStatus: 0, color: "d84040" }
];
/**
* 绑定操作
* @param bindType 绑定类型
*/
function bindAction(bindType: BindType) {
// 这里填充处理绑定操作的函数
if (bindType === BindType.Password) {
updatePasswordRef.value?.onOpen();
} else {
ElMessage.info("开发中");
}
}
</script>
<style lang="scss" scoped>
.list-container {
padding: 0;
margin: 0;
}
.list-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #f0f0f0;
}
.item-content {
flex-grow: 1;
color: #585252; /* 调整字体颜色为深灰色 */
}
.item-content h3 {
margin: 0 0 10px; /* 添加一些间距 */
font-size: 16px;
font-weight: normal; /* 调整标题字体为正常粗细 */
color: #242425; /* 标题字体颜色稍深一点 */
}
.item-content p {
margin: 0; /* 移除段落默认的上下外边距 */
font-size: 14px;
color: #8f8f94; /* 标题字体颜色稍深一点 */
}
.item-action {
margin-left: auto;
}
.item-action .el-button {
font-weight: normal;
color: #409eff; /* 为按钮文字设置主题颜色 */
}
.bind-icon {
padding-right: 10px;
padding-left: 10px;
font-size: 40px;
}
</style>
@@ -0,0 +1,190 @@
<!--
* @Description: 我的消息
* @Author: huguodong
* @Date: 2024-11-22 13:27:25
!-->
<template>
<div>
<el-row :gutter="10" class="min-h-450px">
<el-col :span="2">
<el-menu :default-active="activeName">
<el-menu-item v-for="(item, index) in messageTypeOptions" :key="index" :index="item.value" @click="menuClick">
<span>{{ item.label }}</span>
</el-menu-item>
</el-menu>
</el-col>
<el-col :span="20">
<ProTable
ref="proTable"
:tool-button="false"
title="消息列表"
:init-param="initParam"
:columns="columns"
:request-api="userCenterApi.myMessagePage"
>
<template #tableHeader="scope">
<el-space>
<!-- 表格 header 按钮 -->
<el-input v-model="initParam.searchKey" placeholder="请输入关键字">
<template #append>
<el-button :icon="Search" class="el-input-button" @click="RefreshTable" />
</template>
</el-input>
<s-button :opt="FormOptEnum.VIEW" @click="allRead()">全部已读</s-button>
<s-button :opt="FormOptEnum.VIEW" @click="read(scope.selectedListIds)" :disabled="!scope.isSelected">标记已读</s-button>
<s-button
type="danger"
plain
suffix="消息"
:opt="FormOptEnum.DELETE"
:disabled="!scope.isSelected"
@click="setDelete(scope.selectedListIds, '删除所选消息')"
/>
<s-button type="danger" :opt="FormOptEnum.DELETE" @click="allDelete">全部删除</s-button>
</el-space>
</template>
<!-- 表格 已读 -->
<template #read="scope">
<el-space wrap>
<span v-if="scope.row.read" style="color: #d9d9d9">已读</span>
<span v-else style="color: #ff4d4f">未读</span>
</el-space>
</template>
<!-- 操作 -->
<template #operation="scope">
<s-button link :opt="FormOptEnum.VIEW" @click="onOpenDetail(scope.row)" />
<s-button link :opt="FormOptEnum.DELETE" @click="onDelete(scope.row, `删除【${scope.row.subject}】信息`)" />
</template>
</ProTable>
</el-col>
</el-row>
<!-- 详情表单 -->
<MyMessageDetail ref="detailRef" />
</div>
</template>
<script setup lang="ts">
import { Search } from "@element-plus/icons-vue";
import { useDictStore, useMessageStore } from "@/stores/modules";
import { FormOptEnum, SysDictEnum } from "@/enums";
import { useHandleData } from "@/hooks/useHandleData";
import { MenuItemRegistered } from "element-plus";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { SysMessage, userCenterApi, UserCenter } from "@/api";
import MyMessageDetail from "./myMessageDetail.vue";
const dictStore = useDictStore();
const messageStore = useMessageStore();
const proTable = ref<ProTableInstance>();
// 消息类型选项
const messageTypeOptions = dictStore.getDictList(SysDictEnum.MESSAGE_CATEGORY);
const activeName = ref(messageTypeOptions[0].value); // 默认选中第一个
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive<UserCenter.ReqMyMessagePage>({ category: messageTypeOptions[0].value }); //主表格初始化参数
// 表格配置项
const columns: ColumnProps<SysMessage.SysMessageInfo>[] = [
{ type: "selection", fixed: "left", width: 50 },
{ prop: "subject", label: "主题" },
{ prop: "content", label: "正文" },
{ prop: "sendTime", label: "发送时间" },
{ prop: "read", label: "是否已读" },
{ prop: "operation", label: "操作", width: 160, fixed: "right" }
];
/** 点击菜单 */
function menuClick(item: MenuItemRegistered) {
initParam.category = item.index;
activeName.value = item.index;
}
async function onDelete(record: SysMessage.SysMessageInfo, msg: string) {
let ids = [record.id];
// 二次确认 => 请求api => 刷新表格
const res = await useHandleData(userCenterApi.setDelete, { ids }, msg);
if (res) {
// 更新未读消息数量
if (record.read == false) {
messageStore.unReadCountSubtract(1);
}
RefreshTable();
}
}
/** 删除消息 */
async function setDelete(ids: string[], msg: string) {
// 二次确认 => 请求api => 刷新表格
await useHandleData(userCenterApi.setDelete, { ids }, msg);
messageStore.getUnReadInfo(); //更新未读消息数量
RefreshTable();
}
// 表单引用
const detailRef = ref<InstanceType<typeof MyMessageDetail> | null>(null);
/**
* 打开详情
* @param opt 操作类型
* @param record 记录
*/
function onOpenDetail(record: SysMessage.SysMessageInfo) {
record.read = true;
// 打开详情
detailRef.value?.onOpen({ opt: FormOptEnum.VIEW, record: record, successful: RefreshTable });
}
/** 标记已读 */
function read(ids: string[] | number[]) {
//参数转reqId[]
// const params = record.map(item => ({ id: item }));
userCenterApi.setRead({ ids }).then(() => {
// 更新未读消息数量
messageStore.unReadCountSubtract(ids.length);
// 标记已读成功
RefreshTable();
});
}
/** 全部已读 */
function allRead() {
userCenterApi
.allRead({
category: activeName.value
})
.then(res => {
// 更新未读消息数量,res.data为未读消息数量,转为number
let count = Number(res.data);
if (count > 0) {
messageStore.unReadCountSubtract(count);
}
// 标记已读成功
RefreshTable();
ElMessage.success("全部已读成功");
});
}
/** 全部删除 */
async function allDelete() {
// 二次确认 => 请求api => 刷新表格
await useHandleData(
userCenterApi.setDelete,
{
category: activeName.value
},
"删除全部消息"
);
messageStore.getUnReadInfo(); //更新未读消息数量
RefreshTable();
}
/**
* 刷新表格
*/
function RefreshTable() {
proTable.value?.refresh();
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,68 @@
<!--
* @Description: 站内信详情
* @Author: huguodong
* @Date: 2024-11-11 09:22:49
!-->
<template>
<form-container v-model="visible" title="消息详情" form-size="600px">
<h4 class="title">基本信息</h4>
<el-descriptions :column="1" border>
<el-descriptions-item label="主题" label-align="left">
{{ messageProps.record.subject }}
</el-descriptions-item>
<el-descriptions-item label="发送时间" label-align="left">
{{ messageProps.record.sendTime }}
</el-descriptions-item>
</el-descriptions>
<h4 class="title">正文:</h4>
<div v-html="messageProps.record.content"></div>
<template #footer>
<el-button type="primary" @click="onClose"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { SysMessage, userCenterApi } from "@/api";
import { FormOptEnum } from "@/enums";
import { useMessageStore } from "@/stores/modules";
const messageStore = useMessageStore();
const visible = ref(false); //是否显示表单
// 表单参数
const messageProps = reactive<FormProps.Base<SysMessage.SysMessageInfo>>({
opt: FormOptEnum.VIEW,
record: {},
disabled: false
});
/** 关闭表单*/
function onClose() {
visible.value = false;
}
/**
* 打开表单
* @param props 表单参数
*/
function onOpen(props: FormProps.Base<SysMessage.SysMessageInfo>) {
Object.assign(messageProps, props); //合并参数
visible.value = true; //显示表单
if (props.record.id) {
//如果传了id,就去请求api获取record
userCenterApi.myMessageDetail({ id: props.record.id }).then(res => {
messageProps.record = res.data;
messageStore.unReadCountSubtract(1); //未读数量减1
});
}
}
// 暴露给父组件的方法
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,45 @@
<!--
* @Description: 快捷设置
* @Author: huguodong
* @Date: 2024-03-06 16:09:43
!-->
<template>
<el-form ref="formRef" :model="formData" label-position="right" label-width="170px" class="pt-4 w-800px" label-suffix=" :" size="large">
<s-form-item label="默认快捷方式" prop="shortcuts">
<MenuSelector v-model:menu-value="formData.shortcuts" multiple :check-strictly="false" :menu-tree-api="userCenterApi.shortcutTree" />
</s-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmit"> 保存基本信息 </el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { userCenterApi } from "@/api";
const loading = ref(false); // 加载状态
const formData = reactive({
shortcuts: []
});
onMounted(() => {
userCenterApi.loginWorkbench().then(res => {
// 设置组件回显
formData.shortcuts = JSON.parse(res.data).shortcut;
console.log("[ formData.shortcuts ] >", formData.shortcuts);
});
});
/** 更新快捷方式 */
function handleSubmit() {
const workbenchData = {
shortcut: formData.shortcuts
};
loading.value = true;
userCenterApi.updateUserWorkbench({ WorkbenchData: JSON.stringify(workbenchData) }).finally(() => {
loading.value = false;
});
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,119 @@
<!--
* @Description: 电子签名
* @Author: huguodong
* @Date: 2024-03-01 13:22:09
!-->
<template>
<form-container v-model="visible" title="电子签名" form-size="1500px">
<el-row :gutter="5">
<el-col :span="15">
<div style="border: 1px solid rgb(236 236 236)">
<e-sign
ref="esignRef"
:width="800"
:height="300"
:image="props.image"
:is-crop="options.isCrop"
:line-width="options.lineWidth"
:line-color="options.lineColor"
v-model:bg-color="options.bgColor"
/>
</div>
</el-col>
<el-col :span="9">
<div style="width: auto; height: 90px">
<img :src="resultImg" style="width: 100%; height: 90px; border: 1px solid rgb(236 236 236)" />
</div>
</el-col>
</el-row>
<div style="margin-top: 10px">
<el-space>
<el-form>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="画笔粗细:">
<el-input-number v-model="options.lineWidth" :min="1" :max="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-button type="primary" @click="handleGenerate">预览</el-button>
<el-button @click="handleReset">清屏</el-button>
</el-col>
</el-row>
</el-form>
</el-space>
</div>
<template #footer>
<el-button @click="visible = false"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { ESignInstance } from "@/components/ESign/interface";
const visible = ref(false); //是否显示表单
const options = reactive({
isCrop: false, //是否开启裁剪
lineWidth: 6, //线条宽度
lineColor: "#000000", //线条颜色
bgColor: "" //背景颜色
});
const props = defineProps({
image: {
type: String,
default: ""
}
});
const emit = defineEmits(["successful"]);
const esignRef = ref<ESignInstance>(); //签名容器
const resultImg = ref(props.image); //签名结果
function open() {
visible.value = true;
}
function handleGenerate() {
if (esignRef.value) {
//生成签名
esignRef.value
.generate(null)
.then(res => {
console.log("[ res ] >", res);
resultImg.value = res as string;
})
.catch(() => {
ElMessage.error("无任何签名");
});
}
}
function handleReset() {
//清空签名
resultImg.value = "";
esignRef.value?.reset();
}
function handleSubmit() {
if (esignRef.value) {
//生成签名
esignRef.value
.generate(null)
.then(res => {
console.log("[ res ] >", res);
emit("successful", res);
visible.value = false;
})
.catch(() => {
ElMessage.error("无任何签名");
});
}
}
defineExpose({
open
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,96 @@
<!--
* @Description: 修改密码
* @Author: huguodong
* @Date: 2024-03-06 15:30:11
!-->
<template>
<form-container v-model="visible" title="修改密码" form-size="700px">
<el-form ref="updatePasswordFormRef" :rules="rules" :model="updatePasswordProps" label-width="auto" label-suffix=" :">
<s-form-item label="旧密码" prop="oldPassword">
<s-input v-model="updatePasswordProps.oldPassword"></s-input>
</s-form-item>
<s-form-item label="新密码" prop="newPassword">
<s-input v-model="updatePasswordProps.newPassword" type="password"></s-input>
</s-form-item>
<s-form-item label="新密码" prop="checkNewPassword">
<s-input v-model="updatePasswordProps.checkNewPassword" type="password"></s-input>
</s-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel"> 取消 </el-button>
<el-button type="primary" @click="handleSubmit"> 确定 </el-button>
</template>
</form-container>
</template>
<script setup lang="ts">
import { FormInstance } from "element-plus";
import { UserCenter, userCenterApi } from "@/api";
import smCrypto from "@/utils/smCrypto";
import { required } from "@/utils/formRules";
const visible = ref(false); //是否显示表单
const updatePasswordFormRef = ref<FormInstance>(); //表单实例
// 修改密码参数
const updatePasswordProps = reactive({
oldPassword: "",
newPassword: "",
checkNewPassword: ""
});
// 修改密码验证规则
const rules = {
oldPassword: [required("请输入旧密码")],
newPassword: [required("请输入新密码")],
checkNewPassword: [
required("请再次输入新密码"),
{
validator: (rule: any, value: string, callback: (arg0: Error | undefined) => void) => {
if (value !== updatePasswordProps.newPassword) {
callback(new Error("两次输入密码不一致"));
} else {
callback(undefined);
}
},
trigger: "blur"
}
]
};
/** 提交表单 */
async function handleSubmit() {
updatePasswordFormRef.value?.validate(async valid => {
console.log("[ 123 ] >", 123);
if (!valid) {
return; // 表单验证失败
}
// 加密
let fromData: UserCenter.ReqUpdatePassword = {
password: smCrypto.doSm2Encrypt(updatePasswordProps.oldPassword),
newPassword: smCrypto.doSm2Encrypt(updatePasswordProps.newPassword)
};
// 提交数据
userCenterApi.updatePassword(fromData).then(() => {
handleCancel();
});
});
}
/** 打开表单 */
function onOpen() {
visible.value = true;
}
function handleCancel() {
visible.value = false;
updatePasswordFormRef.value?.resetFields();
}
defineExpose({
onOpen
});
</script>
<style lang="scss" scoped></style>
+237
View File
@@ -0,0 +1,237 @@
<!--
* @Description: 个人中心
* @Author: huguodong
* @Date: 2024-03-01 09:56:39
!-->
<template>
<div>
<el-row :gutter="24">
<el-col :lg="7" :md="24" style="padding-right: 0">
<el-card>
<div class="account-center-avatarHolder">
<div class="avatar">
<img :src="userInfo?.avatar" />
<a style="cursor: pointer" @click="uploadAvatar">
<div :class="userInfo?.avatar ? 'mask' : 'mask-notImg'">
<el-icon><Upload /></el-icon>
</div>
</a>
</div>
<div class="username">{{ userInfo?.name }}</div>
<div class="bio">{{ userInfo?.nickname }}</div>
</div>
<div class="account-center-detail">
<p><i class="title"></i>{{ userInfo?.positionName }}</p>
<p><i class="group"></i>{{ userInfo?.orgNames }}</p>
<p>
<i class="address"></i>
<span>{{ userInfo?.homeAddress ? userInfo?.homeAddress : "暂无地址" }}</span>
</p>
</div>
<el-divider />
<div class="account-center-team">
<div v-if="userInfo?.signature" class="mb-2" style="width: 100%">
<el-image :src="userInfo?.signature" style="height: 120px; border: 1px solid rgb(236 236 236)" width="100%" />
</div>
<el-button @click="openSignName">打开签名板</el-button>
</div>
</el-card>
</el-col>
<el-col :lg="17" :md="24">
<div class="card">
<el-tabs v-model="activeName" class="-mt-4" @tab-click="tabClick">
<el-tab-pane label="基本信息" :name="centerStore.getAccountBasic"><AccountBasic /></el-tab-pane>
<el-tab-pane label="账号相关" :name="centerStore.getFile"><AccountBind /></el-tab-pane>
<el-tab-pane label="快捷方式" :name="centerStore.getShort"><ShortcutSetting /></el-tab-pane>
<el-tab-pane label="我的消息" :name="centerStore.getMessage"><MyMessage /></el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
<sign-name ref="signNameRef" :image="userInfo?.signature" @successful="signSuccess"></sign-name>
<crop-upload ref="cropUploadRef" :img-src="userInfo?.avatar" @successful="uploadSuccess"></crop-upload>
</div>
</template>
<script setup lang="ts" name="userCenter">
import { userCenterApi } from "@/api";
import SignName from "./components/signName.vue";
import AccountBasic from "./components/accountBasic.vue";
import AccountBind from "./components/accountBind.vue";
import ShortcutSetting from "./components/shortcutSetting.vue";
import MyMessage from "./components/myMessage.vue";
import { useUserStore, useCenterStore } from "@/stores/modules";
import { CropUploadInstance } from "@/components/CropUpload/interface";
import { TabsPaneContext } from "element-plus";
const centerStore = useCenterStore();
const userStore = useUserStore();
const userInfo = userStore.userInfoGet;
const activeName = ref(centerStore.getTab);
const cropUploadRef = ref<CropUploadInstance>();
// Tab Click
const tabClick = (tabItem: TabsPaneContext) => {
const name = tabItem.props.name as string;
centerStore.setTab(name);
};
/** 上传头像 */
function uploadAvatar() {
cropUploadRef.value?.show();
}
const signNameRef = ref();
/** 打开签名板 */
function openSignName() {
signNameRef.value.open();
}
/** 签名成功回调 */
function signSuccess(url: string) {
console.log("[ url ] >", url);
const param = {
signature: url
};
userCenterApi.updateSignature(param).then(() => {
userStore.setUserInfoItem("signature", url);
});
}
/** 上传成功回调 */
function uploadSuccess(data: { fileName: string; blobData: any }) {
let result = new File([data.blobData], data.fileName, {
type: "image/jpeg",
lastModified: Date.now()
});
// 如果没有文件名,就转换为file类型,否则就是file类型
if (data.fileName === "") {
result = blobToFile(data.blobData, "avatar.jpg")!;
}
let formData = new FormData();
formData.append("file", result);
userCenterApi.updateAvatar(formData).then(res => {
userStore.setUserInfoItem("avatar", res.data);
});
}
/** blob转file */
function blobToFile(blob: BlobPart, fileName: string) {
return new File([blob], fileName, {
type: "image/jpeg",
lastModified: Date.now()
});
}
/** 监听是否点击了显示更多 */
centerStore.$subscribe((mutations, state) => {
activeName.value = state.tab;
});
</script>
<style lang="scss" scoped>
.account-center-avatarHolder {
margin-bottom: 24px;
text-align: center;
& > .avatar {
width: 104px;
height: 104px;
margin: 0 auto;
margin-bottom: 20px;
overflow: hidden;
border-radius: 50%;
img {
width: 100%;
height: 100%;
}
}
.mask {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 104px;
height: 104px;
margin-top: -104px;
font-size: 25px;
color: #ffffff;
background: rgb(101 101 101 / 60%);
border-radius: 50%;
opacity: 0;
}
.mask-notImg {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 104px;
height: 104px;
margin-top: -24px;
font-size: 25px;
color: #ffffff;
background: rgb(101 101 101 / 60%);
border-radius: 50%;
opacity: 0;
}
.avatar a:hover .mask {
opacity: 1;
}
.avatar a:hover .mask-notImg {
opacity: 1;
}
.username {
margin-bottom: 4px;
font-size: 20px;
font-weight: 500;
line-height: 28px;
}
}
.account-center-detail {
p {
position: relative;
padding-left: 26px;
margin-bottom: 8px;
}
i {
position: absolute;
top: 4px;
left: 0;
width: 14px;
height: 14px;
background: url("/src/assets/icons/userDetail.svg");
}
.title {
background-position: 0 0;
}
.group {
background-position: 0 -22px;
}
.address {
background-position: 0 -44px;
}
}
.account-center-team {
.members {
a {
display: block;
height: 24px;
margin: 12px 0;
line-height: 24px;
.member {
display: inline-block;
max-width: 100px;
margin-left: 12px;
font-size: 14px;
line-height: 24px;
vertical-align: top;
transition: all 0.3s;
}
}
}
}
:deep(.el-tabs__item) {
font-size: large;
}
</style>