This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
<template>
<el-dialog :title="dataForm.deptId ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible" width="600">
<el-form ref="deptDialogFormRef" :model="dataForm" label-width="90px" :rules="dataRules" v-loading="loading">
<el-form-item :label="$t('sysdept.parentId')" prop="parentId">
<el-tree-select
v-model="dataForm.parentId"
:data="parentData"
:props="{ value: 'id', label: 'name', children: 'children' }"
class="w100"
clearable
check-strictly
:render-after-expand="false"
:placeholder="$t('sysdept.inputparentIdTip')"
/>
</el-form-item>
<el-form-item :label="$t('sysdept.name')" prop="name">
<el-input v-model="dataForm.name" :placeholder="$t('sysdept.inputnameTip')" clearable />
</el-form-item>
<el-form-item :label="$t('sysdept.sortOrder')" prop="sortOrder">
<el-input-number v-model="dataForm.sortOrder" :placeholder="$t('sysdept.inputsortOrderTip')" clearable />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="systemDeptDialog">
import { useI18n } from 'vue-i18n';
import { getObj, deptTree, addObj, putObj } from '/@/api/admin/dept';
import { useMessage } from '/@/hooks/message';
import {rule} from "/@/utils/validate";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const deptDialogFormRef = ref();
const dataForm = reactive({
parentId: '',
deptId: '',
name: '',
sortOrder: 9999,
});
const parentData = ref<any[]>([]);
const visible = ref(false);
const loading = ref(false);
const dataRules = ref({
parentId: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }],
name: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '部门名称不能为空', trigger: 'blur' }],
sortOrder: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (type: string, id: string) => {
visible.value = true;
dataForm.deptId = '';
nextTick(() => {
deptDialogFormRef.value?.resetFields();
dataForm.parentId = id;
});
if (type === 'edit') {
getObj(id)
.then((res) => {
Object.assign(dataForm, res.data);
})
.catch((err) => {
useMessage().error(err.msg);
});
}
getDeptData();
};
// 提交
const onSubmit = async () => {
// 立即设置 loading防止重复点击
if (loading.value) return;
loading.value = true;
try {
const valid = await deptDialogFormRef.value.validate().catch(() => {});
if (!valid) {
loading.value = false;
return false;
}
dataForm.deptId ? await putObj(dataForm) : await addObj(dataForm);
useMessage().success(t(dataForm.deptId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 从后端获取菜单信息
const getDeptData = async () => {
deptTree().then((res) => {
parentData.value = [];
const dept = {
id: '0',
name: '根部门',
children: [] as any[],
};
dept.children = res.data;
parentData.value.push(dept);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,21 @@
export default {
sysdept: {
name: 'dept name',
parentId: 'parent dept',
createTime: 'createTime',
weight: 'weight',
leaderId: 'dept leader',
sortOrder: 'sortOrder',
inputdeptNameTip: 'input deptName',
inputnameTip: 'input deptName',
inputparentIdTip: 'select deptName',
inputLeaderIdTip: 'input leader',
inputsortOrderTip: 'input sortOrder',
importTip: 'import dept',
addNodeText:'add dept',
editNodeText:'edit dept',
delNodeText:'delete dept',
view: 'tree/table view',
tenantNodeErrorText: 'The current node cannot be operated. You need to maintain it in tenant management',
},
};

View File

@@ -0,0 +1,21 @@
export default {
sysdept: {
name: '部门名称',
parentId: '上级部门',
createTime: '创建时间',
weight: '排序',
sortOrder: '排序',
leaderId: '部门负责人',
inputdeptNameTip: '请输入部门名称',
inputnameTip: '请输入部门名称',
inputLeaderIdTip: '请输入部门领导',
inputparentIdTip: '请选择上级部门',
inputsortOrderTip: '请输入排序',
importTip: '导入部门',
addNodeText: '添加部门',
editNodeText: '编辑部门',
delNodeText: '删除部门',
tenantNodeErrorText: '当前节点不可操作,请在租户管理功能中维护',
view: '树/表视图'
},
};

View File

@@ -0,0 +1,130 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="filter">
<el-form-item prop="deptName" :label="$t('sysdept.name')">
<el-input :placeholder="$t('sysdept.inputdeptNameTip')" style="max-width: 180px" v-model="state.queryForm.deptName"></el-input>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="filter">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="top-right-btn" v-if="!defaultTreeViewRef" v-auth="'sys_dept_add'" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain icon="upload-filled" type="primary" class="ml10" @click="excelUploadRef.show()">
{{ $t('common.importBtn') }}
</el-button>
<el-button @click="handleExpand"> {{ $t('common.expandBtn') }}</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_dept_add'"
@exportExcel="exportExcel"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
>
<el-tooltip class="item" effect="dark" :content="$t('queryTree.view')" placement="top">
<el-button circle icon="Grid" @click="handleView"></el-button>
</el-tooltip>
</right-toolbar>
</div>
</el-row>
<tree-view ref="treeViewRef" v-if="defaultTreeViewRef" />
<table-view ref="tableViewRef" v-if="!defaultTreeViewRef" />
<upload-excel
ref="excelUploadRef"
:title="$t('sysdept.importTip')"
url="/admin/dept/import"
temp-url="/admin/sys-file/local/file/dept.xlsx"
@refreshDataList="getDataList"
/>
</div>
</div>
</template>
<script lang="ts" name="systemDept" setup>
import { downBlobFile } from '/@/utils/other';
import { useI18n } from 'vue-i18n';
const TreeView = defineAsyncComponent(() => import('./tree-view.vue'));
const TableView = defineAsyncComponent(() => import('./table-view.vue'));
const { t } = useI18n();
// 默认树视图展示
const defaultTreeViewRef = ref(true);
const treeViewRef = ref();
const tableViewRef = ref();
const excelUploadRef = ref();
const showSearch = ref(true);
const isExpand = ref(false);
const queryRef = ref();
const state = reactive({
queryForm: {
deptName: '',
},
});
/**
* 过滤节点
*/
const filter = () => {
if (defaultTreeViewRef.value) {
treeViewRef.value.filter(state.queryForm.deptName);
} else {
tableViewRef.value.state.queryForm.deptName = state.queryForm.deptName;
tableViewRef.value.getDataList();
}
};
const handleAdd = () => {
tableViewRef.value.handleAdd();
};
const handleView = () => {
defaultTreeViewRef.value = !defaultTreeViewRef.value;
};
/**
* 处理展开/折叠树
*/
const handleExpand = () => {
if (defaultTreeViewRef.value) {
treeViewRef.value.handleExpand();
} else {
tableViewRef.value.handleExpand();
}
};
/**
* 导出Excel
*/
const exportExcel = () => {
downBlobFile('/admin/dept/export', state.queryForm, 'dept.xlsx');
};
const getDataList = () => {
if (defaultTreeViewRef.value) {
treeViewRef.value.getOrgData();
} else {
tableViewRef.value.getDataList();
}
};
/**
* 重置查询条件
*/
const resetQuery = () => {
queryRef.value.resetFields();
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
style="width: 100%"
row-key="id"
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle?.headerCellStyle"
>
<el-table-column :label="$t('sysdept.name')" prop="name" width="400" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysdept.weight')" prop="weight" show-overflow-tooltip width="80"></el-table-column>
<el-table-column prop="createTime" :label="$t('sysdept.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250">
<template #default="scope">
<el-button text type="primary" icon="folder-add" @click="deptDialogRef.openDialog('add', scope.row?.id)"
v-auth="'sys_dept_add'">
{{ $t('common.addBtn') }}
</el-button
>
<el-button text type="primary" icon="edit-pen" @click="deptDialogRef.openDialog('edit', scope.row?.id)"
v-auth="'sys_dept_edit'">{{
$t('common.editBtn')
}}
</el-button>
<el-button text type="primary" icon="delete" @click="handleDelete(scope.row)" v-auth="'sys_dept_del'">
{{ $t('common.delBtn') }}
</el-button
>
</template>
</el-table-column>
</el-table>
<dept-form ref="deptDialogRef" @refresh="getDataList()"/>
</template>
<script setup lang="ts" name="systemDept">
import {BasicTableProps, useTable} from '/@/hooks/table';
import {deptTree, delObj} from '/@/api/admin/dept';
import {useMessage, useMessageBox} from '/@/hooks/message';
import {useI18n} from 'vue-i18n';
// 引入组件
const DeptForm = defineAsyncComponent(() => import('./form.vue'));
const {t} = useI18n();
// 定义变量内容
const tableRef = ref(); // 表格引用
const deptDialogRef = ref(); // 部门对话框引用
const excelUploadRef = ref(); // Excel上传引用
const showSearch = ref(true); // 是否显示搜索栏
const isExpand = ref(false); // 是否展开
/**
* 查询部门树方法,返回 Promise 对象
* @param params - 查询参数
* @returns Promise&lt;any&gt;
*/
const queryDeptTree = (params?: any) => {
return deptTree(params);
};
/**
* 定义响应式表格数据
*/
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: queryDeptTree, // 页面列表数据
queryForm: {
deptName: '', // 部门名称
},
isPage: false, // 是否分页
descs: ['create_time'], // 排序字段
});
/**
* 使用 useTable 定义表格相关操作
*/
const {getDataList, tableStyle} = useTable(state);
/**
* 展开/折叠部门树方法
*/
const handleExpand = async () => {
isExpand.value = !isExpand.value;
const dataList = await deptTree();
toggleExpand(dataList.data, isExpand.value);
};
/**
* 递归方法,用于展开/折叠部门树
* @param children - 子节点
* @param unfold - 是否展开
*/
const toggleExpand = (children: any[], unfold = true) => {
for (const key in children) {
tableRef.value?.toggleRowExpansion(children[key], unfold);
if (children[key].children) {
toggleExpand(children[key].children!, unfold);
}
}
};
/**
* 删除当前行
* @param row - 当前行数据
*/
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(row.id);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
const handleAdd = ()=>{
deptDialogRef.value.openDialog('add')
}
/**
* 暴露组件中的一些方法和变量
*/
defineExpose({
handleAdd, // 新增时间
state, // 响应式表格数据
getDataList, // 获取列表数据方法
handleExpand // 展开/折叠部门树方法
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div style="height: 100%">
<vue3-tree-org
ref="treeOrgRef"
:props="props"
:data="data"
:label-style="style"
:define-menus="defineMenus"
:default-expand-level="expandLevel"
center
:horizontal="horizontal"
:collapsable="collapsable"
:only-one-node="onlyOneNode"
:filter-node-method="filterNodeMethod"
:clone-node-drag="cloneNodeDrag"
:node-add="addNode"
:node-delete="delNode"
:node-edit="editNode"
@on-node-click="onNodeClick"
/>
</div>
<dept-form ref="deptDialogRef" @refresh="getOrgData()" />
</template>
<script lang="ts" name="treeView" setup>
import { useMessage, useMessageBox } from '/@/hooks/message';
import { delObj, deptTree } from '/@/api/admin/dept';
import { getObj } from '/@/api/admin/tenant';
import { Session } from '/@/utils/storage';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
const DeptForm = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义org组件key-value定义
const props = reactive({ id: 'id', pid: 'parentId', label: 'name', expand: 'expand', children: 'children' });
const data = reactive({});
// 定义org组件右键定义
const defineMenus = reactive([
{ name: t('sysdept.addNodeText'), command: 'add' },
{ name: t('sysdept.editNodeText'), command: 'edit' },
{ name: t('sysdept.delNodeText'), command: 'delete' },
]);
const cloneNodeDrag = ref(true);
const collapsable = ref(false);
const expandLevel = ref(2); //默认展开层级
const horizontal = ref(false);
const onlyOneNode = ref(false);
const treeOrgRef = ref();
const deptDialogRef = ref();
// 添加主题配置
const themeConfig = useThemeConfig();
const { themeConfig: theme } = storeToRefs(themeConfig);
// 添加 style 定义
const style = computed(() => ({
background: theme.value.isDark ? 'var(--el-bg-color-overlay)' : 'var(--el-bg-color-page)',
color: theme.value.isDark ? 'var(--el-text-color-primary)' : '#5e6d82',
}));
/**
* 过滤节点
*/
const filter = (deptName: string) => {
treeOrgRef.value.filter(deptName);
};
/**
* 节点过滤方法
* @param {string} value 过滤条件
* @param {object} data 节点数据
* @returns {boolean} 返回过滤结果
*/
const filterNodeMethod = (value, data) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/**
* 处理展开/折叠树
*/
const handleExpand = async () => {
collapsable.value = !collapsable.value;
};
/**
* 检查节点是否是根租户节点
* @param {object} node 节点对象
* @returns {boolean} 如果节点是根租户节点返回true否则返回false
*/
const checkNode = (node) => {
if (node?.id === '0') {
useMessage().error(t('sysdept.tenantNodeErrorText'));
return false;
}
return true;
};
/**
* 当用户左键点击节点,模拟触发组件的右键事件
* @param e
*/
const onNodeClick = (e: any) => {
const { clientX, clientY } = e;
const rightFun = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: false,
view: window,
button: 2,
buttons: 0,
clientX,
clientY,
});
e.target.dispatchEvent(rightFun);
};
/**
* 添加部门
* @param {object} node 节点对象
*/
const addNode = (node) => {
deptDialogRef.value.openDialog('add', node?.id);
};
/**
* 编辑部门
* @param {object} node 节点对象
*/
const editNode = (node) => {
if (!checkNode(node)) {
return;
}
deptDialogRef.value.openDialog('edit', node?.id);
};
/**
* 删除部门
* @param {object} node 节点对象
*/
const delNode = async (node) => {
if (!checkNode(node)) {
return;
}
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(node.id);
await getOrgData();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
/**
* 查询部门数据
*/
const getOrgData = async () => {
// 查询当前租户信息
const tenant = await getObj(Session.getTenant());
deptTree().then((res) => {
Object.assign(data, { id: '0', name: tenant.data.name });
data.children = res.data;
});
};
onMounted(() => {
getOrgData();
});
// 暴露变量
defineExpose({
getOrgData,
handleExpand,
filter,
});
</script>
<style lang="scss" scoped>
:deep(.zm-tree-org) {
height: 100%;
padding: 15px;
position: relative;
background: var(--el-bg-color);
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,280 @@
<template>
<el-dialog
:title="state.ruleForm.menuId ? $t('common.editBtn') : $t('common.addBtn')"
v-model="visible"
width="600"
:close-on-click-modal="false"
draggable
>
<el-form ref="menuDialogFormRef" :model="state.ruleForm" :rules="dataRules" label-width="100px" v-loading="loading">
<el-form-item :label="$t('sysmenu.menuType')" prop="menuType">
<el-radio-group v-model="state.ruleForm.menuType">
<el-radio border label="0">菜单</el-radio>
<el-radio border label="1">按钮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('sysmenu.parentId')" prop="parentId">
<el-tree-select
v-model="state.ruleForm.parentId"
:data="state.parentData"
:render-after-expand="false"
:props="{ value: 'id', label: 'name', children: 'children' }"
class="w100"
clearable
check-strictly
:placeholder="$t('sysmenu.inputParentIdTip')"
>
</el-tree-select>
</el-form-item>
<el-form-item prop="name">
<template #label>
{{ state.ruleForm.menuType === '0' ? t('sysmenu.name') : t('sysmenu.buttonName') }}
</template>
<el-input v-model="state.ruleForm.name" clearable :placeholder="$t('sysmenu.inputNameTip')"></el-input>
</el-form-item>
<el-form-item :label="$t('sysmenu.path')" prop="path" v-if="state.ruleForm.menuType === '0'">
<el-input v-model="state.ruleForm.path" :placeholder="$t('sysmenu.inputPathTip')"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.permission')" prop="permission" v-if="state.ruleForm.menuType === '1'">
<template #label>
{{ t('sysmenu.permission') }}
<tip content="对应后台接口@PreAuthorize注解入参字符串"></tip>
</template>
<el-input v-model="state.ruleForm.permission" maxlength="30" :placeholder="$t('sysmenu.inputPermissionTip')"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder">
<el-input-number v-model="state.ruleForm.sortOrder" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.icon')" prop="icon" v-if="state.ruleForm.menuType === '0'">
<IconSelector :placeholder="$t('sysmenu.inputIconTip')" v-model="state.ruleForm.icon"/>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item prop="keepAlive" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.keepAlive') }}
<tip content="组件保留状态,避免重新渲染"/>
</template>
<el-radio-group v-model="state.ruleForm.keepAlive">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="visible" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.visible') }}
<tip content="左侧菜单树是否显示"/>
</template>
<el-radio-group v-model="state.ruleForm.visible">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row class="mt-4">
<el-col :span="12">
<el-form-item prop="param" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.param') }}
<tip content="多个路径指向同一个组件"/>
</template>
<el-radio-group v-model="state.ruleForm.param">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="embedded"
v-if="state.ruleForm.menuType === '0' && state.ruleForm.path?.startsWith('http')">
<template #label> {{ $t('sysmenu.embedded') }}
<tip content="iframe嵌套还是打开独立的Tab"/>
</template>
<el-radio-group v-model="state.ruleForm.embedded">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="mt-4" :label="$t('sysmenu.component')" prop="component" v-if="state.ruleForm.menuType === '0'
&& state.ruleForm.param === '1'">
<el-input v-model="state.ruleForm.component" :placeholder="$t('sysmenu.inputComponentTip')"/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="systemMenuDialog">
import {useI18n} from 'vue-i18n';
import {getObj, pageList, putObj, addObj, validateExist} from '/@/api/admin/menu';
import {useMessage} from '/@/hooks/message';
import {rule, validateNull} from "/@/utils/validate";
import Tip from "/@/components/Tip/index.vue";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const {t} = useI18n();
// 引入组件
const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
// 定义变量内容
const visible = ref(false);
const loading = ref(false);
const menuDialogFormRef = ref();
const originalName = ref(''); // To store the original menu name for comparison during edits
// 定义需要的数据
const state = reactive({
ruleForm: {
menuId: '',
name: '',
permission: '',
parentId: '',
icon: '',
path: '',
param: '0',
component: '',
sortOrder: 0,
menuType: '1',
keepAlive: '0',
visible: '1',
embedded: '0',
},
parentData: [] as any[], // 上级菜单数据
});
// 表单校验规则
const dataRules = reactive({
menuType: [{required: true, message: '菜单类型不能为空', trigger: 'blur'}],
parentId: [{required: true, message: '上级菜单不能为空', trigger: 'blur'}],
name: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '菜单不能为空',
trigger: 'blur'
}, {
validator: (rule: any, value: any, callback: any) => {
// 如果是按钮类型菜单,跳过名称唯一性校验
if (state.ruleForm.menuType === '1') {
callback();
return;
}
// 如果是编辑状态且菜单名称未改变,跳过校验
if (state.ruleForm.menuId !== '' && value === originalName.value) {
callback();
return;
}
// 其他情况下,验证菜单名称唯一性
validateExist(rule, value, callback, false);
},
trigger: 'blur',
}],
path: [{validator: rule.overLength, trigger: 'blur'}, {required: true, message: '路径不能为空', trigger: 'blur'}],
icon: [{validator: rule.overLength, trigger: 'blur'}, {required: true, message: '图标不能为空', trigger: 'blur'}],
permission: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '权限代码不能为空',
trigger: 'blur'
}, {
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, state.ruleForm.menuId !== '');
},
trigger: 'blur',
}],
sortOrder: [{required: true, message: '排序不能为空', trigger: 'blur'}],
component: [{min: 5, max: 255, message: '组件名称长度必须介于 5 和 255 之间', trigger: 'blur'},
{
validator: (rule: any, value: any, callback: any) => {
if (state.ruleForm.menuType === '0' && state.ruleForm.param === '1' && validateNull(state.ruleForm.component)) {
callback(new Error('请输入组件名称'));
} else {
return callback();
}
},
trigger: 'blur',
}],
});
// 打开弹窗
const openDialog = (type: string, row?: any) => {
state.ruleForm.menuId = '';
visible.value = true;
originalName.value = ''; // Reset the original name
nextTick(() => {
menuDialogFormRef.value?.resetFields();
state.ruleForm.parentId = row?.id || '-1';
});
if (row?.id && type === 'edit') {
state.ruleForm.menuId = row.id;
// 获取当前节点菜单信息
getMenuDetail(row.id);
}
// 渲染上级菜单列表树
getAllMenuData();
};
// 获取菜单节点的详细信息
const getMenuDetail = (id: string) => {
getObj({menuId: id}).then((res) => {
if (res.data[0].component) {
state.ruleForm.param = '1'
}
originalName.value = res.data[0].name; // Store the original name
Object.assign(state.ruleForm, res.data[0]);
});
};
// 从后端获取菜单信息(含层级)
const getAllMenuData = () => {
state.parentData = [];
pageList({
type: '0',
}).then((res) => {
let menu = {
id: '-1',
name: '根菜单',
children: [],
};
menu.children = res.data;
state.parentData.push(menu);
});
};
// 保存数据
const onSubmit = async () => {
// 立即设置 loading防止重复点击
if (loading.value) return;
loading.value = true;
try {
const valid = await menuDialogFormRef.value.validate().catch(() => {
});
if (!valid) {
loading.value = false;
return false;
}
state.ruleForm.menuId ? await putObj(state.ruleForm) : await addObj(state.ruleForm);
useMessage().success(t(state.ruleForm.menuId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量 只有暴漏出来的变量 父组件才能使用
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,37 @@
export default {
sysmenu: {
index: '#',
name: 'menu name',
buttonName: 'button name',
sortOrder: 'sortOrder',
path: 'path',
menuType: 'menuType',
keepAlive: 'keepAlive',
permission: 'permission',
inputNameTip: 'input name',
parentId: 'parent menu',
embedded: 'embedded',
param: 'param',
component: 'component',
visible: 'visible',
icon: 'icon',
inputMenuIdTip: 'input menuId',
inputPermissionTip: 'input permission',
inputPathTip: 'input path',
inputParentIdTip: 'input parentId',
inputIconTip: 'input icon',
inputVisibleTip: 'input visible',
inputSortOrderTip: 'input sortOrder',
inputKeepAliveTip: 'input keepAlive',
inputMenuTypeTip: 'input menuType',
inputCreateByTip: 'input createBy',
inputCreateTimeTip: 'input createTime',
inputUpdateByTip: 'input updateBy',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
inputEmbeddedTip: 'input embedded',
inputComponentTip: 'input component',
deleteDisabledTip: 'menu inclusion subordinates cannot be deleted',
},
};

View File

@@ -0,0 +1,31 @@
export default {
sysmenu: {
index: '#',
name: '菜单名称',
buttonName: '按钮名称',
sortOrder: '排序',
path: '路由',
menuType: '类型',
keepAlive: '缓冲',
permission: '权限标识',
inputNameTip: '请输入菜单名称',
parentId: '上级菜单',
embedded: '内嵌',
param: '带参',
component: '组件',
visible: '显示',
icon: '图标',
inputMenuIdTip: '',
inputPermissionTip: '请输入权限标识',
inputPathTip: '请输入路由路径',
inputParentIdTip: '请选择上级菜单',
inputIconTip: '请选择图标',
inputVisibleTip: '请选择是否显示',
inputSortOrderTip: '请输入排序',
inputKeepAliveTip: '请选择是否缓冲',
inputMenuTypeTip: '请选择菜单类型',
inputEmbeddedTip: '请选择是否内嵌',
inputComponentTip: '请输入组件名称',
deleteDisabledTip: '菜单包含下级不能删除',
},
};

View File

@@ -0,0 +1,197 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('sysmenu.name')" prop="menuName">
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable style="max-width: 180px" v-model="state.queryForm.menuName" />
</el-form-item>
<el-form-item>
<el-button @click="query" class="ml10" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary" v-auth="'sys_menu_add'">
{{ $t('common.addBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
ref="tableRef"
:data="tableList"
lazy
:load="load"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
row-key="path"
style="width: 100%"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle?.headerCellStyle"
>
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.icon')" prop="icon" show-overflow-tooltip>
<template #default="scope">
<SvgIcon :name="scope.row.meta.icon" />
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.path')" prop="path" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.menuType')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.menuType === '0'">左菜单</el-tag>
<el-tag v-if="scope.row.menuType === '2'">顶菜单</el-tag>
<el-tag type="success" v-if="scope.row.menuType === '1'">按钮</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.keepAlive')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.meta.isKeepAlive">开启</el-tag>
<el-tag type="info" v-else>关闭</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.permission')" :show-overflow-tooltip="true" prop="permission"></el-table-column>
<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250">
<template #default="scope">
<el-button icon="folder-add" @click="onOpenAddMenu('add', scope.row)" text type="primary" v-auth="'sys_menu_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary" v-auth="'sys_menu_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-tooltip icon="delete" :content="$t('sysmenu.deleteDisabledTip')" :disabled="!deleteMenuDisabled(scope.row)" placement="top">
<span style="margin-left: 12px">
<el-button
icon="delete"
:disabled="deleteMenuDisabled(scope.row)"
@click="handleDelete(scope.row)"
text
type="primary"
v-auth="'sys_menu_del'"
>
{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<MenuDialog @refresh="query()" ref="menuDialogRef" />
</div>
</template>
<script lang="ts" name="systemMenu" setup>
import { delObj, pageList } from '/@/api/admin/menu';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const MenuDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义变量内容
const tableRef = ref();
const menuDialogRef = ref();
const showSearch = ref(true);
const queryRef = ref();
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: pageList, // H
queryForm: {
parentId: -1,
menuName: '',
},
isPage: false,
});
const { getDataList, tableStyle } = useTable(state);
// 根据类型判断是否有子节点
const setHasChildren = (arr: any[]) => {
arr.forEach((item) => {
// 添加 hasChildren 属性
item.hasChildren = item.menuType !== '1';
});
};
const tableList = computed(() => {
const list = state.dataList;
if (Array.isArray(list)) {
setHasChildren(list);
}
return list;
});
// 打开新增菜单弹窗
const onOpenAddMenu = (type?: string, row?: any) => {
menuDialogRef.value.openDialog(type, row);
};
// 打开编辑菜单弹窗
const onOpenEditMenu = (type: string, row: any) => {
menuDialogRef.value.openDialog(type, row);
};
//是否禁用删除
const deleteMenuDisabled = (row: any) => {
return (row.children || []).length > 0;
};
// 搜索事件
const query = () => {
state.dataList = [];
state.queryForm.parentId = undefined;
getDataList();
};
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
state.dataList = [];
getDataList();
};
const load = (row: any, treeNode: unknown, resolve: (date: any[]) => void) => {
const param = {
parentId: row.id,
};
pageList(param)
.then((res) => {
const childrenList = res.data || [];
if (Array.isArray(childrenList)) {
setHasChildren(childrenList);
}
resolve(childrenList);
})
.catch(() => {
// Handle API error by resolving with empty array
resolve([]);
});
};
// 删除操作
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(row.id);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,135 @@
<template>
<el-dialog :title="form.postId ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible" width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="90px" v-loading="loading">
<el-form-item :label="t('post.postCode')" prop="postCode">
<el-input v-model="form.postCode" :placeholder="t('post.inputpostCodeTip')" />
</el-form-item>
<el-form-item :label="t('post.postName')" prop="postName">
<el-input v-model="form.postName" :placeholder="t('post.inputpostNameTip')" />
</el-form-item>
<el-form-item :label="t('post.postSort')" prop="postSort">
<el-input-number v-model="form.postSort" :placeholder="t('post.inputpostSortTip')" />
</el-form-item>
<el-form-item :label="t('post.remark')" prop="remark">
<el-input type="textarea" maxlength="100" row="3" v-model="form.remark" :placeholder="t('post.inputremarkTip')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="systemPostDialog">
import { useMessage } from '/@/hooks/message';
import { getObj, addObj, putObj, validatePostCode, validatePostName } from '/@/api/admin/post';
import { useI18n } from 'vue-i18n';
import {rule} from "/@/utils/validate";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
postId: '',
postCode: '',
postName: '',
postSort: 0,
remark: '',
delFlag: '',
createTime: '',
createBy: '',
updateTime: '',
updateBy: '',
});
// 定义校验规则
const dataRules = ref({
postCode: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '岗位编码不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePostCode(rule, value, callback, form.postId !== '');
},
trigger: 'blur',
},
],
postName: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '岗位名称不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePostName(rule, value, callback, form.postId !== '');
},
trigger: 'blur',
},
],
postSort: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '岗位排序不能为空', trigger: 'blur' }],
remark: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '岗位描述不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.postId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取Post信息
if (id) {
form.postId = id;
getPostData(id);
}
};
// 提交
const onSubmit = async () => {
// 立即设置 loading防止重复点击
if (loading.value) return;
loading.value = true;
try {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) {
loading.value = false;
return false;
}
form.postId ? await putObj(form) : await addObj(form);
useMessage().success(t(form.postId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表格数据
const getPostData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,28 @@
export default {
post: {
index: '#',
importPostTip: ' import Post',
postId: 'postId',
postCode: 'postCode',
postName: 'postName',
postSort: 'postSort',
remark: 'remark',
delFlag: 'delFlag',
createTime: 'createTime',
createBy: 'createBy',
updateTime: 'updateTime',
updateBy: 'updateBy',
tenantId: 'tenantId',
inputpostIdTip: 'input postId',
inputpostCodeTip: 'input postCode',
inputpostNameTip: 'input postName',
inputpostSortTip: 'input postSort',
inputremarkTip: 'input remark',
inputdelFlagTip: 'input delFlag',
inputcreateTimeTip: 'input createTime',
inputcreateByTip: 'input createBy',
inputupdateTimeTip: 'input updateTime',
inputupdateByTip: 'input updateBy',
inputtenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,28 @@
export default {
post: {
index: '#',
importPostTip: '导入岗位管理',
postId: '岗位ID',
postCode: '岗位编码',
postName: '岗位名称',
postSort: '岗位排序',
remark: '岗位描述',
delFlag: '是否删除 -1已删除 0正常',
createTime: '创建时间',
createBy: '创建人',
updateTime: '更新时间',
updateBy: '更新人',
tenantId: '租户ID',
inputpostIdTip: '请输入岗位ID',
inputpostCodeTip: '请输入岗位编码',
inputpostNameTip: '请输入岗位名称',
inputpostSortTip: '请输入岗位排序',
inputremarkTip: '请输入岗位描述',
inputdelFlagTip: '请输入是否删除 -1已删除 0正常',
inputcreateTimeTip: '请输入创建时间',
inputcreateByTip: '请输入创建人',
inputupdateTimeTip: '请输入更新时间',
inputupdateByTip: '请输入更新人',
inputtenantIdTip: '请输入租户ID',
},
};

View File

@@ -0,0 +1,143 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('post.postName')" prop="postName">
<el-input :placeholder="$t('post.inputpostNameTip')" style="max-width: 180px" v-model="state.queryForm.postName" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'sys_post_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="excelUploadRef.show()" class="ml10" icon="upload-filled" type="primary" v-auth="'sys_post_add'">
{{ $t('common.importBtn') }}
</el-button>
<el-button plain :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'sys_post_del'">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_post_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
v-loading="state.loading"
row-key="postId"
border
:cell-style="tableStyle?.cellStyle"
:header-cell-style="tableStyle?.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('post.index')" type="index" width="60" />
<el-table-column :label="t('post.postCode')" prop="postCode" show-overflow-tooltip />
<el-table-column :label="t('post.postName')" prop="postName" show-overflow-tooltip />
<el-table-column :label="t('post.postSort')" prop="postSort" show-overflow-tooltip />
<el-table-column :label="t('post.remark')" prop="remark" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="200">
<template #default="scope">
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.postId)" text type="primary" v-auth="'sys_post_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.postId])" text type="primary" v-auth="'sys_post_del'"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
<!-- 导入excel -->
<upload-excel
:title="$t('post.importPostTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/post.xlsx"
url="/admin/post/import"
/>
</div>
</template>
<script lang="ts" name="systemPost" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/admin/post';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义变量内容
const formDialogRef = ref();
const excelUploadRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/post/export', Object.assign(state.queryForm,{ids:selectObjs}), 'post.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { postId: string }[]) => {
selectObjs.value = objs.map(({ postId }) => postId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,209 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.roleId ? $t('common.editBtn') : $t('common.addBtn')" width="600" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" label-width="90px" ref="dataFormRef" v-loading="loading">
<el-form-item :label="$t('sysrole.roleName')" prop="roleName">
<el-input :placeholder="$t('sysrole.please_enter_a_role_name')" clearable v-model="form.roleName"></el-input>
</el-form-item>
<el-form-item :label="$t('sysrole.roleCode')" prop="roleCode">
<el-input :placeholder="$t('sysrole.please_enter_the_role_Code')" :disabled="form.roleId !== ''" clearable v-model="form.roleCode"></el-input>
</el-form-item>
<el-form-item :label="$t('sysrole.roleDesc')" prop="roleDesc">
<el-input
maxlength="100"
:rows="3"
:placeholder="$t('sysrole.please_enter_the_role_description')"
type="textarea"
v-model="form.roleDesc"
></el-input>
</el-form-item>
<el-form-item :label="$t('sysrole.menu_authority')" prop="dsType">
<el-select :placeholder="$t('sysrole.please_select')" clearable v-model="form.dsType">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in dictType" />
</el-select>
</el-form-item>
<el-form-item v-if="form.dsType === 1">
<el-tree
:check-strictly="true"
:data="dataForm.deptData"
:default-checked-keys="dataForm.checkedDsScope"
:props="dataForm.deptProps"
default-expand-all
highlight-current
node-key="id"
ref="deptTreeRef"
show-checkbox
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="systemRoleDialog" setup>
import { rule } from '/@/utils/validate';
import { deptTree } from '/@/api/admin/dept';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj, validateRoleCode, validateRoleName } from '/@/api/admin/role';
import { useI18n } from 'vue-i18n';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const deptTreeRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
roleId: '',
roleName: '',
roleCode: '',
roleDesc: '',
dsType: 0,
dsScope: '',
});
// 页面对应元数据
const dataForm = reactive({
deptData: [],
checkedDsScope: [],
deptProps: {
children: 'children',
label: 'name',
value: 'id',
},
});
// 定义校验规则
const dataRules = ref({
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateRoleName(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleCode: [
{ required: true, message: '角色标识不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{ validator: rule.validatorCapital, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateRoleCode(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleDesc: [{ validator: rule.overLength, trigger: 'blur' }],
dsType: [{ required: true, message: '请选择数据权限类型', trigger: 'blur' }],
menu_authority: [{ required: true, message: '数据权限不能为空', trigger: 'blur' }],
});
const dictType = ref([
{
label: '全部',
value: 0,
},
{
label: '自定义',
value: 1,
},
{
label: '本级及子级',
value: 2,
},
{
label: '本级',
value: 3,
},
{
label: '本人',
value: 4,
},
]);
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.roleId = '';
nextTick(() => {
dataFormRef.value.resetFields();
});
// 获取角色信息
if (id) {
form.roleId = id;
getRoleData(id);
}
getDeptData();
};
// 提交
const onSubmit = async () => {
// 立即设置 loading防止重复点击
if (loading.value) return;
loading.value = true;
if (form.dsType === 1) {
form.dsScope = deptTreeRef.value.getCheckedKeys().join(',');
} else {
form.dsScope = '';
}
try {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) {
loading.value = false;
return false;
}
form.roleId ? await putObj(form) : await addObj(form);
useMessage().success(t(form.roleId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化角色数据
const getRoleData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
if (res.data.dsScope) {
dataForm.checkedDsScope = res.data.dsScope.split(',');
} else {
dataForm.checkedDsScope = [];
}
});
};
// 获取菜单结构数据
const getDeptData = () => {
deptTree().then((res: any) => {
dataForm.deptData = res.data;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,23 @@
export default {
sysrole: {
index: '#',
roleName: 'roleName',
inputRoleNameTip: 'input roleName',
permissionTip: 'Grant',
deleteDisabledTip: 'not allowed to delete',
mustCheckOneTip: 'the assign permissions menu must be selected',
roleCode: 'roleCode',
roleDesc: 'role description',
data_authority: 'data authority',
createTime: 'createTime',
please_enter_a_role_name: 'please enter a role name',
please_enter_the_role_Code: 'please enter the role Code',
please_enter_the_role_description: 'please enter the role description',
menu_authority: 'menu authority',
please_select: 'please select',
importRoleTip: 'import role',
assignPermission: 'Assign Permission',
update: 'Update',
cancel: 'Cancel',
},
};

View File

@@ -0,0 +1,23 @@
export default {
sysrole: {
index: '#',
roleName: '角色名称',
inputRoleNameTip: '请输入角色名称',
permissionTip: '授权',
deleteDisabledTip: '角色不允许删除',
mustCheckOneTip: '必须选择【分配权限】菜单',
roleCode: '角色标识',
roleDesc: '角色描述',
data_authority: '数据权限',
createTime: '创建时间',
please_enter_a_role_name: '请输入角色名称',
please_enter_the_role_Code: '请输入角色标识',
please_enter_the_role_description: '请输入角色描述',
menu_authority: '数据权限',
please_select: '请选择',
importRoleTip: '导入角色',
assignPermission: '分配权限',
update: '更新',
cancel: '取 消',
},
};

View File

@@ -0,0 +1,197 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item :label="$t('sysrole.roleName')" prop="roleName">
<el-input :placeholder="$t('sysrole.inputRoleNameTip')" v-model="state.queryForm.roleName" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="roleDialogRef.openDialog()" v-auth="'sys_role_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain icon="upload-filled" type="primary" class="ml10" @click="excelUploadRef.show()" v-auth="'sys_user_add'">
{{ $t('common.importBtn') }}
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" v-auth="'sys_user_del'" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_role_export'"
@exportExcel="exportExcel"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
style="width: 100%"
row-key="roleId"
@selection-change="handleSelectionChange"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="center" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80" />
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip></el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dictType" :value="scope.row.dsType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="250">
<template #default="scope">
<el-button text type="primary" icon="edit-pen" v-auth="'sys_role_edit'" @click="roleDialogRef.openDialog(scope.row.roleId)">{{
$t('common.editBtn')
}}</el-button>
<el-button text type="primary" icon="turn-off" v-auth="'sys_role_perm'" @click="permessionRef.openDialog(scope.row)">{{
$t('sysrole.permissionTip')
}}</el-button>
<el-tooltip :content="$t('sysrole.deleteDisabledTip')" :disabled="scope.row.roleId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
text
type="primary"
icon="delete"
:disabled="scope.row.roleId === '1'"
v-auth="'sys_role_del'"
@click="handleDelete([scope.row.roleId])"
>{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 角色编辑新增 -->
<role-dialog ref="roleDialogRef" @refresh="getDataList()" />
<!-- 导入角色 -->
<upload-excel
ref="excelUploadRef"
:title="$t('sysrole.importRoleTip')"
url="/admin/role/import"
temp-url="/admin/sys-file/local/file/role.xlsx"
@refreshDataList="getDataList"
/>
<!-- 授权 -->
<permession ref="permessionRef" />
</div>
</template>
<script setup lang="ts" name="systemRole">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { pageList, delObj } from '/@/api/admin/role';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const RoleDialog = defineAsyncComponent(() => import('./form.vue'));
const Permession = defineAsyncComponent(() => import('./permession.vue'));
const { t } = useI18n();
// 定义变量内容
const roleDialogRef = ref();
const permessionRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const showSearch = ref(true);
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleName: '',
},
pageList: pageList, // H
descs: ['create_time'],
});
const dictType = ref([
{
label: '全部',
value: '0',
},
{
label: '自定义',
value: '1',
},
{
label: '本级及子级',
value: '2',
},
{
label: '本级',
value: '3',
},
{
label: '本人',
value: '4',
},
]);
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/role/export',Object.assign(state.queryForm,{ids:selectObjs}), 'role.xlsx');
};
// 是否可以多选
const handleSelectable = (row: any) => {
return row.roleId !== '1';
};
// 多选事件
const handleSelectionChange = (objs: { roleId: string }[]) => {
selectObjs.value = objs.map(({ roleId }) => roleId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,142 @@
<template>
<div class="system-role-dialog-container">
<el-dialog width="30%" v-model="state.dialog.isShowDialog" :close-on-click-modal="false" draggable>
<template #header>
<div class="flex justify-between items-center">
<div>{{ state.dialog.title }}</div>
<div class="flex mr-16">
<el-checkbox :label="$t('common.expand')" @change="handleExpand" />
<el-checkbox :label="$t('common.selectAll')" @change="handleSelectAll" />
</div>
</div>
</template>
<el-scrollbar class="h-[400px] sm:h-[600px]">
<el-tree
v-loading="loading"
ref="menuTree"
:data="state.treeData"
:default-checked-keys="state.checkedKeys"
:check-strictly="!checkStrictly"
:props="state.defaultProps"
class="filter-tree"
node-key="id"
highlight-current
show-checkbox
/>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button @click="state.dialog.isShowDialog = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ state.dialog.submitTxt }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="role-permession">
import { fetchRoleTree, permissionUpd } from '/@/api/admin/role';
import { pageList } from '/@/api/admin/menu';
import { useMessage } from '/@/hooks/message';
import { Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import other from '/@/utils/other';
import { CheckboxValueType } from 'element-plus';
const { t } = useI18n();
const menuTree = ref();
const checkStrictly = ref(true);
const loading = ref(false);
const state = reactive({
checkedKeys: [] as any[],
treeData: [] as any[],
defaultProps: {
label: 'name',
value: 'id',
},
roleId: '',
dialog: {
isShowDialog: false,
title: t('sysrole.assignPermission'),
submitTxt: t('common.editBtn'),
},
});
const checkedKeys: Ref<any[]> = ref([]);
// 打开弹窗
const openDialog = (row: any) => {
state.checkedKeys = [];
state.treeData = [];
checkedKeys.value = [];
state.roleId = row.roleId;
loading.value = true;
fetchRoleTree(row.roleId)
.then((res) => {
checkedKeys.value = res.data;
return pageList();
})
.then((r) => {
state.treeData = r.data;
state.checkedKeys = other.resolveAllEunuchNodeId(state.treeData, checkedKeys.value, []);
})
.finally(() => {
loading.value = false;
});
state.dialog.isShowDialog = true;
};
const handleExpand = (check: CheckboxValueType) => {
const treeList = state.treeData;
for (let i = 0; i < treeList.length; i++) {
//@ts-ignore
menuTree.value.store.nodesMap[treeList[i].id].expanded = check;
}
};
const handleSelectAll = (check: CheckboxValueType) => {
if (check) {
menuTree.value?.setCheckedKeys(state.treeData.map((item) => item.id));
} else {
menuTree.value?.setCheckedKeys([]);
}
};
// 提交授权数据
const onSubmit = () => {
// 初始角色选择节点必须包含 【分配权限】 菜单
if (state.roleId === '1') {
if (
!menuTree.value
.getCheckedNodes()
.map((item: { name: string }) => {
return item.name;
})
.includes('分配权限')
) {
useMessage().error(t('sysrole.mustCheckOneTip'));
return;
}
}
const menuIds = menuTree.value.getCheckedKeys().join(',').concat(',').concat(menuTree.value.getHalfCheckedKeys().join(','));
loading.value = true;
permissionUpd(state.roleId, menuIds)
.then(() => {
state.dialog.isShowDialog = false;
useMessage().success(t('common.editSuccessText'));
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,253 @@
<template>
<el-drawer :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible" :close-on-click-modal="true" draggable size="50%">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="90px" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.name')" prop="name">
<el-input v-model="form.name" :placeholder="t('tenant.inputnameTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.code')" prop="code">
<el-input v-model="form.code" :placeholder="t('tenant.inputcodeTip')" :disabled="form.id !== ''" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.startTime')" prop="startTime">
<el-date-picker class="!w-full" v-model="form.startTime" type="date" :placeholder="t('tenant.inputstartTimeTip')" :value-format="dateTimeStr" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.endTime')" prop="endTime">
<el-date-picker class="!w-full" v-model="form.endTime" type="date" :placeholder="t('tenant.inputendTimeTip')" :value-format="dateTimeStr" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :span="12" :label="t('tenant.tenantDomain')" prop="tenantDomain">
<el-input v-model="form.tenantDomain" :placeholder="t('tenant.inputtenantDomainTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.status')" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="item.value" v-for="(item, index) in status_type" border :key="index">{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-divider content-position="left" v-if="form.id !== '1'">
<div>
<span class="mr-4">{{ $t('tenantmenu.name') }}</span>
<el-checkbox :label="$t('common.expand')" @change="handleExpand" />
<el-checkbox :label="$t('common.selectAll')" @change="handleSelectAll" />
</div>
</el-divider>
<el-scrollbar class="h-[400px] sm:h-[600px] ml-12" v-if="form.id !== '1'">
<el-tree
show-checkbox
ref="menuTreeRef"
:disabled="true"
:check-strictly="false"
:data="menuData"
:props="defaultProps"
:default-checked-keys="checkedMenu"
node-key="id"
highlight-current
/>
</el-scrollbar>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts" name="systemTenantDialog">
import { validateTenantCode, validateTenantName } from '/@/api/admin/tenant';
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { getObj, addObj, putObj, treemenu,fetchList } from '/@/api/admin/tenant';
import { useI18n } from 'vue-i18n';
import other from '/@/utils/other';
import { CheckboxValueType } from 'element-plus';
import {rule} from "/@/utils/validate";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const menuTreeRef = ref();
const visible = ref(false);
const loading = ref(false);
// 字典
const { status_type } = useDict('status_type');
// 提交表单数据
const form = reactive({
id: '',
name: '',
code: '',
tenantDomain: '',
startTime: '',
endTime: '',
status: '0',
delFlag: '',
createBy: '',
updateBy: '',
createTime: '',
updateTime: '',
menuId: '',
});
const menuData = ref<any[]>([]);
const defaultProps = reactive({
label: 'name',
value: 'id',
disabled: true,
});
const checkedMenu = ref<any[]>([]);
// 定义校验规则
const dataRules = ref({
name: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '名称不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateTenantName(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
code: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '编码不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateTenantCode(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
status: [{ required: true, message: 'status不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string): void => {
visible.value = true;
form.id = '';
form.menuId = '';
checkedMenu.value = [];
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
if (id) {
form.id = id;
getTenantData(id);
}
getMenuData();
};
// 提交
const onSubmit = async () => {
// 立即设置 loading防止重复点击
if (loading.value) return;
loading.value = true;
try {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) {
loading.value = false;
return false;
}
if (menuTreeRef.value?.getCheckedKeys().length === 0) {
useMessage().error('请选择租户套餐菜单');
loading.value = false;
return false;
}
if (menuTreeRef.value?.getCheckedKeys()) {
let checkMenu = [...menuTreeRef.value.getCheckedKeys(), ...menuTreeRef.value.getHalfCheckedKeys()]
if (!checkMenu.includes('1300')) {
useMessage().error('必须分配角色管理功能');
loading.value = false;
return false;
}
if (!checkMenu.includes('1302')) {
useMessage().error('必须分配角色管理功能');
loading.value = false;
return false;
}
form.menuId = checkMenu.join(',');
}
form.id ? await putObj(form) : await addObj(form);
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
await fetchList()
loading.value = false;
}
};
/**
* 初始化表格数据。
* @param {string} id - 部门 ID。
*/
const getTenantData = async (id) => {
const res = await getObj(id);
Object.assign(form, res.data);
};
/**
* 获取菜单数据
*/
const getMenuData = async () => {
const res = await treemenu();
menuData.value = res.data;
checkedMenu.value = form.menuId ? other.resolveAllEunuchNodeId(menuData.value, form.menuId.split(','), []) : [];
};
const handleExpand = (check: CheckboxValueType) => {
const treeList = menuData.value;
for (let i = 0; i < treeList.length; i++) {
//@ts-ignore
menuTreeRef.value.store.nodesMap[treeList[i].id].expanded = check;
}
};
const handleSelectAll = (check: CheckboxValueType) => {
if (check) {
menuTreeRef.value?.setCheckedKeys(menuData.value.map((item) => item.id));
} else {
menuTreeRef.value?.setCheckedKeys([]);
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,40 @@
export default {
tenant: {
index: '#',
importTenantTip: ' import Tenant',
id: 'id',
name: 'name',
code: 'code',
tenantDomain: 'tenantDomain',
startTime: 'startTime',
endTime: 'endTime',
status: 'status',
delFlag: 'delFlag',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
menuId: 'menuIds',
individuationBtn: 'individuation',
inputidTip: 'input id',
inputnameTip: 'input name',
inputcodeTip: 'input code',
inputtenantDomainTip: 'input tenantDomain',
inputstartTimeTip: 'input startTime',
inputendTimeTip: 'input endTime',
inputstatusTip: 'input status',
inputdelFlagTip: 'input delFlag',
inputcreateByTip: 'input createBy',
inputupdateByTip: 'input updateBy',
inputcreateTimeTip: 'input createTime',
inputupdateTimeTip: 'input updateTime',
inputmenuIdTip: 'input menuId',
deleteDisabledTip: 'base tenants are not allowed to delete',
},
tenantmenu: {
name: 'tenantmenu',
index: '#',
status: 'status',
createTime: 'createTime',
},
};

View File

@@ -0,0 +1,53 @@
export default {
tenant: {
index: '#',
importTenantTip: '导入租户',
id: '租户id',
name: '租户名称',
code: '编码',
tenantDomain: '域名',
startTime: '开始时间',
endTime: '结束时间',
status: '状态',
delFlag: 'delFlag',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建',
updateTime: '更新时间',
menuId: '租户套餐',
individuationBtn: '个性化',
inputidTip: '请输入租户id',
inputnameTip: '请输入名称',
inputcodeTip: '请输入编码',
inputtenantDomainTip: '请输入域名',
inputstartTimeTip: '请输入开始时间',
inputendTimeTip: '请输入结束时间',
inputstatusTip: '请输入status',
inputdelFlagTip: '请输入delFlag',
inputcreateByTip: '请输入创建人',
inputupdateByTip: '请输入修改人',
inputcreateTimeTip: '请输入创建',
inputupdateTimeTip: '请输入更新时间',
inputmenuIdTip: '请选择租户套餐',
deleteDisabledTip: '基础租户不允许删除',
},
tenantmenu: {
name: '套餐',
index: '#',
status: '状态',
createTime: '创建',
},
individuation: {
websiteName: '网站名称',
miniQr: '移动端二维码',
logo: '网站图标',
footerAuthor: '页脚信息',
background: '登录页背景图',
inputIndividuationNameTip: '请输入网站名称',
inputMiniQrTip: '请输入网站图标',
inputLogoTip: '请输入网站Logo',
inputFooterAuthorTip: '请输入页脚信息',
inputBackgroundTip: '请输入登录页背景图',
}
};

View File

@@ -0,0 +1,240 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('tenant.name')" prop="name">
<el-input :placeholder="$t('tenant.inputnameTip')" style="max-width: 180px" v-model="state.queryForm.name" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'sys_systenant_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
{{ $t('common.refreshCacheBtn') }}
</el-button>
<el-button
plain
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
icon="Delete"
type="primary"
v-auth="'sys_systenant_del'"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_systenant_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-scrollbar>
<div class="mx-auto mt-4">
<div class="px-4">
<div class="grid sm:grid-cols-2 sm:gap-x-6 lg:grid-cols-3">
<div class="p-6 mb-6 bg-gray-100 rounded-lg dark:bg-gray-800" v-for="tenant in state.dataList" :key="tenant.id">
<div class="flex items-center justify-between">
<div class="flex items-center">
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ tenant.name }}</h3>
</div>
<div class="flex items-end">
<svg
t="1710908184286"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5178"
class="w-5 h-5 mr-2 text-base text-gray-500 dark:text-gray-400"
>
<path
d="M512 1003.52a497.92 497.92 0 1 1 497.92-497.92A498.56 498.56 0 0 1 512 1003.52zM512 71.68a433.92 433.92 0 1 0 433.92 433.92A434.56 434.56 0 0 0 512 71.68z"
fill="#323333"
p-id="5179"
></path>
<path
d="M152.96 369.92a33.92 33.92 0 0 1 35.2-36.48 39.04 39.04 0 0 1 29.44 16l148.48 198.4V369.92a35.2 35.2 0 1 1 69.76 0V640a30.72 30.72 0 0 1-34.56 33.28 36.48 36.48 0 0 1-29.44-12.8l-147.2-198.4V640a30.72 30.72 0 0 1-34.56 33.28 31.36 31.36 0 0 1-37.12-33.28zM463.36 504.32a162.56 162.56 0 1 1 323.84 0 158.08 158.08 0 0 1-161.92 168.96 159.36 159.36 0 0 1-161.92-168.96z m252.16 0c-3.84-69.12-33.92-104.96-90.24-108.8s-84.48 39.68-88.32 108.8 33.28 104.32 88.32 108.16 86.4-36.48 90.24-108.16zM856.96 603.52A37.12 37.12 0 0 1 896 640c0 21.12-13.44 32-36.48 33.28s-35.84-12.16-37.12-33.28a37.76 37.76 0 0 1 34.56-36.48z"
fill="#323333"
p-id="5180"
></path>
</svg>
<span class="mr-1 dark:text-gray-300">{{ tenant.code }}</span>
</div>
</div>
<p class="my-3 text-sm font-normal text-gray-500 dark:text-gray-400">
状态 {{ status_type.find((item: { value: string }) => item.value === tenant.status)?.label }}
</p>
<p class="my-3 text-sm font-normal text-gray-500 dark:text-gray-400">
有效期 {{ parseDate(tenant.startTime) }} - {{ parseDate(tenant.endTime) }}
</p>
<div class="flex items-center justify-between mt-6 text-sm font-semibold text-gray-900 dark:text-gray-100">
<div class="flex">
<svg
t="1710908265535"
class="w-6 h-5 mr-1 text-yellow-500"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="6175"
width="128"
height="128"
>
<path
d="M512.001274 15.045039a497.880869 497.880869 0 0 1 496.99699 497.086142c0 137.285182-55.886849 261.573274-145.67566 351.652466-89.81301 90.098296-214.335444 145.167493-351.32133 145.167494-137.262257 0-261.529972-55.069197-351.31751-145.173862-89.788811-90.079192-145.67566-214.367284-145.67566-351.646098a497.880869 497.880869 0 0 1 496.99317-497.086142zM489.215293 932.144136V771.518957a405.118201 405.118201 0 0 0-129.123952 26.595319 137.138718 137.138718 0 0 0-17.101903 6.511917c3.53042 7.062113 6.785742 14.098754 10.570881 20.887043 25.514032 45.602527 55.33538 80.88762 88.987717 101.223193a372.744559 372.744559 0 0 0 46.671078 5.40898z m320.120674-717.103117a128.736778 128.736778 0 0 0-10.591259-10.061441 441.222218 441.222218 0 0 1-58.315604 36.934404c27.141693 70.795612 43.939205 156.01602 46.672351 247.447775h144.843999a419.629601 419.629601 0 0 0-122.609487-274.320738z m-46.672352-40.41388a415.300634 415.300634 0 0 0-85.71329-49.415687 434.384259 434.384259 0 0 1 33.623044 51.562976c3.805517 7.062113 7.861934 15.201692 11.942549 23.339997 13.571483-8.138305 27.117495-16.556802 40.137509-25.493654z m-181.233302-77.3572c-15.172399-2.404557-30.921738-4.056417-46.64688-5.133883V253.306336a429.022402 429.022402 0 0 0 129.127773-26.588951c5.985921-2.177856 11.392353-4.356986 17.076431-6.787015-2.954752-7.337211-7.036641-14.373851-10.541588-20.887043-24.989309-46.15527-55.610477-80.888893-89.013189-101.774662z m-92.217567-5.133883c-15.723868 1.077466-31.473207 2.729325-46.671078 5.133883-33.652337 20.887043-63.473685 55.619393-88.987717 101.774662-3.781319 6.510644-7.036641 13.547285-10.816686 20.887042 5.684078 2.430029 11.392353 4.609159 17.351529 6.787016a428.859382 428.859382 0 0 0 129.123952 26.595318V92.130962z m-142.141419 33.075396a414.845959 414.845959 0 0 0-85.732395 49.415687c13.020014 8.940673 26.591498 17.355349 40.412607 25.493654a233.26369 233.26369 0 0 1 11.667451-23.339997c10.041064-19.007209 21.708515-35.811089 33.652337-51.562976z m-122.088585 79.763031c-3.255322 3.004423-7.060839 6.536116-10.316162 10.061441a419.628328 419.628328 0 0 0-122.614581 274.32456h144.874565c2.703853-91.430482 19.529385-176.648342 46.370508-247.447775a478.47757 478.47757 0 0 1-58.31433-36.934405zM92.051999 535.189712a420.779662 420.779662 0 0 0 122.614581 274.319464l10.316162 10.293237a425.318773 425.318773 0 0 1 58.318151-37.413278c-26.841123-71.096182-43.666655-155.742195-46.370508-247.19815H92.051999z m169.286933 314.488813a415.88267 415.88267 0 0 0 85.732395 49.383847 360.53328 360.53328 0 0 1-33.652337-51.562976c-4.075521-7.313012-8.137031-15.176219-11.667451-22.788528a416.635367 416.635367 0 0 0-40.412607 24.962563z m273.441954 82.465611a371.780443 371.780443 0 0 0 46.646879-5.40898c33.402711-20.335574 64.02388-55.620666 89.013189-101.223194 3.504948-6.787015 7.586836-13.823656 10.541589-20.887042a133.885943 133.885943 0 0 0-17.076431-6.511918 405.259571 405.259571 0 0 0-129.120132-26.594045v160.625179z m142.169439-33.081764a416.339892 416.339892 0 0 0 85.71329-49.383847 390.678122 390.678122 0 0 0-40.137509-24.962563c-4.075521 7.612308-8.137031 15.475516-11.942548 22.788528-10.592533 18.706639-21.684316 36.362558-33.623045 51.562976z m121.789288-79.259959l10.591259-10.293237a420.780936 420.780936 0 0 0 122.614582-274.319464H787.101455c-2.729325 91.455954-19.530658 176.101968-46.672351 247.198149a395.620965 395.620965 0 0 1 58.315604 37.414552z m-99.82478-558.105599a203.624467 203.624467 0 0 1-19.004662 7.33721 460.617875 460.617875 0 0 1-145.124191 29.826442v190.500018h206.195866c-2.452954-84.393841-17.375727-162.553409-42.064466-227.66367z m-209.69954 37.163652a456.062207 456.062207 0 0 1-144.873292-29.826442 187.929892 187.929892 0 0 1-19.530658-7.33721c-24.412368 65.110261-39.608965 143.269829-42.064466 227.66367H489.215293V298.860466z m0 426.554165v-190.224919H282.746877c2.454228 84.144215 17.652098 161.977742 42.064466 227.388572 6.510644-2.428756 13.020014-5.158081 19.530658-7.33721 45.019218-17.354076 93.864332-27.396413 144.873292-29.826443z m45.570687 0a465.365857 465.365857 0 0 1 144.874566 29.826443c6.50937 2.17913 12.743643 4.908455 19.254287 7.33721 24.688739-65.410831 39.608965-143.244357 42.064466-227.388572H534.78598v190.224919z"
fill="#3B3F51"
p-id="6176"
></path>
</svg>
<span class="dark:text-gray-300">{{ tenant.tenantDomain }}</span>
</div>
<div class="flex items-center">
<el-button
class="!p-0"
icon="HomeFilled"
@click="individuationRef.openDialog(tenant.id)"
text
type="primary"
v-auth="'sys_systenant_edit'"
>{{ $t('tenant.individuationBtn') }}
</el-button>
<el-button
class="!p-0"
icon="edit-pen"
@click="formDialogRef.openDialog(tenant.id)"
text
type="primary"
v-auth="'sys_systenant_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button
class="!p-0"
icon="delete"
:disabled="tenant.id === '1'"
@click="handleDelete([tenant.id])"
text
type="primary"
v-auth="'sys_systenant_del'"
>{{ $t('common.delBtn') }}
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
<!-- 编辑新增 -->
<individuation ref="individuationRef" />
<!-- 导入excel -->
<upload-excel
:title="$t('tenant.importTenantTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/tenant.xlsx"
url="/admin/tenant/import"
/>
</div>
</template>
<script lang="ts" name="systemTenant" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchPage, fetchList } from '/@/api/admin/tenant';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
import { useDict } from '/@/hooks/dict';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const Individuation = defineAsyncComponent(() => import('./individuation.vue'));
const { t } = useI18n();
// 定义变量内容
const formDialogRef = ref();
const individuationRef = ref();
const excelUploadRef = ref();
const tenantMenuRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
// 字典
const { status_type } = useDict('status_type');
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchPage,
pagination: {
size: 6,
pageSizes: [3, 6, 9, 12],
},
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/tenant/export', Object.assign(state.queryForm, { ids: selectObjs }), 'tenant.xlsx');
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
} finally {
handleRefreshCache();
}
};
//刷新缓存
const handleRefreshCache = () => {
fetchList().then(() => {
useMessage().success('同步成功');
});
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<el-drawer :title="$t('tenant.individuationBtn')" v-model="visible" :close-on-click-modal="false">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" v-loading="loading">
<el-row>
<el-col :span="24" class="mt-4">
<el-form-item :label="t('individuation.websiteName')" prop="websiteName" label-width="120px" align="left">
<el-input v-model="form.websiteName" :placeholder="t('individuation.inputIndividuationNameTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mt-4">
<el-form-item prop="footerAuthor" label-width="120px" align="left">
<template #label>
{{ t('individuation.footerAuthor') }}
<tip content="浏览器底部版权信息、备案信息"/>
</template>
<el-input v-model="form.footer" :placeholder="t('individuation.inputFooterAuthorTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mt-4">
<el-form-item prop="icon" label-width="120px" align="left">
<template #label>
{{ t('individuation.miniQr') }}
<tip content="登录页右下角显示的移动端二维码"/>
</template>
<upload-img v-model:image-url="form.miniQr"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mt-4">
<el-form-item :label="t('individuation.background')" prop="background" label-width="120px" align="left">
<upload-img v-model:image-url="form.background"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts" name="systemTenantDialog">
import {useDict} from '/@/hooks/dict';
import {useMessage} from '/@/hooks/message';
import {getObj, putObj} from '/@/api/admin/tenant';
import {useI18n} from 'vue-i18n';
import UploadImg from "/@/components/Upload/Image.vue";
import {useThemeConfig} from "/@/stores/themeConfig";
import pinia from "/@/stores";
import {storeToRefs} from "pinia";
import Tip from "/@/components/Tip/index.vue";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const {t} = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 字典
const {status_type} = useDict('status_type');
// 导入配置文件
const stores = useThemeConfig(pinia);
const {themeConfig} = storeToRefs(stores);
// 提交表单数据
const form = reactive({
id: '',
websiteName: themeConfig.value.globalTitle,
background: '',
miniQr: '',
footer: themeConfig.value.footerAuthor,
});
// 定义校验规则
const dataRules = ref({
});
// 打开弹窗
const openDialog = (id: string): void => {
visible.value = true;
form.id = ''
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
if (id) {
form.id = id;
getTenantData(id);
}
};
/**
* 初始化表格数据。
* @param {string} id - 部门 ID。
*/
const getTenantData = async (id: any) => {
const res = await getObj(id);
Object.assign(form, res.data);
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
await putObj(form);
useMessage().success(t('common.editSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,305 @@
<template>
<div class="system-user-dialog-container">
<el-dialog :close-on-click-modal="false" :title="dataForm.userId ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="dataForm" :rules="dataRules" label-width="90px" ref="dataFormRef" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.username')" prop="username">
<el-input :disabled="dataForm.userId !== ''" :placeholder="$t('sysuser.inputUsernameTip')" v-model="dataForm.username"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.password')" prop="password">
<el-input clearable :placeholder="$t('sysuser.inputPasswordTip')" type="password" v-model="dataForm.password"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.name')" prop="name">
<el-input clearable :placeholder="$t('sysuser.inputNameTip')" v-model="dataForm.name"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.phone')" prop="phone">
<el-input clearable :placeholder="$t('sysuser.inputPhoneTip')" v-model="dataForm.phone"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.role')" prop="role">
<el-select clearable multiple :placeholder="$t('sysuser.selectRole')" v-model="dataForm.role">
<el-option :key="item.roleId" :label="item.roleName" :value="item.roleId" v-for="item in roleData" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.post')" prop="post">
<el-select clearable multiple :placeholder="$t('sysuser.selectPost')" v-model="dataForm.post">
<el-option :key="item.postId" :label="item.postName" :value="item.postId" v-for="item in postData" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.dept')" prop="deptId">
<el-tree-select
:data="deptData"
:props="{ value: 'id', label: 'name', children: 'children' }"
check-strictly
class="w100"
clearable
:placeholder="$t('sysuser.selectDept')"
v-model="dataForm.deptId"
>
</el-tree-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.email')" prop="email">
<el-input clearable :placeholder="$t('sysuser.inputEmailTip')" v-model="dataForm.email"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.nickname')" prop="nickname">
<el-input clearable :placeholder="$t('sysuser.inputNicknameTip')" v-model="dataForm.nickname"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.lockFlag')" prop="lockFlag">
<el-radio-group v-model="dataForm.lockFlag">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in lock_flag">{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" name="systemUserDialog" setup>
import { addObj, getObj, putObj, validatePhone, validateUsername } from '/@/api/admin/user';
import { list as roleList } from '/@/api/admin/role';
import { list as postList } from '/@/api/admin/post';
import { deptTree } from '/@/api/admin/dept';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
import { useMessage } from '/@/hooks/message';
import { rule } from '/@/utils/validate';
const { t } = useI18n();
// 定义刷新表格emit
const emit = defineEmits(['refresh']);
// @ts-ignore
const { lock_flag } = useDict('lock_flag');
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const deptData = ref<any[]>([]);
const roleData = ref<any[]>([]);
const postData = ref<any[]>([]);
const loading = ref(false);
const props = defineProps({
deptId: {
type: String,
default: '',
},
});
const dataForm = reactive({
userId: '',
username: '',
password: '' as String | undefined,
salt: '',
wxOpenid: '',
qqOpenid: '',
lockFlag: '0',
phone: '' as String | undefined,
deptId: '',
roleList: [],
postList: [],
nickname: '',
name: '',
email: '',
post: [] as string[],
role: [] as string[],
});
const dataRules = ref({
// 用户名校验,不能为空 、长度 5-20、不能和已有数据重复
username: [
{ required: true, message: t('sysuser.usernameRequired'), trigger: 'blur' },
{ min: 5, max: 20, message: t('sysuser.usernameLength'), trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateUsername(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
password: [
{ required: true, message: t('sysuser.passwordRequired'), trigger: 'blur' },
{
min: 6,
max: 20,
message: t('sysuser.passwordLength'),
trigger: 'blur',
},
],
// 姓名校验,不能为空、只能是中文
name: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: t('sysuser.nameRequired'), trigger: 'blur' },
{ validator: rule.chinese, trigger: 'blur' },
],
deptId: [{ required: true, message: t('sysuser.deptRequired'), trigger: 'blur' }],
role: [{ required: true, message: t('sysuser.roleRequired'), trigger: 'blur' }],
post: [{ required: true, message: t('sysuser.postRequired'), trigger: 'blur' }],
// 手机号校验,不能为空、新增的时不能重复校验
phone: [
{ required: true, message: t('sysuser.phoneRequired'), trigger: 'blur' },
{ validator: rule.validatePhone, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePhone(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
email: [
{ type: 'email', message: t('sysuser.emailFormat'), trigger: ['blur', 'change'] },
{ validator: rule.overLength, trigger: 'blur' },
],
lockFlag: [{ required: true, message: t('sysuser.statusRequired'), trigger: 'blur' }],
});
// 打开弹窗
const openDialog = async (id: string) => {
visible.value = true;
dataForm.userId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 修改获取用户信息
if (id) {
dataForm.userId = id;
await getUserData(id);
dataForm.password = '******';
}
// 加载使用的数据
getDeptData();
getPostData();
getRoleData();
};
// 提交
const onSubmit = async () => {
// 立即设置 loading防止重复点击
if (loading.value) return;
loading.value = true;
try {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) {
loading.value = false;
return false;
}
const { userId, phone, password } = dataForm;
if (userId) {
// 清除占位符,避免提交错误的数据
if (phone?.includes('*')) dataForm.phone = undefined;
if (password?.includes('******')) dataForm.password = undefined;
await putObj(dataForm);
useMessage().success(t('common.editSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
} else {
await addObj(dataForm);
useMessage().success(t('common.addSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
}
} catch (error: any) {
useMessage().error(error.msg);
} finally {
loading.value = false;
}
};
/**
* 从服务器获取用户数据
*
* @async
* @param {string} id - 用户 ID
* @return {Promise} - 包含用户数据的 Promise 对象
*/
const getUserData = async (id: string) => {
try {
loading.value = true;
const { data } = await getObj(id);
Object.assign(dataForm, data);
if (data.roleList) {
dataForm.role = data.roleList.map((item: { roleId: string }) => item.roleId);
}
if (data.postList) {
dataForm.post = data.postList.map((item: { postId: string }) => item.postId);
}
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化部门数据
const getDeptData = () => {
// 获取部门数据
deptTree().then((res) => {
deptData.value = res.data;
// 默认选择在树中选中的部门
if (!dataForm.userId) {
dataForm.deptId = props.deptId;
}
});
};
// 岗位数据
const getPostData = () => {
postList().then((res) => {
postData.value = res.data;
// 默认选择第一个
if (!dataForm.userId) {
dataForm.post = [res.data[0].postId];
}
});
};
// 角色数据
const getRoleData = () => {
roleList().then((res) => {
roleData.value = res.data;
// 默认选择第一个
if (!dataForm.userId) {
dataForm.role = [res.data[0].roleId];
}
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,45 @@
export default {
sysuser: {
index: '#',
username: 'username',
name: 'name',
phone: 'phone',
post: 'post',
role: 'role',
lockFlag: 'lockFlag',
createTime: 'createTime',
password: 'password',
dept: 'dept',
email: 'email',
nickname: 'nickname',
inputUsernameTip: 'input username',
inputPhoneTip: 'input phone',
inputPasswordTip: 'input password',
inputNameTip: 'input name',
inputEmailTip: 'input email',
inputNicknameTip: 'input nickname',
importUserTip: 'user import',
deleteDisabledTip: 'admin are not allowed to delete',
noDataScopeTip: 'no data permissions',
passwordBtn: 'password',
passwordLength: 'Password length must be between 5 and 20 characters',
nameRequired: 'Name cannot be empty',
deptRequired: 'Department cannot be empty',
roleRequired: 'Role cannot be empty',
postRequired: 'Post cannot be empty',
phoneRequired: 'Phone cannot be empty',
emailFormat: 'Please enter a valid email address',
statusRequired: 'Status cannot be empty',
usernameRequired: 'Username cannot be empty',
usernameLength: 'Username length must be between 5 and 20 characters',
passwordRequired: 'Password cannot be empty',
selectDept: 'Please select a department',
selectRole: 'Please select roles',
selectPost: 'Please select posts',
},
personal: {
name: 'personal info',
passwordRule: 'The two passwords are inconsistent',
passwordScore: 'Password level is too low',
},
};

View File

@@ -0,0 +1,45 @@
export default {
sysuser: {
index: '#',
username: '用户名',
name: '姓名',
phone: '手机号',
post: '岗位',
role: '角色',
lockFlag: '启用',
createTime: '创建时间',
password: '密码',
dept: '部门',
email: '邮箱',
nickname: '昵称',
inputUsernameTip: '请输入用户名',
inputPhoneTip: '请输入手机号',
inputNameTip: '请输入姓名',
inputPasswordTip: '请输入密码',
inputEmailTip: '请输入邮箱',
inputNicknameTip: '请输入昵称',
importUserTip: '用户导入',
deleteDisabledTip: 'admin 不允许被删除',
noDataScopeTip: '没有数据权限',
passwordBtn: '密码',
passwordLength: '用户密码长度必须介于 5 和 20 之间',
nameRequired: '姓名不能为空',
deptRequired: '部门不能为空',
roleRequired: '角色不能为空',
postRequired: '岗位不能为空',
phoneRequired: '手机号不能为空',
emailFormat: '请输入正确的邮箱地址',
statusRequired: '状态不能为空',
usernameRequired: '用户名不能为空',
usernameLength: '用户名称长度必须介于 5 和 20 之间',
passwordRequired: '密码不能为空',
selectDept: '请选择所属部门',
selectRole: '请选择角色',
selectPost: '请选择岗位',
},
personal: {
name: '个人信息',
passwordRule: '两次输入密码不一致',
passwordScore: '密码等级太低',
},
};

View File

@@ -0,0 +1,263 @@
<template>
<div class="layout-padding">
<splitpanes>
<pane size="15">
<div class="layout-padding-auto layout-padding-view">
<el-scrollbar>
<query-tree :placeholder="$t('common.queryDeptTip')" :query="deptData.queryList" :show-expand="true" @node-click="handleNodeClick">
<!-- 没有数据权限提示 -->
<template #default="{ node, data }">
<el-tooltip v-if="data.isLock" class="item" effect="dark" :content="$t('sysuser.noDataScopeTip')" placement="right-start">
<span
>{{ node.label }}
<SvgIcon name="ele-Lock" />
</span>
</el-tooltip>
<span v-if="!data.isLock">{{ node.label }}</span>
</template>
</query-tree>
</el-scrollbar>
</div>
</pane>
<pane class="ml8">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('sysuser.username')" prop="username">
<el-input v-model="state.queryForm.username" :placeholder="$t('sysuser.inputUsernameTip')" clearable />
</el-form-item>
<el-form-item :label="$t('sysuser.phone')" prop="phone">
<el-input v-model="state.queryForm.phone" :placeholder="$t('sysuser.inputPhoneTip')" clearable />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">{{ $t('common.queryBtn') }}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'sys_user_add'" icon="folder-add" type="primary" @click="userDialogRef.openDialog()">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain v-auth="'sys_user_add'" class="ml10" icon="upload-filled" type="primary" @click="excelUploadRef.show()">
{{ $t('common.importBtn') }}
</el-button>
<el-button
plain
v-auth="'sys_user_del'"
:disabled="multiple"
class="ml10"
icon="Delete"
type="primary"
@click="handleDelete(selectObjs)"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_user_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10 mr20"
style="float: right"
/>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
@selection-change="handleSelectionChange"
row-key="userId"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column :selectable="handleSelectable" type="selection" width="40" />
<el-table-column :label="$t('sysuser.index')" type="index" width="60" fixed="left" />
<el-table-column :label="$t('sysuser.username')" prop="username" fixed="left" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysuser.name')" prop="name" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysuser.phone')" prop="phone" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysuser.post')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-for="(item, index) in scope.row.postList" :key="index">{{ item.postName }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysuser.role')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-for="(item, index) in scope.row.roleList" :key="index">{{ item.roleName }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysuser.lockFlag')" show-overflow-tooltip>
<template #default="scope">
<el-switch v-model="scope.row.lockFlag" @change="changeSwitch(scope.row)" active-value="0" inactive-value="9"></el-switch>
</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="200" fixed="right">
<template #default="scope">
<div style="display: flex">
<!-- 重置密码 -->
<popover-input v-model="inputPassword" @confirm="changePassword(scope.row)">
<template #default>
<el-button v-auth="'sys_user_edit'" icon="RefreshLeft" text type="primary" class="mr-4">
{{ $t('sysuser.passwordBtn') }}
</el-button>
</template>
</popover-input>
<!-- 修改信息 -->
<el-button v-auth="'sys_user_edit'" icon="edit-pen" text type="primary" @click="userDialogRef.openDialog(scope.row.userId)">
{{ $t('common.editBtn') }}
</el-button>
<!-- 删除用户 -->
<el-tooltip :content="$t('sysuser.deleteDisabledTip')" :disabled="scope.row.userId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
icon="delete"
v-auth="'sys_user_del'"
:disabled="scope.row.username === 'admin'"
text
type="primary"
@click="handleDelete([scope.row.userId])"
>{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle"> </pagination>
</div>
</pane>
</splitpanes>
<user-form ref="userDialogRef" @refresh="getDataList(false)" :deptId="state.queryForm.deptId" />
<upload-excel
ref="excelUploadRef"
:title="$t('sysuser.importUserTip')"
temp-url="/admin/sys-file/local/file/user.xlsx"
url="/admin/user/import"
@refreshDataList="getDataList"
/>
</div>
</template>
<script lang="ts" name="systemUser" setup>
import { delObj, pageList, putObj } from '/@/api/admin/user';
import { deptTree } from '/@/api/admin/dept';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 动态引入组件
const UserForm = defineAsyncComponent(() => import('./form.vue'));
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'));
const PopoverInput = defineAsyncComponent(() => import('/@/components/PopoverInput/index.vue'));
const { t } = useI18n();
// 定义变量内容
const userDialogRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const showSearch = ref(true);
const inputPassword = ref();
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// 定义表格查询、后台调用的API
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
deptId: '',
username: '',
phone: '',
},
pageList: pageList,
});
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 部门树使用的数据
const deptData = reactive({
queryList: (name: String) => {
return deptTree({
deptName: name,
});
},
});
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields();
state.queryForm.deptId = '';
getDataList();
};
// 点击树
const handleNodeClick = (e: any) => {
state.queryForm.deptId = e.id;
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/user/export', Object.assign(state.queryForm, { ids: selectObjs }), 'users.xlsx');
};
// 是否可以多选
const handleSelectable = (row: any) => {
return row.username !== 'admin';
};
// 多选事件
const handleSelectionChange = (objs: { userId: string }[]) => {
selectObjs.value = objs.map(({ userId }) => userId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
//表格内开关 (用户状态)
const changeSwitch = async (row: any) => {
// 不修改密码
row.password = undefined;
row.phone = undefined;
await putObj(row);
useMessage().success(t('common.optSuccessText'));
getDataList();
};
//修改用户密码
const changePassword = async (row: any) => {
if (!inputPassword.value || inputPassword.value.length < 6 || inputPassword.value.length > 20) {
useMessage().error(t('sysuser.inputPasswordTip'));
return;
}
row.phone = undefined;
row.password = inputPassword.value;
await putObj(row);
useMessage().success(t('common.optSuccessText'));
getDataList();
};
</script>

View File

@@ -0,0 +1,565 @@
<template>
<el-drawer v-model="visible" :title="$t('personal.name')" size="40%">
<el-tabs style="height: 200px" class="demo-tabs">
<el-tab-pane label="基本信息" v-loading="loading">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
基本信息
</template>
<el-form :model="formData" :rules="ruleForm" label-width="100px" class="mt30" ref="formdataRef">
<el-row :gutter="20">
<el-col :span="24" class="mb20">
<el-form-item prop="avatar">
<ImageUpload v-model:imageUrl="formData.avatar" borderRadius="50%">
<template #empty>
<el-icon><Avatar /></el-icon>
<span>请上传头像</span>
</template>
</ImageUpload>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" clearable disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="手机" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="昵称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入昵称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item>
<el-button type="primary" @click="handleSaveUser"> 更新个人信息 </el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="安全信息">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
</svg>
安全信息
</template>
<el-form :model="passwordFormData" :rules="passwordRuleForm" label-width="100px" class="mt30" ref="passwordFormdataRef">
<el-row :gutter="20">
<el-col :span="24" class="mb20">
<el-form-item label="原密码" prop="password">
<el-input v-model="passwordFormData.password" :type="showPassword ? 'text' : 'password'" placeholder="请输入密码" clearable type="password">
<template #suffix>
<i
class="iconfont el-input__icon login-content-password"
:class="showPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
@click="showPassword = !showPassword"
>
</i>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="新密码" prop="newpassword1">
<strength-meter
v-model="passwordFormData.newpassword1"
:minlength="6"
:maxlength="16"
placeholder="请输入新密码"
@score="passwordScore"
></strength-meter>
<!-- <el-input v-model="passwordFormData.newpassword1" clearable type="password"></el-input>-->
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="确认密码" prop="newpassword2">
<strength-meter v-model="passwordFormData.newpassword2" :minlength="6" :maxlength="16" placeholder="请重复密码"></strength-meter>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item>
<el-button type="primary" @click="handleChangePassword"> 修改密码 </el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="第三方账号">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.6 9.75m6.633-4.596a18.666 18.666 0 0 1-2.485 5.33" />
</svg>
社交登录
</template>
<el-table :data="socialList" class="mt10">
<el-table-column type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="name" label="平台"></el-table-column>
<el-table-column label="状态">
<template #default="scope">
<el-tag v-if="scope.row.openId"> 已绑定 </el-tag>
<el-tag v-else> 未绑定 </el-tag>
</template>
</el-table-column>
<el-table-column prop="action" label="操作">
<template #default="scope">
<el-button @click="unbinding(scope.row.type)" text type="primary" v-if="scope.row.openId"> 解绑 </el-button>
<el-button @click="handleClick(scope.row.type)" text type="primary" v-else> 绑定 </el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-drawer>
</template>
<script setup lang="ts" name="personal">
import {useUserInfo} from '/@/stores/userInfo';
import {editInfo, getObj, password, unbindingUser} from '/@/api/admin/user';
import {useMessage} from '/@/hooks/message';
import {rule, validateNull} from '/@/utils/validate';
import other from '/@/utils/other';
import {Session} from '/@/utils/storage';
import {useI18n} from 'vue-i18n';
import {getLoginAppList} from "/@/api/admin/social";
import { SocialLoginEnum } from '/@/api/login';
const { t } = useI18n();
const ImageUpload = defineAsyncComponent(() => import('/@/components/Upload/Image.vue'));
const StrengthMeter = defineAsyncComponent(() => import('/@/components/StrengthMeter/index.vue'));
const visible = ref(false);
// 定义变量内容
const formData = ref({
userId: '',
username: '',
name: '',
email: '',
avatar: '',
nickname: '',
wxDingUserid: '',
wxCpUserid: '',
phone: ('' as string) || undefined,
});
const showPassword = ref(false);
const passwordFormData = reactive({
password: '',
newpassword1: '',
newpassword2: '',
});
const formdataRef = ref();
const passwordFormdataRef = ref();
const ruleForm = reactive({
phone: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: rule.validatePhone, trigger: 'blur' },
],
nickname: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '昵称不能为空', trigger: 'blur' }],
email: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '邮箱不能为空', trigger: 'blur' }],
name: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '姓名不能为空', trigger: 'blur' }],
});
const validatorPassword2 = (rule: any, value: any, callback: any) => {
if (value !== passwordFormData.newpassword1) {
callback(new Error(t('personal.passwordRule')));
} else {
callback();
}
};
const validatorScore = (rule: any, value: any, callback: any) => {
if (score.value <= 1) {
callback(new Error(t('personal.passwordScore')));
} else {
callback();
}
};
const passwordRuleForm = reactive({
password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
newpassword1: [
{
min: 6,
max: 20,
message: '用户密码长度必须介于 6 和 20 之间',
trigger: 'blur',
},
{ validator: validatorScore, trigger: 'blur' },
],
newpassword2: [
{
min: 6,
max: 20,
message: '用户密码长度必须介于 6 和 20 之间',
trigger: 'blur',
},
{ validator: validatorPassword2, trigger: 'blur' },
],
});
const score = ref(0);
const passwordScore = (e: any) => {
score.value = e;
};
const handleChangePassword = () => {
passwordFormdataRef.value.validate((valid: boolean) => {
if (!valid) {
return false;
}
password(passwordFormData)
.then(() => {
useMessage().success('修改成功');
// 需要重新登录
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
})
.catch((err) => {
useMessage().error(err.msg);
});
});
};
// 保存用户
const handleSaveUser = () => {
formdataRef.value.validate((valid: boolean) => {
if (!valid) {
return false;
}
if (formData.value.phone && formData.value.phone.includes('*')) {
formData.value.phone = undefined;
}
editInfo(formData.value)
.then(() => {
useMessage().success('修改成功');
// 更新上下文的 user信息
useUserInfo().setUserInfos();
})
.catch((err) => {
useMessage().error(err.msg);
});
});
};
const socialList = ref([] as any);
const initSocialList = () => {
socialList.value = [
{
name: '企业微信',
type: SocialLoginEnum.WEIXIN_CP,
openId: formData.value.wxCpUserid,
},
{
name: '钉钉办公',
type: SocialLoginEnum.DINGTALK,
openId: formData.value.wxDingUserid,
},
];
};
const handleClick = async (thirdpart: SocialLoginEnum) => {
// 获取租户配置的账号信息
const { data } = await getLoginAppList();
const result = data.find((item: any) => item.type === thirdpart);
if (validateNull(result)) {
useMessage().error(t('scan.appErrorTip'));
return;
}
let redirect_uri, url;
redirect_uri = encodeURIComponent(window.location.origin + '/#/authredirect');
if (thirdpart === SocialLoginEnum.WEIXIN_CP) {
url = `https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=${result.appId}&agentid=${result.ext}&redirect_uri=${redirect_uri}&state=${SocialLoginEnum.WEIXIN_CP}-BIND`;
}
if (thirdpart === SocialLoginEnum.DINGTALK) {
url = `https://login.dingtalk.com/oauth2/auth?redirect_uri=${redirect_uri}&response_type=code&client_id=${result.appId}&scope=openid&state=${SocialLoginEnum.DINGTALK}-BIND&prompt=consent`;
}
if (url) {
other.openWindow(url, thirdpart, 540, 540);
}
};
const unbinding = (type: SocialLoginEnum) => {
unbindingUser(type)
.then(() => {
useMessage().success('解绑成功');
})
.catch((err) => {
useMessage().error(err.msg);
})
.finally(() => {
initUserInfo(formData.value.userId);
});
};
const open = () => {
visible.value = true;
const data = useUserInfo().userInfos;
initUserInfo(data.user.userId);
// Object.assign(formData, data.user);
};
const loading = ref(false);
const initUserInfo = (userId: any) => {
loading.value = true;
getObj(userId)
.then((res) => {
formData.value = res.data;
initSocialList();
})
.catch((err) => {
useMessage().error(err.msg);
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
open,
});
</script>
<style scoped lang="scss">
@import '/@/theme/mixins/index.scss';
.personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 180px;
height: 130px;
border-radius: 3px;
:deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: var(--el-text-color-secondary);
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: var(--el-text-color-secondary);
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
.personal-recommend-row {
.personal-recommend-col {
.personal-recommend {
position: relative;
height: 100px;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
&:hover {
i {
right: 0px !important;
bottom: 0px !important;
transition: all ease 0.3s;
}
}
i {
position: absolute;
right: -10px;
bottom: -10px;
font-size: 70px;
transform: rotate(-30deg);
transition: all ease 0.3s;
}
.personal-recommend-auto {
padding: 15px;
position: absolute;
left: 0;
top: 5%;
color: var(--next-color-white);
.personal-recommend-msg {
font-size: 12px;
margin-top: 10px;
}
}
}
}
}
.personal-edit {
.personal-edit-title {
position: relative;
padding-left: 10px;
color: var(--el-text-color-regular);
&::after {
content: '';
width: 2px;
height: 10px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background: var(--el-color-primary);
}
}
.personal-edit-safe-box {
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
padding: 15px 0;
.personal-edit-safe-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.personal-edit-safe-item-left {
flex: 1;
overflow: hidden;
.personal-edit-safe-item-left-label {
color: var(--el-text-color-regular);
margin-bottom: 5px;
}
.personal-edit-safe-item-left-value {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
margin-right: 15px;
}
}
}
&:last-of-type {
padding-bottom: 0;
border-bottom: none;
}
}
}
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 100%;
}
.item {
display: flex;
flex-direction: column;
justify-content: center;
}
</style>