<script lang="ts" setup>
|
import type { UploadChangeParam } from 'ant-design-vue';
|
|
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
|
import { useGlobSetting, useMessage } from '@jnpf/hooks';
|
|
import { updatePreferences } from '@vben/preferences';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
|
import { StorageManager } from '@vben-core/shared/cache';
|
|
import { CheckOutlined } from '@ant-design/icons-vue';
|
|
import { getUserPositions, getUserSettingInfo, setMajor, updateAvatar, updateUserInfo } from '#/api/permission/userSetting';
|
import { getRealJnpfAppEnCode } from '#/utils/jnpf';
|
|
import Account from './components/Account/index.vue';
|
import Authorize from './components/Authorize.vue';
|
import Entrust from './components/Entrust/index.vue';
|
import JustAuth from './components/JustAuth.vue';
|
import Preference from './components/Preference.vue';
|
import SysLog from './components/SysLog.vue';
|
import TenantInfo from './components/TenantInfo.vue';
|
import UserInfo from './components/UserInfo.vue';
|
|
interface State {
|
activeKey: string;
|
subActiveKey: number | string;
|
user: any;
|
tenantInfo: any;
|
isTenant: boolean;
|
userLoading: boolean;
|
organizeList: any[];
|
activeOrganize: string;
|
}
|
|
defineExpose({ init });
|
|
const { createMessage } = useMessage();
|
const ls = new StorageManager();
|
const userStore = useUserStore();
|
const accessStore = useAccessStore();
|
const globSetting = useGlobSetting();
|
const apiUrl = ref(globSetting.apiURL);
|
const uploadUrl = ref(globSetting.uploadURL);
|
const preferenceRef = ref();
|
const state = reactive<State>({
|
activeKey: '',
|
user: {},
|
tenantInfo: {},
|
isTenant: false,
|
userLoading: false,
|
organizeList: [],
|
activeOrganize: '',
|
subActiveKey: '',
|
});
|
const { activeKey, user, tenantInfo, isTenant, subActiveKey } = toRefs(state);
|
|
const getHeaders = computed(() => ({ Authorization: accessStore.accessToken }));
|
const getUseSocials = computed(() => !!ls.getItem('useSocials'));
|
const getStandingList = computed(() => userStore.getUserInfo?.standingList || []);
|
|
watch(
|
() => state.activeKey,
|
(val) => {
|
if (val === 'organize') return getUserPositionsList();
|
},
|
);
|
|
function beforeUpload(file) {
|
const isAccept = /image\/*/.test(file.type);
|
if (!isAccept) createMessage.error(`请上传图片`);
|
return isAccept;
|
}
|
function handleChange({ file }: UploadChangeParam) {
|
if (file.status === 'error') {
|
createMessage.error('上传失败');
|
return;
|
}
|
if (file.status === 'done') {
|
if (file.response.code === 200) {
|
if (!file.response.data || !file.response.data.name) return;
|
updateAvatar(file.response.data.name).then((res) => {
|
state.user.avatar = file.response.data.url;
|
userStore.setUserInfo({ headIcon: file.response.data.url });
|
createMessage.success(res.msg);
|
});
|
} else {
|
createMessage.error(file.response.msg);
|
}
|
}
|
}
|
function getInfo(data) {
|
state.userLoading = true;
|
state.subActiveKey = data?.subActiveKey || '1';
|
getUserSettingInfo().then((res) => {
|
state.user = res.data;
|
state.tenantInfo = res.data.currentTenantInfo;
|
state.isTenant = res.data.isTenant || false;
|
state.activeKey = data?.activeKey || 'user';
|
state.userLoading = false;
|
const preferenceJson = res.data.preferenceJson ? JSON.parse(res.data.preferenceJson) : null;
|
preferenceRef.value?.initConfig(preferenceJson);
|
});
|
}
|
function getUserPositionsList() {
|
getUserPositions().then((res) => {
|
state.organizeList = res.data || [];
|
const list = state.organizeList.filter((o) => o.isDefault);
|
if (!list.length) return (state.activeOrganize = '');
|
const activeItem = list[0];
|
state.activeOrganize = activeItem.id;
|
});
|
}
|
function toggleStanding(item) {
|
if (item.currentStanding) return;
|
changeMajor(item.id, 'standing');
|
}
|
function changeMajor(majorId, majorType) {
|
if (majorType !== 'standing' && state[`active${majorType}`] === majorId) return;
|
const query = { majorId, majorType };
|
setMajor(query).then((res) => {
|
if (majorType !== 'standing') state[`active${majorType}`] = majorId;
|
createMessage.success(res.msg).then(() => {
|
location.reload();
|
});
|
});
|
}
|
function savePreference() {
|
preferenceRef.value?.handleSubmit();
|
}
|
function updatePreferenceJson(data) {
|
if (!data) return;
|
const preferenceJson = JSON.parse(data);
|
const appEnCode = getRealJnpfAppEnCode();
|
if (!appEnCode) preferenceJson.app.layout = 'mixed-nav';
|
if (appEnCode && ['teamwork', 'workFlow'].includes(appEnCode)) preferenceJson.app.layout = 'sidebar-nav';
|
updatePreferences(preferenceJson);
|
}
|
function init(data) {
|
getInfo(data);
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper profile-wrapper bg-white">
|
<a-tabs v-model:active-key="activeKey" tab-position="left" class="common-left-tabs profile-left-tabs" destroy-inactive-tab-pane>
|
<a-tab-pane key="user" tab="个人资料">
|
<UserInfo :user="user" @update-info="getInfo" />
|
</a-tab-pane>
|
<a-tab-pane key="account" tab="账号安全">
|
<Account :user="user" />
|
</a-tab-pane>
|
<a-tab-pane key="tenantInfo" tab="租户信息" v-if="isTenant">
|
<TenantInfo :tenant-info="tenantInfo" />
|
</a-tab-pane>
|
<a-tab-pane key="justAuth" tab="社交账号" v-if="getUseSocials">
|
<JustAuth />
|
</a-tab-pane>
|
<a-tab-pane key="preference" tab="偏好设置" force-render>
|
<div class="flex h-full flex-col overflow-hidden">
|
<jnpf-group-title content="偏好设置">
|
<template #action>
|
<a-button type="primary" class="mr-[10px]" @click="savePreference">保存</a-button>
|
</template>
|
</jnpf-group-title>
|
<div class="flex-1 overflow-auto p-[20px] pt-[10px]">
|
<Preference ref="preferenceRef" :save-api="updateUserInfo" @update-preference="updatePreferenceJson" />
|
</div>
|
</div>
|
</a-tab-pane>
|
<a-tab-pane key="line" disabled />
|
<a-tab-pane key="standing" tab="我的身份">
|
<div class="flex h-full flex-col overflow-hidden">
|
<jnpf-group-title content="我的身份" />
|
<div class="flex-1 overflow-auto">
|
<div class="organize-list standing-list">
|
<a-row :gutter="80" v-if="getStandingList.length">
|
<a-col :span="12" class="organize-item" v-for="(item, i) in getStandingList" :key="i">
|
<div class="organize-item-main" :class="{ active: item.currentStanding }" @click="toggleStanding(item)">
|
<i :class="item.icon"></i>
|
<p class="organize-name">{{ item.name }}</p>
|
<p class="btn">默认</p>
|
<div class="icon-checked">
|
<CheckOutlined />
|
</div>
|
</div>
|
</a-col>
|
</a-row>
|
<jnpf-empty v-else />
|
</div>
|
</div>
|
</div>
|
</a-tab-pane>
|
<a-tab-pane key="organize" tab="我的组织">
|
<div class="flex h-full flex-col overflow-hidden">
|
<jnpf-group-title content="我的组织" />
|
<div class="flex-1 overflow-auto">
|
<a-alert message="默认组织仅逐级审批时使用" show-icon type="warning" class="mt-[10px]" />
|
<div class="organize-list organize">
|
<a-row :gutter="80" v-if="state.organizeList.length">
|
<a-col :span="12" class="organize-item" v-for="(item, i) in state.organizeList" :key="i">
|
<div class="organize-item-main" :class="{ active: state.activeOrganize === item.id }" @click="changeMajor(item.id, 'position')">
|
<div class="organize-item-cell">
|
<div class="cell-label">所属组织</div>
|
<div class="cell-value">{{ item.orgTreeName }}</div>
|
</div>
|
<div class="organize-item-cell">
|
<div class="cell-label">任职岗位</div>
|
<div class="cell-value">{{ item.fullName }}</div>
|
</div>
|
<div class="organize-item-cell">
|
<div class="cell-label">上级岗位</div>
|
<div class="cell-value">{{ item.parentName || '-' }}</div>
|
</div>
|
<div class="organize-item-cell">
|
<div class="cell-label">上级责任人</div>
|
<div class="cell-value">{{ item.managerName }}</div>
|
</div>
|
<p class="btn">默认</p>
|
<div class="icon-checked">
|
<CheckOutlined />
|
</div>
|
</div>
|
</a-col>
|
</a-row>
|
<jnpf-empty v-else />
|
</div>
|
</div>
|
</div>
|
</a-tab-pane>
|
<a-tab-pane key="authorize" class="authorize-tab" tab="系统权限">
|
<Authorize />
|
</a-tab-pane>
|
<a-tab-pane key="entrust" tab="委托代理">
|
<Entrust :sub-active-key="subActiveKey" />
|
</a-tab-pane>
|
<a-tab-pane key="sysLog" tab="登录日志">
|
<SysLog />
|
</a-tab-pane>
|
<template #leftExtra>
|
<div class="head">
|
<a-upload
|
:show-upload-list="false"
|
:action="`${uploadUrl}/userAvatar`"
|
class="avatar-uploader"
|
:headers="getHeaders"
|
accept="image/*"
|
:before-upload="beforeUpload"
|
@change="handleChange">
|
<div class="avatar-box">
|
<a-avatar :size="50" :src="apiUrl + user.avatar" class="avatar" v-if="user.avatar" />
|
<div class="avatar-hover">更换头像</div>
|
</div>
|
</a-upload>
|
<span class="username">{{ user.realName }}</span>
|
</div>
|
</template>
|
</a-tabs>
|
</div>
|
</template>
|
|
<style lang="scss">
|
.profile-wrapper {
|
.profile-left-tabs {
|
width: 100%;
|
margin-right: 0;
|
|
.ant-tabs-tab-disabled {
|
padding: 0 !important;
|
|
.ant-tabs-tab-btn {
|
width: 100%;
|
border-bottom: 1px solid var(--border-color-base1);
|
}
|
}
|
|
.ant-tabs-content-holder {
|
width: 100% !important;
|
|
.ant-tabs-content-left {
|
height: 100%;
|
|
& > .ant-tabs-tabpane {
|
height: 100%;
|
padding-left: 10px;
|
overflow: auto;
|
}
|
|
& > .authorize-tab {
|
padding-left: 0 !important;
|
}
|
}
|
}
|
}
|
|
.head {
|
width: 160px;
|
height: 70px;
|
padding-top: 10px;
|
padding-left: 10px;
|
|
.avatar-uploader {
|
display: inline-block;
|
vertical-align: top;
|
|
.avatar-hover {
|
position: absolute;
|
top: 0;
|
left: 0;
|
display: none;
|
width: 50px;
|
height: 50px;
|
overflow: hidden;
|
font-size: 12px;
|
line-height: 50px;
|
color: #fff;
|
text-align: center;
|
cursor: pointer;
|
background: rgb(0 0 0 / 50%);
|
border-radius: 50%;
|
}
|
|
&:hover {
|
& .avatar-hover {
|
display: block;
|
}
|
}
|
}
|
|
.avatar-box {
|
position: relative;
|
}
|
|
.avatar {
|
display: inline-block;
|
width: 50px;
|
height: 50px;
|
margin-right: 10px;
|
overflow: hidden;
|
vertical-align: top;
|
border-radius: 50%;
|
}
|
|
.username {
|
display: inline-block;
|
width: 90px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
font-size: 14px;
|
line-height: 50px;
|
white-space: nowrap;
|
}
|
}
|
|
.organize-list {
|
width: 100%;
|
padding: 50px;
|
|
&.standing-list {
|
.organize-item .organize-item-main {
|
display: flex;
|
align-items: center;
|
height: 70px;
|
|
&.active {
|
color: var(--primary-color);
|
}
|
}
|
}
|
|
.organize-item {
|
margin-bottom: 30px;
|
|
.organize-item-main {
|
position: relative;
|
padding: 20px;
|
color: var(--text-color-base);
|
cursor: pointer;
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
box-shadow: 0 0 6px rgb(0 0 0 / 16%);
|
|
&.active {
|
border: 1px solid var(--primary-color);
|
box-shadow: 0 0 6px rgb(6 58 108 / 26%);
|
|
.btn,
|
.icon-checked {
|
display: block;
|
color: var(--primary-color);
|
}
|
}
|
|
.organize-item-cell {
|
display: flex;
|
line-height: 40px;
|
|
.cell-label {
|
flex-shrink: 0;
|
width: 100px;
|
color: var(--text-color-secondary);
|
}
|
|
.cell-value {
|
flex: 1;
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
}
|
|
.icon-ym {
|
margin-right: 10px;
|
font-size: 24px;
|
}
|
|
.organize-name {
|
font-size: 14px;
|
line-height: 24px;
|
}
|
|
.btn {
|
position: absolute;
|
right: 45px;
|
bottom: 7px;
|
display: none;
|
font-size: 12px;
|
}
|
|
.icon-checked {
|
position: absolute;
|
right: -2px;
|
bottom: -2px;
|
display: none;
|
width: 20px;
|
height: 20px;
|
border: 20px solid var(--primary-color);
|
border-top: 20px solid transparent !important;
|
border-left: 20px solid transparent !important;
|
border-bottom-right-radius: 2px;
|
transform: scale(0.9);
|
|
.anticon-check {
|
position: absolute;
|
top: 0;
|
left: 0;
|
font-size: 16px;
|
color: #fff;
|
}
|
}
|
}
|
}
|
}
|
}
|
</style>
|