src/api/authority.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/api/interceptor.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/assets/style/global.less | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/locale/en-US.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/locale/zh-CN.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/router/routes/modules/authority.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/authority/components/authheader.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/authority/organization/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/authority/organization/locale/en-US.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/authority/organization/locale/zh-CN.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/authority/users/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/authority/users/locale/en-US.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/views/authority/users/locale/zh-CN.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/authority.ts
New file @@ -0,0 +1,80 @@ import axios from "axios"; import { Pagination } from "@/types/global"; export interface User { userId: string; userName: string; nickName: string; email: string; phoneNumber: string; dept: any; createTime: string; status: string; } export interface Organization { deptId: string; deptName: string; email: string; leader: string; phone: string; orderNum: string; parentId: string; status: string; address: string; parentName: string; } export interface Result<T> { code: number; msg: string; rows: T; total: number; } export function UserList(params: Pagination) { return axios.get<Result<User[]>>("/base/system/user/list", { params }); } export function Userstatus(userID, status) { return axios.put("/base/system/user/changeStatus", { "userId": userID, "status": status }); } export function UserChangePwd(userId) { return axios.put("/base/system/user/profile/updatePwd", { "userId": userId }); } export function UserEdit(user) { return axios.put("/base/system/user", { user }); } export function UserAdd(user) { return axios.post("/base/system/user", { user }); } export function UserDelete(userId) { return axios.delete("/base/system/user/" + userId); } export function OrganizationList(key: string) { return axios.post<Result<Organization[]>>("/base/system/dept/list", { "deptName": key }); } export function OrganizationAdd(organization) { return axios.post("/base/system/dept", { organization }); } export function OrganizationDelete(id) { return axios.delete("/base/system/dept/" + id); } export function OrganizationUpdate(organization) { return axios.put("/base/system/dept", { organization }); } export function OrganizationById(id) { return axios.get<Result<Organization>>("/base/system/dept/" + id); } src/api/interceptor.ts
@@ -2,7 +2,7 @@ import type { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Message, Modal } from '@arco-design/web-vue'; import { useUserStore } from '@/store'; import { getAuthorization, getToken, setAuthorization } from "@/utils/auth"; import { getAuthorization, getToken, setAuthorization, setToken } from "@/utils/auth"; export interface HttpResponse<T = unknown> { status: number; @@ -14,7 +14,7 @@ if (import.meta.env.VITE_API_BASE_URL) { axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL; } axios.defaults.withCredentials = true axios.interceptors.request.use( (config: AxiosRequestConfig) => { // let each request carry token @@ -35,11 +35,8 @@ if (!config.headers) { config.headers = {}; } config.headers.Authorization = authorization; config.headers.token = getToken(); config.headers.Authorization = `${authorization}`; } config.headers.Authorization = 'Ijg3NmFjZjJlNGIzMzExZWY4NzI0MDI0MmFjMTIwMDA2Ig.ZqNvxw.Kgp8PcT4n2cxpzxFrYHttO4i2Eo'; return config; }, (error) => { @@ -79,7 +76,7 @@ return Promise.reject(new Error(res.msg || 'Error')); } if(response.config.url === '/v1/user/login') { if(response.config.url === '/v1/user/login'|| response.config.url === '/base/login') { setAuthorization(response.headers.authorization); } return res; src/assets/style/global.less
@@ -92,3 +92,7 @@ } } } .container { padding: 0 20px 20px 20px; } src/locale/en-US.ts
@@ -3,6 +3,9 @@ import localeWorkplace from '@/views/dashboard/workplace/locale/en-US'; import localeUser from '@/views/authority/users/locale/en-US'; import localeOrganization from '@/views/authority/organization/locale/en-US'; import localeMonitor from '@/views/dashboard/monitor/locale/en-US'; import localeSearchTable from '@/views/list/search-table/locale/en-US'; @@ -51,6 +54,9 @@ ...localeLogin, ...localeWorkplace, ...localeUser, ...localeOrganization, ...localeMonitor, ...localeSearchTable, ...localeCardList, src/locale/zh-CN.ts
@@ -3,6 +3,9 @@ import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN'; import localeUser from '@/views/authority/users/locale/zh-CN'; import localeOrganization from '@/views/authority/organization/locale/zh-CN'; import localeMonitor from '@/views/dashboard/monitor/locale/zh-CN'; import localeSearchTable from '@/views/list/search-table/locale/zh-CN'; @@ -54,6 +57,9 @@ ...localeLogin, ...localeWorkplace, ...localeUser, ...localeOrganization, ...localeMonitor, ...localeSearchTable, ...localeCardList, src/router/routes/modules/authority.ts
New file @@ -0,0 +1,38 @@ import { DEFAULT_LAYOUT } from '../base'; import { AppRouteRecordRaw } from '../types'; const AUTHORITY: AppRouteRecordRaw = { path: '/authority', name: 'authority', component: DEFAULT_LAYOUT, meta: { locale: 'menu.authority', requiresAuth: true, icon: 'icon-lock', order: 1, }, children: [ { path: 'users', name: 'users', component: () => import('@/views/authority/users/index.vue'), meta: { locale: 'menu.user.title', requiresAuth: true, roles: ['*'], }, }, { path: 'organization', name: 'organization', component: () => import('@/views/authority/organization/index.vue'), meta: { locale: 'menu.organization.title', requiresAuth: true, roles: ['*'], }, }, ], }; export default AUTHORITY; src/views/authority/components/authheader.vue
New file @@ -0,0 +1,15 @@ <template> <Breadcrumb :items="items" /> </template> <script lang="ts" setup> import { PropType } from 'vue'; defineProps({ items: { type: Array as PropType<string[]>, default() { return []; }, }, }); </script> src/views/authority/organization/index.vue
New file @@ -0,0 +1,222 @@ <template> <div class="container"> <authheader :items="menuTips"></authheader> <a-row :gutter="20"> <a-col :span="8"> <a-card :title="$t('menu.organization.title')" :bordered="false" :style="{ width: '100%',height: '900px', 'overflow-y': 'auto' }"> <a-tree class="tree-demo" draggable blockNode :data="treeData" :show-line="showLine" :fieldNames="{ key:'deptId', title:'deptName', children:'children', }" @drop="onDrop" @select="showDetail" > <template #extra="nodeData"> <IconPlus style="position: absolute; right: 60px; font-size: 12px; top: 10px; color: #3370ff;" @click="() => onIconClick(nodeData)" /> <IconDelete style="position: absolute; right: 40px; font-size: 12px; top: 10px; color: #3370ff;" @click="() => onIconClickDelete(nodeData)" /> </template> </a-tree> </a-card> </a-col> <a-col :span="16"> <a-card :title="$t('menu.organization.detail')" :bordered="false" :style="{ width: '100%' }"> <a-form :model="deptform" layout="horizontal"> <a-form-item field="parentName" label="上级机构"> <a-input v-model="deptform.parentId" /> </a-form-item> <a-form-item field="status" label="机构状态"> <a-switch checked-value="0" unchecked-value="1" v-model="deptform.status"></a-switch> </a-form-item> <a-form-item field="deptName" label="机构名称"> <a-input v-model="deptform.deptName" /> </a-form-item> <a-form-item field="leader" label="联系人"> <a-input v-model="deptform.leader" /> </a-form-item> <a-form-item field="phone" label="联系电话"> <a-input v-model="deptform.phone" /> </a-form-item> <a-form-item field="address" label="机构地址" style="align: start"> <a-input v-model="deptform.address" /> </a-form-item> <a-form-item> <a-space> <a-button @click="editdept">保存</a-button> <a-button @click="reset(deptform.deptId)">重置</a-button> </a-space> </a-form-item> </a-form> </a-card> </a-col> </a-row> <a-modal width="50%" v-model:visible="visible" title="新增" @cancel="handleCancel" @ok="addDept"> <a-form :model="deptform" layout="horizontal"> <a-form-item field="parentName" label="上级机构"> <a-input v-model="deptform.parentName" /> </a-form-item> <a-form-item field="status" label="机构状态"> <a-switch checked-value="0" unchecked-value="1" v-model="deptform.status"></a-switch> </a-form-item> <a-form-item field="deptName" label="机构名称"> <a-input v-model="deptform.deptName" /> </a-form-item> <a-form-item field="leader" label="联系人"> <a-input v-model="deptform.leader" /> </a-form-item> <a-form-item field="phone" label="联系电话"> <a-input v-model="deptform.phone" /> </a-form-item> <a-form-item field="address" label="机构地址" style="align: start"> <a-input v-model="deptform.address" /> </a-form-item> </a-form> </a-modal> </div> </template> <script lang="ts" setup> import { ref } from "vue"; import { IconPlus } from "@arco-design/web-vue/es/icon"; import { Organization, OrganizationAdd, OrganizationById, OrganizationDelete, OrganizationList, OrganizationUpdate } from "@/api/authority"; import Authheader from "@/views/authority/components/authheader.vue"; import { Modal } from "@arco-design/web-vue"; let visible = ref(false); let treeData = ref([]); let showLine = ref(true); let menuTips = ref(["权限管理", "机构"]); let deptform = ref<Organization>({ deptName: "", email: "", leader: "", orderNum: "0", parentId: "", parentName: "", phone: "", status: "", address: "", deptId: "" }); const onIconClick = (nodeData) => { deptform.value.parentName = nodeData.deptName; deptform.value.parentId = nodeData.deptId; visible.value = true; }; const addDept = async () => { await OrganizationAdd({ ...deptform.value } as unknown as Organization).then((res) => { OrganizationData(""); }); }; const onIconClickDelete = (nodeData) => { OrganizationDelete(nodeData.deptId).then(() => { OrganizationData(""); }); }; const showDetail = (id) => { OrganizationById(id).then((res) => { deptform.value = {...res.data}; }); }; const editdept=()=>{ OrganizationUpdate({ ...deptform.value } as unknown as Organization).then((res) => { OrganizationData(""); Modal.success({ title: "保存成功", content: "保存成功" }); }); } const reset=(id)=>{ OrganizationById(id).then((res) => { deptform.value = {...res.data}; }); } const onDrop = ({ dragNode, dropNode, dropPosition }) => { const data = treeData.value; OrganizationUpdate({ orderNum: "0", parentId: dropNode.deptId, deptId: dragNode.deptId }); const loop = (data, key, callback) => { data.some((item, index, arr) => { if (item.deptId === key) { callback(item, index, arr); return true; } if (item.children) { return loop(item.children, key, callback); } return false; }); }; loop(data, dragNode.deptId, (_, index, arr) => { arr.splice(index, 1); }); if (dropPosition === 0) { loop(data, dropNode.deptId, (item) => { item.children = item.children || []; item.children.push(dragNode); }); } else { loop(data, dropNode.deptId, (_, index, arr) => { arr.splice(dropPosition < 0 ? index : index + 1, 0, dragNode); }); } }; const OrganizationData = async (key) => { await OrganizationList(key).then((res) => { treeData.value = [...res.rows]; }); }; OrganizationData(""); </script> <style scoped> .tree-demo :deep(.tree-node-dropover) > :deep(.arco-tree-node-title), .tree-demo :deep(.tree-node-dropover) > :deep(.arco-tree-node-title):hover { animation: blinkBg 0.4s 2; } @keyframes blinkBg { 0% { background-color: transparent; } 100% { background-color: var(--color-primary-light-1); } } </style> src/views/authority/organization/locale/en-US.ts
New file @@ -0,0 +1,5 @@ export default { "menu.organization.title": "Organization", 'menu.organization.detail': 'Detail', }; // export default { "menu.user.title": "Account" }; src/views/authority/organization/locale/zh-CN.ts
New file @@ -0,0 +1,4 @@ export default { 'menu.organization.title': '机构', 'menu.organization.detail': '详情', }; src/views/authority/users/index.vue
New file @@ -0,0 +1,500 @@ <template> <div class="container"> <authheader :items="menuTips"></authheader> <a-card ref="account" class="general-card" :title="$t('menu.user.title')"> <a-row> <a-col :flex="2"></a-col> <a-col :flex="1"> <a-form :model="formModel"> <a-form-item field="name"> <a-input v-model="formModel.name" :style="{width:'320px'}" :placeholder="$t('请输入')" /> </a-form-item> </a-form> </a-col> <a-divider style="height: 40px" direction="vertical" /> <a-col :flex="'200px'" style="text-align: right"> <a-button @click="reset" style="margin-right: 20px"> <template #icon> <icon-refresh /> </template> {{ $t("searchTable.form.reset") }} </a-button> <a-button type="primary" @click="search"> <template #icon> <icon-search /> </template> {{ $t("searchTable.form.search") }} </a-button> </a-col> </a-row> <a-divider style="margin-top: 0" /> <a-row style="margin-bottom: 16px"> <a-col :span="12"> <a-space> <a-button type="primary" :align="'right'" @click="operation(0)">+ 新建账户</a-button> </a-space> </a-col> <a-col :span="12" style="display: flex; align-items: center; justify-content: end" > <a-tooltip :content="$t('searchTable.actions.refresh')"> <div class="action-icon" @click="search" > <icon-refresh size="18" /> </div> </a-tooltip> <a-dropdown @select="handleSelectDensity"> <a-tooltip :content="$t('searchTable.actions.density')"> <div class="action-icon"> <icon-line-height size="18" /> </div> </a-tooltip> <template #content> <a-doption v-for="item in densityList" :key="item.value" :value="item.value" :class="{ active: item.value === size }" > <span>{{ item.name }}</span> </a-doption> </template> </a-dropdown> </a-col> </a-row> <a-table row-key="id" :loading="loading" :pagination="pagination" :columns="columns" :data="renderData" :bordered="false" :size="size" @page-change="onPageChange" > <template #index="{ rowIndex }"> {{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }} </template> <template #dept="{ record }">{{ record.dept ? record.dept.deptName : "" }}</template> <template #status="{record}"> <a-switch checked-value="0" unchecked-value="1" @change="statusChange(record.status,record)" v-model="record.status" /> </template> <template #operations="{ record }"> <a-button type="outline" @click="operation(1,record)">重置密码</a-button> <a-button type="outline" @click="operation(2,record)">编辑</a-button> <a-popconfirm content="Are you sure you want to delete?" type="success" @ok="operation(3,record)"> <a-button type="outline">删除</a-button> </a-popconfirm> <a-button type="outline" @click="operation(4,record)">权限配置</a-button> <a-button type="outline" @click="operation(5,record)">部门配置</a-button> </template> </a-table> </a-card> <a-modal v-model:visible="visible" :title="save" @cancel="handleCancel(1)" @ok="editHandleOk"> <a-form :model="editform"> <a-form-item field="name" label="昵称"> <a-input v-model="editform.nickName" /> </a-form-item> <a-form-item field="userName" label="用户名"> <a-input v-model="editform.userName" /> </a-form-item> <a-form-item field="phoneNumber" label="手机号"> <a-input v-model="editform.phoneNumber" /> </a-form-item> <a-form-item field="email" label="邮箱"> <a-input v-model="editform.email" /> </a-form-item> </a-form> </a-modal> <a-modal width="50%" v-model:visible="deptvisible" title="部门配置" @cancel="handleCancel(2)" @ok="editDeptHandleOk"> <div :style="{ display: 'flex' }"> <a-card :style="{ width: '460px',height: '500px', 'overflow-y': 'auto' }" title="机构" hoverable> <a-tree class="tree-demo" v-model:checked-keys="checkedKeys" v-model:expanded-keys="expandKdys" :checkable="true" :data="treeData" :show-line="showLine" @check="onCheck" :fieldNames="{ key:'deptId', title:'deptName', children:'children', }" :check-strictly="checkStrictly" > </a-tree> </a-card> <a-card class="card-demo" title="用户所属机构" hoverable > <a-space wrap> <a-tag v-for="(tag, index) of checkStrictly" :key="tag.deptId" @close="handleRemove(tag)" > {{ tag.deptName }} </a-tag> </a-space> </a-card> </div> </a-modal> <a-modal width="30%" v-model:visible="resourcevisible" title="权限配置" @cancel="handleCancel(3)" @ok="editDeptHandleOk"> <div :style="{ display:'flex', 'flex-direction':'column' }"> <a-tabs :style="{ width: '100%',height: '500px', 'overflow-y': 'auto' }"> <a-tab-pane key="1"> <template #title> <icon-calendar/> 菜单 </template> Content of Tab Panel 1 </a-tab-pane> <a-tab-pane key="2"> <template #title> <icon-clock-circle/> 知识库 </template> Content of Tab Panel 2 </a-tab-pane> <a-tab-pane key="3"> <template #title> <icon-user/> 智能体 </template> Content of Tab Panel 3 </a-tab-pane> </a-tabs> <a-card :style="{ width: '100%',height: '200px', 'overflow-y': 'auto', margin:'1px'}" class="card-demo" title="用户所有权限" hoverable > <a-space wrap> <a-tag v-for="(tag, index) of checkResourceStrictly" :key="tag.deptId" @close="handleRemove(tag)" > {{ tag.deptName }} </a-tag> </a-space> </a-card> </div> </a-modal> </div> </template> <script lang="ts" setup> import { computed, reactive, ref } from "vue"; import { useI18n } from "vue-i18n"; import useLoading from "@/hooks/loading"; import { Pagination } from "@/types/global"; import type { TableColumnData } from "@arco-design/web-vue/es/table/interface"; import { OrganizationList, User, UserAdd, UserChangePwd, UserDelete, UserEdit, UserList, Userstatus } from "@/api/authority"; import { Modal } from "@arco-design/web-vue"; import Authheader from "@/views/authority/components/authheader.vue"; let treeData = ref([]); let checkedKeys = ref([]); let expandKdys = ref([]); let checkStrictly = ref([]); let checkResourceStrictly=ref([]) let menuTips = ref(["权限管理", "账号"]); type SizeProps = "mini" | "small" | "medium" | "large"; const account = ref(null); const generateFormModel = () => { return { name: "" }; }; let showLine = ref(true); const { loading, setLoading } = useLoading(true); const { t } = useI18n(); let save = ref("新增"); let renderData = ref<User[]>([]); let formModel = ref(generateFormModel()); let editform = ref<User>({ createTime: "", dept: undefined, email: "", nickName: "", phoneNumber: "", status: "", userId: "", userName: "" }); let size = ref<SizeProps>("medium"); let visible = ref(false); let deptvisible = ref(false); let resourcevisible = ref(false); let selectUser = ref({}); const onCheck = (newCheckedKeys, event) => { let o = { "deptId": event.node.deptId, "deptName": event.node.deptName }; if (event.checked) { checkStrictly.value.push(o); } else { checkStrictly.value.forEach((val, idx, array) => { // val: 当前值 if (val.deptId == event.node.deptId) { checkStrictly.value.splice(idx, 1); return true; } }); } }; const handleRemove = (key) => { checkStrictly.value = checkStrictly.value.filter((tag) => tag !== key); }; const basePagination: Pagination = { current: 1, pageSize: 20 }; const pagination = reactive({ ...basePagination }); const densityList = computed(() => [ { name: t("searchTable.size.mini"), value: "mini" }, { name: t("searchTable.size.small"), value: "small" }, { name: t("searchTable.size.medium"), value: "medium" }, { name: t("searchTable.size.large"), value: "large" } ]); const columns = computed<TableColumnData[]>(() => [ { title: t("序号"), dataIndex: "index", slotName: "index" }, { title: t("用户名"), dataIndex: "userName" }, { title: t("创建时间"), dataIndex: "createTime" }, { title: t("所属部门"), dataIndex: "dept", slotName: "dept" }, { title: t("状态"), dataIndex: "status", slotName: "status" }, { title: t("searchTable.columns.operations"), dataIndex: "operations", slotName: "operations" } ]); const statusChange = async (value, record) => { await Userstatus(record.userId, value).then((res) => { }); }; const handleCancel = (type) => { if (type == 1) { visible.value = false; } if (type == 2) { deptvisible.value = false; } if (type == 2) { resourcevisible.value = false; } }; const editDeptHandleOk = async () => { let depts: Array = [], user: User = { "userId": selectUser.value.userId }; checkStrictly.value.forEach((val) => { depts.push(val.deptId); }); user.dept = depts; await UserEdit(user).then((res) => { fetchData(); }); }; const editHandleOk = async () => { if (editform.value.userId.length > 0) { await UserEdit({ ...editform.value } as unknown as User).then((res) => { fetchData(); }); } else { await UserAdd({ ...editform.value } as unknown as User).then((res) => { fetchData(); }); } }; const operation = async (t, record) => { if (t == 0) { save.value = "新增"; visible.value = true; editform.value.userId = ""; editform.value.userName = ""; editform.value.nickName = ""; editform.value.email = ""; editform.value.phoneNumber = ""; } //重置密码 if (t == 1) { await UserChangePwd(record.userId).then((res) => { if (res.code == 20000) { Modal.success({ title: "重置密码", content: "该用户密码重置为000000" }); } else { Modal.error({ title: "重置密码", content: "该用户密码重置失败" }); } }); } //编辑 if (t == 2) { visible.value = true; save.value = "编辑"; editform.value.userId = record.userId; editform.value.userName = record.userName; editform.value.nickName = record.nickName; editform.value.email = record.email; editform.value.phoneNumber = record.phoneNumber; } //删除 if (t == 3) { await UserDelete(record.userId).then((res) => { if (res.code == 20000) { fetchData(); } }); } //权限 if (t == 4) { resourcevisible.value = true; } //机构 if (t == 5) { deptvisible.value = true; checkedKeys.value = []; expandKdys.value = []; checkStrictly.value = []; selectUser.value = record; expandKdys.value.push("0"); record.dept.forEach((val) => { checkStrictly.value.push({ "deptId": val.deptId, "deptName": val.deptName }); checkedKeys.value.push(val.deptId); expandKdys.value.push(val.deptId); }); } }; const fetchData = async ( params: Pagination = { current: 1, pageSize: 20 } ) => { setLoading(true); try { await UserList(params).then((res) => { renderData.value = res.rows; console.log(renderData); pagination.current = params.current; pagination.total = res.total; }); } catch (err) { // you can report use errorHandler or other } finally { setLoading(false); } }; const search = () => { fetchData({ ...basePagination, ...formModel.value } as unknown as Pagination); }; const onPageChange = (current: number) => { fetchData({ ...basePagination, current }); }; const OrganizationData = async (key) => { await OrganizationList(key).then((res) => { treeData.value = [...res.rows]; }); }; fetchData(); OrganizationData(""); const reset = () => { formModel.value = generateFormModel(); }; const handleSelectDensity = ( val: string | number | Record<string, any> | undefined, e: Event ) => { size.value = val as SizeProps; }; </script> <style scoped> .card-demo { width: 460px; margin-left: 24px; transition-property: all; } .card-demo:hover { transform: translateY(-4px); } </style> src/views/authority/users/locale/en-US.ts
New file @@ -0,0 +1,4 @@ export default { "menu.user.title": "Account", }; // export default { "menu.user.title": "Account" }; src/views/authority/users/locale/zh-CN.ts
New file @@ -0,0 +1,3 @@ export default { 'menu.user.title': '账户', };