用户管理修改

This commit is contained in:
dzq 2025-03-31 09:41:56 +08:00
parent 1c23d04690
commit e8672104a4
23 changed files with 954 additions and 201 deletions

View File

@ -1,25 +1,16 @@
<template> <script setup lang="ts">
<el-config-provider :locale="currentLocale"> import { computed } from "vue";
<router-view />
<ReDialog />
</el-config-provider>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus"; import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn"; import zhCn from "element-plus/lib/locale/lang/zh-cn";
import { ReDialog } from "@/components/ReDialog"; import { ReDialog } from "@/components/ReDialog";
export default defineComponent({
name: "app", const currentLocale = computed(() => zhCn);
components: {
[ElConfigProvider.name]: ElConfigProvider,
ReDialog
},
computed: {
currentLocale() {
return zhCn;
}
}
});
</script> </script>
<template>
<ElConfigProvider :locale="currentLocale">
<router-view />
<ReDialog />
</ElConfigProvider>
</template>

View File

@ -23,6 +23,10 @@ export type LoginByPasswordDTO = {
captchaCode: string; captchaCode: string;
/** 验证码对应的缓存key */ /** 验证码对应的缓存key */
captchaCodeKey: string; captchaCodeKey: string;
/** 企业微信 */
corpid: string;
code: string;
state: string;
}; };
/** /**
@ -54,7 +58,7 @@ export interface CurrentUserInfoDTO {
email?: string; email?: string;
loginDate?: Date; loginDate?: Date;
loginIp?: string; loginIp?: string;
nickName?: string; nickname?: string;
phoneNumber?: string; phoneNumber?: string;
postId?: number; postId?: number;
postName?: string; postName?: string;
@ -106,3 +110,15 @@ type Result = {
export const getAsyncRoutes = () => { export const getAsyncRoutes = () => {
return http.request<Result>("get", "/getRouters"); return http.request<Result>("get", "/getRouters");
}; };
type qyUserinfoQuery = {
// 授权企业id
corpid: string;
// 企业微信 code
code: string;
}
/** 获取企业微信访问用户身份接口 */
export const getQyUserinfo = (params: qyUserinfoQuery) => {
return http.request<ResponseData<string>>("get", "/getQyUserinfo", { params });
};

112
src/api/qy/qyUser.ts Normal file
View File

@ -0,0 +1,112 @@
import { http } from "@/utils/http";
/**
*
*/
export interface QyUserDTO {
/** 用户ID导出列用户ID */
id?: number;
/** 全局唯一ID导出列全局唯一ID */
openUserid?: string;
/** 企业用户ID导出列企业用户ID */
userid?: string;
/** 用户姓名(导出列:用户姓名) */
name?: string;
/** 手机号码(导出列:手机号码) */
mobile?: string;
/** 所属部门(导出列:所属部门) */
department?: string;
/** 部门排序(导出列:部门排序) */
userOrder?: string;
/** 职务信息(导出列:职务信息) */
position?: string;
/** 性别1男 2女导出列性别 */
gender?: string;
/** 邮箱(导出列:邮箱) */
email?: string;
/** 企业邮箱(导出列:企业邮箱) */
bizMail?: string;
/** 部门负责人(导出列:部门负责人) */
isLeaderInDept?: string;
/** 直属上级(导出列:直属上级) */
directLeader?: string;
/** 头像地址(导出列:头像地址) */
avatar?: string;
/** 座机号码(导出列:座机号码) */
telephone?: string;
/** 别名(导出列:别名) */
alias?: string;
/** 激活状态(导出列:激活状态) */
status?: string;
/** 个人二维码(导出列:个人二维码) */
qrCode?: string;
/** 操作人(导出列:操作人) */
operator?: string;
/** 有效状态(导出列:有效状态) */
enableStatus?: string;
/** 创建时间(导出列:创建时间) */
createTimeStr?: string;
/** 企业ID导出列企业ID */
corpid?: string;
/** 应用ID导出列应用ID */
appid?: string;
}
export interface QyUserQuery extends BasePageQuery {
/** 姓名(导出列:姓名) */
name?: string;
/** 手机号(导出列:联系方式) */
mobile?: string;
corpid?: string;
mainDepartment?: number;
}
export interface AddQyUserCommand {
name: string;
mobile: string;
department: string;
corpid: string;
}
export interface UpdateQyUserCommand extends AddQyUserCommand {
id: number;
}
/**
*
* @param params
* @returns
*/
export const getQyUserListApi = (params: QyUserQuery) => {
return http.request<ResponseData<PageDTO<QyUserDTO>>>("get", "/qywx/users", {
params: { ...params, mainDepartment: params.mainDepartment?.toString() }
});
};
export const addQyUserApi = (data: AddQyUserCommand) => {
return http.request<ResponseData<void>>("post", "/qywx/users", { data });
};
export const updateQyUserApi = (id: number, data: UpdateQyUserCommand) => {
return http.request<ResponseData<void>>("put", `/qywx/users/${id}`, { data });
};
export const deleteQyUserApi = (ids: number[]) => {
return http.request<ResponseData<void>>("delete", `/qywx/users/${ids.join(',')}`);
};
/**
*
* @param params
* @param params.corpid ID
* @param params.code code
*/
export const syncQyUserApi = (params: { corpid: string; code: string }) => {
return http.request<ResponseData<void>>("post", "/qywx/users/sync", {
params
});
};
export const exportQyUserExcelApi = (params: QyUserQuery, fileName: string) => {
return http.download("/qywx/users/excel", fileName, { params });
};

View File

@ -48,6 +48,11 @@ export const getDeptListApi = (params?: DeptQuery) => {
params params
}); });
}; };
export const getQyDeptListApi = (corpid: string) => {
return http.request<ResponseData<Array<DeptDTO>>>("get", "/qywx/departments/depts", {
params: { corpid }
});
};
/** 新增部门 */ /** 新增部门 */
export const addDeptApi = (data: DeptRequest) => { export const addDeptApi = (data: DeptRequest) => {

View File

@ -6,6 +6,7 @@ export interface UserQuery extends BasePageQuery {
status?: number; status?: number;
userId?: number; userId?: number;
username?: string; username?: string;
nickname?: string,
} }
/** /**

View File

@ -7,16 +7,27 @@
<div v-if="showQr" class="qr-popover"> <div v-if="showQr" class="qr-popover">
<ReQrcode text="http://wxshop.ab98.cn/shop-api/api/shop/wechatAuth" :options="{ width: 200 }" /> <ReQrcode text="http://wxshop.ab98.cn/shop-api/api/shop/wechatAuth" :options="{ width: 200 }" />
<div class="qr-tip">微信扫码访问</div> <div class="qr-tip">微信扫码访问</div>
<el-button class="copy-btn" type="primary" size="small" @click="copyLink">
复制链接
</el-button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import ReQrcode from '@/components/ReQrcode' import ReQrcode from '@/components/ReQrcode'
import Iphone from "@iconify-icons/ep/iphone"; import Iphone from "@iconify-icons/ep/iphone";
import { copyTextToClipboard } from "@pureadmin/utils";
const showQr = ref(false) const showQr = ref(false)
const copyLink = () => {
const success = copyTextToClipboard('http://wxshop.ab98.cn/shop-api/api/shop/wechatAuth');
success ? ElMessage.success('链接复制成功') : ElMessage.error('复制失败,请手动复制');
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -32,6 +43,9 @@ const showQr = ref(false)
border-radius: 4px; border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 999; z-index: 999;
display: flex;
flex-direction: column;
align-items: center;
} }
.qr-tip { .qr-tip {

View File

@ -17,6 +17,7 @@ const {
onPanel, onPanel,
pureApp, pureApp,
username, username,
nickname,
userAvatar, userAvatar,
avatarsStyle, avatarsStyle,
toggleSideBar toggleSideBar
@ -43,7 +44,7 @@ const {
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
<img :src="userAvatar" :style="avatarsStyle" /> <img :src="userAvatar" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="nickname" class="dark:text-white">{{ nickname }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">

View File

@ -19,6 +19,7 @@ const {
backTopMenu, backTopMenu,
onPanel, onPanel,
username, username,
nickname,
userAvatar, userAvatar,
avatarsStyle avatarsStyle
} = useNav(); } = useNav();
@ -53,7 +54,7 @@ nextTick(() => {
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover"> <span class="el-dropdown-link navbar-bg-hover">
<img :src="userAvatar" :style="avatarsStyle" /> <img :src="userAvatar" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="nickname" class="dark:text-white">{{ nickname }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">

View File

@ -22,6 +22,7 @@ const {
onPanel, onPanel,
resolvePath, resolvePath,
username, username,
nickname,
userAvatar, userAvatar,
getDivStyle, getDivStyle,
avatarsStyle avatarsStyle
@ -82,7 +83,7 @@ watch(
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
<img :src="userAvatar" :style="avatarsStyle" /> <img :src="userAvatar" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="nickname" class="dark:text-white">{{ nickname }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">

View File

@ -40,6 +40,10 @@ export function useNav() {
return useUserStoreHook()?.username; return useUserStoreHook()?.username;
}); });
const nickname = computed(() => {
return useUserStoreHook()?.nickname;
});
const avatarsStyle = computed(() => { const avatarsStyle = computed(() => {
return username.value ? { marginRight: "10px" } : ""; return username.value ? { marginRight: "10px" } : "";
}); });
@ -140,6 +144,7 @@ export function useNav() {
isCollapse, isCollapse,
pureApp, pureApp,
username, username,
nickname,
userAvatar, userAvatar,
avatarsStyle, avatarsStyle,
tooltipEffect tooltipEffect

View File

@ -38,6 +38,7 @@ export type setType = {
export type userType = { export type userType = {
username?: string; username?: string;
nickname?: string;
roles?: Array<string>; roles?: Array<string>;
/** 字典ListMap 用于下拉框直接展示 */ /** 字典ListMap 用于下拉框直接展示 */
dictionaryList: Map<String, Array<DictionaryData>>; dictionaryList: Map<String, Array<DictionaryData>>;

View File

@ -19,6 +19,10 @@ export const useUserStore = defineStore({
username: username:
storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.userInfo storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.userInfo
.username ?? "", .username ?? "",
// 昵称
nickname:
storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.userInfo
.nickname ?? "",
// 页面级别权限 // 页面级别权限
roles: storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.roleKey roles: storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.roleKey
? [storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.roleKey] ? [storageSession().getItem<TokenDTO>(sessionKey)?.currentUser.roleKey]
@ -40,6 +44,11 @@ export const useUserStore = defineStore({
/** TODO 这里不是应该再进一步存到sessionStorage中吗 */ /** TODO 这里不是应该再进一步存到sessionStorage中吗 */
this.username = username; this.username = username;
}, },
/** 存储昵称 */
SET_NICKNAME(nickname: string) {
/** TODO 这里不是应该再进一步存到sessionStorage中吗 */
this.nickname = nickname;
},
/** 存储角色 */ /** 存储角色 */
SET_ROLES(roles: Array<string>) { SET_ROLES(roles: Array<string>) {
this.roles = roles; this.roles = roles;

53
src/store/modules/wx.ts Normal file
View File

@ -0,0 +1,53 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { store } from "@/store";
export const useWxStore = defineStore("wx", () => {
// 授权企业id
const corpid = ref<string>("")
// 微信授权 code
const code = ref<string>("")
// 防止 CSRF 攻击的 state 参数
const state = ref<string>("")
// 用户 userid
const userid = ref<string>("")
// 设置 userid
const setUserid = (id: string) => {
userid.value = id
}
const handleWxCallback = async (params: { corpid: string; code: string; state: string }) => {
console.log('handleWxCallback:', params)
if (params.code && params.corpid) {
corpid.value = params.corpid;
code.value = params.code;
state.value = params.state || state.value;
// try {
// // 调用获取 userid 的接口
// const res = await getQyUserinfo({
// corpid: params.corpid,
// code: params.code
// })
// console.log('获取 userid 成功:', res)
// if (res && res.code == 0) {
// userid.value = res.data
// }
// } catch (err) {
// console.error('获取 userid 失败:', err)
// }
}
}
return { corpid, code, state, userid, setUserid, handleWxCallback }
})
/**
* @description setup 使 store
*/
export function useWxStoreOutside() {
return useWxStore(store)
}

View File

@ -42,6 +42,7 @@ export function setTokenFromBackend(data: TokenDTO): void {
useUserStoreHook().SET_USERNAME(data.currentUser.userInfo.username); useUserStoreHook().SET_USERNAME(data.currentUser.userInfo.username);
useUserStoreHook().SET_ROLES([data.currentUser.roleKey]); useUserStoreHook().SET_ROLES([data.currentUser.roleKey]);
useUserStoreHook().SET_NICKNAME(data.currentUser.userInfo.nickname);
storageSession().setItem(sessionKey, data); storageSession().setItem(sessionKey, data);
} }

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { PureTableBar } from "@/components/RePureTableBar"; import { PureTableBar } from "@/components/RePureTableBar";
import { useRoute } from "vue-router"; import { onBeforeRouteUpdate, useRoute } from "vue-router";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { getCabinetCellList, deleteCabinetCell, CabinetCellDTO } from "@/api/cabinet/cabinet-cell"; import { getCabinetCellList, deleteCabinetCell, CabinetCellDTO } from "@/api/cabinet/cabinet-cell";
import EditPen from "@iconify-icons/ep/edit-pen"; import EditPen from "@iconify-icons/ep/edit-pen";
@ -12,6 +12,7 @@ import Refresh from "@iconify-icons/ep/refresh";
import CellFormModal from "./cell-form-modal.vue"; import CellFormModal from "./cell-form-modal.vue";
import CellEditModal from "./cell-edit-modal.vue"; import CellEditModal from "./cell-edit-modal.vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { on } from "events";
defineOptions({ defineOptions({
name: "CabinetCell" name: "CabinetCell"
@ -47,6 +48,12 @@ onMounted(() => {
getList(); getList();
} }
}); });
onBeforeRouteUpdate(() => {
if (route.query.cabinetId) {
searchFormParams.value.cabinetId = Number(route.query.cabinetId);
getList();
}
});
const getList = async () => { const getList = async () => {
try { try {
@ -136,7 +143,6 @@ const switchCellType = (cellType: number) => {
} }
}; };
getList();
</script> </script>
<template> <template>

View File

@ -41,6 +41,7 @@ import Lock from "@iconify-icons/ri/lock-fill";
import User from "@iconify-icons/ri/user-3-fill"; import User from "@iconify-icons/ri/user-3-fill";
import * as CommonAPI from "@/api/common/login"; import * as CommonAPI from "@/api/common/login";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { useWxStore } from "@/store/modules/wx";
defineOptions({ defineOptions({
name: "Login" name: "Login"
@ -58,6 +59,8 @@ const ruleFormRef = ref<FormInstance>();
// 01234 // 01234
const currentPage = ref(0); const currentPage = ref(0);
const wxStore = useWxStore();
const { initStorage } = useLayout(); const { initStorage } = useLayout();
initStorage(); initStorage();
const { dataTheme, dataThemeChange } = useDataThemeChange(); const { dataTheme, dataThemeChange } = useDataThemeChange();
@ -65,23 +68,66 @@ dataThemeChange();
// const { title, getDropdownItemStyle, getDropdownItemClass } = useNav(); // const { title, getDropdownItemStyle, getDropdownItemClass } = useNav();
const { title } = useNav(); const { title } = useNav();
const urlParams = new URLSearchParams(window.location.search);
console.log('urlParams', urlParams);
const corpid = urlParams.get('corpid') || undefined;
const code = urlParams.get('code') || undefined;
const state = urlParams.get('state') || undefined;
if (code && corpid) {
wxStore.handleWxCallback({ corpid, code, state })
}
const ruleForm = reactive({ const ruleForm = reactive({
username: "admin", username: "admin",
password: getPassword(), password: getPassword(),
captchaCode: "", captchaCode: "",
captchaCodeKey: "" captchaCodeKey: "",
corpid: wxStore.corpid,
code: wxStore.code,
state: wxStore.state
}); });
const onLogin = async (formEl: FormInstance | undefined) => { const onLogin = async (formEl: FormInstance | undefined) => {
loading.value = true; loading.value = true;
if (!formEl) return; if (!formEl) return;
if (code && corpid) {
CommonAPI.loginByPassword({
username: ruleForm.username,
password: ruleForm.password ? rsaEncrypt(ruleForm.password) : '',
captchaCode: ruleForm.captchaCode,
captchaCodeKey: ruleForm.captchaCodeKey,
corpid: ruleForm.corpid,
code: ruleForm.code,
state: ruleForm.state
})
.then(({ data }) => {
// tokensessionStorage
setTokenFromBackend(data);
//
initRouter().then(() => {
router.push(getTopMenu(true).path);
message("登录成功", { type: "success" });
});
if (isRememberMe.value) {
savePassword(ruleForm.password);
}
})
.catch(() => {
loading.value = false;
//
getCaptchaCode();
});
} else {
await formEl.validate((valid, fields) => { await formEl.validate((valid, fields) => {
if (valid) { if (valid) {
CommonAPI.loginByPassword({ CommonAPI.loginByPassword({
username: ruleForm.username, username: ruleForm.username,
password: rsaEncrypt(ruleForm.password), password: ruleForm.password ? rsaEncrypt(ruleForm.password) : '',
captchaCode: ruleForm.captchaCode, captchaCode: ruleForm.captchaCode,
captchaCodeKey: ruleForm.captchaCodeKey captchaCodeKey: ruleForm.captchaCodeKey,
corpid: ruleForm.corpid,
code: ruleForm.code,
state: ruleForm.state
}) })
.then(({ data }) => { .then(({ data }) => {
// tokensessionStorage // tokensessionStorage
@ -105,6 +151,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
return fields; return fields;
} }
}); });
}
}; };
/** 使用公共函数,避免`removeEventListener`失效 */ /** 使用公共函数,避免`removeEventListener`失效 */
@ -147,8 +194,9 @@ onBeforeMount(async () => {
onMounted(() => { onMounted(() => {
window.document.addEventListener("keypress", onkeypress); window.document.addEventListener("keypress", onkeypress);
// //
ruleForm.password = "admin123" if (wxStore.code && wxStore.corpid) {
onLogin(ruleFormRef.value); onLogin(ruleFormRef.value);
}
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -0,0 +1,50 @@
<template>
<el-form ref="formRef" label-width="120px" :model="formInline" :disabled="true">
<el-form-item label="用户ID">
<el-input v-model="formInline.userId" clearable />
</el-form-item>
<el-form-item label="用户名">
<el-input v-model="formInline.username" />
</el-form-item>
<el-form-item label="用户昵称">
<el-input v-model="formInline.nickname" />
</el-form-item>
<el-form-item label="性别">
<el-select v-model="formInline.sex">
<el-option label="男" :value="1" />
<el-option label="女" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="手机号码">
<el-input v-model="formInline.phoneNumber" />
</el-form-item>
<el-form-item label="所属部门">
<el-input v-model="formInline.deptId" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="formInline.status" :active-value="1" :inactive-value="0" active-text="启用"
inactive-text="停用" />
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref } from "vue";
defineProps({
formInline: {
type: Object,
default: () => ({
userId: null,
username: "",
nickname: "",
sex: 1,
phoneNumber: "",
deptId: null,
status: 1
})
}
});
const formRef = ref();
</script>

View File

@ -0,0 +1,114 @@
import dayjs from "dayjs";
import { message } from "@/utils/message";
import {
QyUserQuery,
getQyUserListApi
} from "@/api/qy/qyUser";
import { ElMessageBox } from "element-plus";
import { type PaginationProps } from "@pureadmin/table";
import { reactive, ref, computed, onMounted, toRaw, h } from "vue";
import { CommonUtils } from "@/utils/common";
import { addDialog } from "@/components/ReDialog";
import userDetail from "./UserDetail.vue";
import { handleTree, setDisabledForTreeOptions } from "@/utils/tree";
import { getQyDeptListApi } from "@/api/system/dept";
import { getPostListApi } from "@/api/system/post";
import { getRoleListApi } from "@/api/system/role";
import { useWxStore } from "@/store/modules/wx";
export function useHook() {
const wxStore = useWxStore();
const searchFormParams = reactive<QyUserQuery>({
/** 姓名(导出列:姓名) */
name: undefined,
/** 手机号(导出列:联系方式) */
mobile: undefined,
corpid: wxStore.corpid,
mainDepartment: undefined,
});
const formRef = ref();
const timeRange = ref<[string, string]>();
const dataList = ref([]);
const pageLoading = ref(true);
const switchLoadMap = ref({});
const pagination = reactive<PaginationProps>({
total: 0,
pageSize: 8,
currentPage: 1,
background: true
});
const deptTreeList = ref([]);
const postOptions = ref([]);
const roleOptions = ref([]);
const buttonClass = computed(() => {
return [
"!h-[20px]",
"reset-margin",
"!text-gray-500",
"dark:!text-white",
"dark:hover:!text-primary"
];
});
async function onSearch() {
// 点击搜索的时候 需要重置分页
pagination.currentPage = 1;
getList();
}
async function getList() {
CommonUtils.fillPaginationParams(searchFormParams, pagination);
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
pageLoading.value = true;
const { data } = await getQyUserListApi(toRaw(searchFormParams)).finally(
() => {
pageLoading.value = false;
}
);
dataList.value = data.rows;
pagination.total = data.total;
}
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
onSearch();
};
onMounted(async () => {
onSearch();
const deptResponse = await getQyDeptListApi(wxStore.corpid);
deptTreeList.value = await setDisabledForTreeOptions(
handleTree(deptResponse.data),
"status"
);
const postResponse = await getPostListApi({});
postOptions.value = postResponse.data.rows;
const roleResponse = await getRoleListApi({});
roleOptions.value = roleResponse.data.rows;
});
const handleViewDetail = (row: any) => {
};
return {
searchFormParams,
pageLoading,
dataList,
pagination,
buttonClass,
onSearch,
resetForm,
getList,
handleViewDetail
};
}

View File

@ -0,0 +1,168 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import tree from "./tree.vue";
import { useHook } from "./hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import Password from "@iconify-icons/ri/lock-password-line";
import More from "@iconify-icons/ep/more-filled";
import Delete from "@iconify-icons/ep/delete";
import EditPen from "@iconify-icons/ep/edit-pen";
import Download from "@iconify-icons/ep/download";
import Upload from "@iconify-icons/ep/upload";
import Search from "@iconify-icons/ep/search";
import Refresh from "@iconify-icons/ep/refresh";
import AddFill from "@iconify-icons/ri/add-circle-line";
import { useUserStoreHook } from "@/store/modules/user";
defineOptions({
name: "QyUser"
});
const formRef = ref();
const {
searchFormParams,
pageLoading,
dataList,
pagination,
buttonClass,
onSearch,
resetForm,
getList,
handleViewDetail
} = useHook();
watch(
() => searchFormParams.mainDepartment,
() => {
onSearch();
}
);
</script>
<template>
<div class="main">
<tree class="w-[17%] float-left" v-model="searchFormParams.mainDepartment" />
<div class="float-right w-[82%]">
<el-form ref="formRef" :inline="true" :model="searchFormParams"
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]">
<el-form-item label="姓名:" prop="name">
<el-input v-model="searchFormParams.name" placeholder="请输入" clearable class="!w-[160px]" />
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch">
搜索
</el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<div class="grid-container">
<el-row :gutter="20">
<el-col v-for="(item, index) in dataList" :key="index" :xs="24" :sm="12" :md="8" :lg="6">
<el-card class="user-card">
<div class="card-content">
<el-avatar :size="60" :src="item.avatar" class="avatar" />
<div class="user-info">
<div class="name">{{ item.name }}</div>
<div class="gender">性别{{ item.gender === '1' ? '男' : item.gender === '2' ? '女' : '' }}</div>
<div class="create-time">创建时间{{ item.createTime }}</div>
</div>
</div>
<el-button type="primary" size="small" class="detail-btn" @click="handleViewDetail(item)">
查看详情
</el-button>
</el-card>
</el-col>
</el-row>
<div class="pagination-wrapper">
<el-pagination background layout="prev, pager, next" :page-size="pagination.pageSize"
:total="pagination.total" v-model:current-page="pagination.currentPage" @current-change="getList"
@size-change="getList" />
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
.user-card {
margin-bottom: 20px;
min-height: 180px;
display: flex;
flex-direction: column;
justify-content: space-between;
.card-content {
display: flex;
align-items: center;
margin-bottom: 15px;
.avatar {
margin-right: 15px;
flex-shrink: 0;
}
.user-info {
flex: 1;
.name {
font-size: 16px;
font-weight: 500;
margin-bottom: 6px;
}
.gender,
.create-time {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
}
}
.detail-btn {
width: 100%;
margin-top: auto;
}
}
.grid-container {
margin: 20px 0;
padding-bottom: 60px;
position: relative;
.el-row {
margin-bottom: -20px;
}
}
.pagination-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--el-bg-color);
padding: 12px 20px;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.05);
z-index: 10;
:deep(.el-pagination) {
margin: 0;
padding: 8px 0;
}
}
</style>

View File

@ -0,0 +1,210 @@
<script setup lang="ts">
import { handleTree } from "@/utils/tree";
import { getQyDeptListApi } from "@/api/system/dept";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, computed, watch, onMounted, getCurrentInstance } from "vue";
import Dept from "@iconify-icons/ri/git-branch-line";
import Reset from "@iconify-icons/ri/restart-line";
import Search from "@iconify-icons/ep/search";
import More2Fill from "@iconify-icons/ri/more-2-fill";
import OfficeBuilding from "@iconify-icons/ep/office-building";
import LocationCompany from "@iconify-icons/ep/add-location";
import ExpandIcon from "./svg/expand.svg?component";
import UnExpandIcon from "./svg/unexpand.svg?component";
import { useWxStore } from "@/store/modules/wx";
// TODO SideBar TreeSelect
interface Tree {
id: number;
deptName: string;
highlight?: boolean;
children?: Tree[];
}
defineProps({
modelValue: {
type: Number,
required: true
}
});
const wxStore = useWxStore();
const treeRef = ref();
const treeData = ref([]);
const isExpand = ref(true);
const searchValue = ref("");
const highlightMap = ref({});
const { proxy } = getCurrentInstance();
const defaultProps = {
children: "children",
label: "deptName"
};
const buttonClass = computed(() => {
return [
"!h-[20px]",
"reset-margin",
"!text-gray-500",
"dark:!text-white",
"dark:hover:!text-primary"
];
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return data.deptName.includes(value);
};
function nodeClick(value) {
console.log(value);
const nodeId = value.$treeNodeId;
console.log(nodeId);
highlightMap.value[nodeId] = highlightMap.value[nodeId]?.highlight
? Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
highlight: false
})
: Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
highlight: true
});
Object.values(highlightMap.value).forEach((v: Tree) => {
if (v.id !== nodeId) {
v.highlight = false;
}
});
proxy.$emit("update:modelValue", value.id);
}
function toggleRowExpansionAll(status) {
isExpand.value = status;
const nodes = (proxy.$refs["treeRef"] as any).store._getAllNodes();
for (let i = 0; i < nodes.length; i++) {
nodes[i].expanded = status;
}
}
/** 重置状态(选中状态、搜索框值、树初始化) */
function onReset() {
highlightMap.value = {};
searchValue.value = "";
toggleRowExpansionAll(true);
}
watch(searchValue, val => {
treeRef.value!.filter(val);
});
onMounted(async () => {
const { data } = await getQyDeptListApi(wxStore.corpid);
treeData.value = handleTree(data);
});
</script>
<template>
<div class="h-full bg-bg_color overflow-y-auto" :style="{ height: `calc(100vh - 133px)` }">
<!--<div class="flex items-center h-[56px]">
<p class="flex-1 ml-2 font-bold text-base truncate" title="部门列表">
部门列表
</p>
<el-input
style="flex: 2"
size="default"
v-model="searchValue"
placeholder="请输入部门名称"
clearable
>
<template #suffix>
<el-icon class="el-input__icon">
<IconifyIconOffline
v-show="searchValue.length === 0"
:icon="Search"
/>
</el-icon>
</template>
</el-input>
<el-dropdown :hide-on-click="false">
<IconifyIconOffline class="w-[38px] cursor-pointer" width="20px" :icon="More2Fill" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:icon="useRenderIcon(isExpand ? ExpandIcon : UnExpandIcon)"
@click="toggleRowExpansionAll(isExpand ? false : true)"
>
{{ isExpand ? "折叠全部" : "展开全部" }}
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:icon="useRenderIcon(Reset)"
@click="onReset"
>
重置状态
</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-divider />-->
<el-tree ref="treeRef" :data="treeData" node-key="id" size="default" :props="defaultProps" default-expand-all
:expand-on-click-node="false" :filter-node-method="filterNode" @node-click="nodeClick">
<template #default="{ node, data }">
<span :class="[
'text-base',
'flex',
'items-center',
'tracking-wider',
'gap-2',
'select-none',
searchValue.trim().length > 0 &&
node.label.includes(searchValue) &&
'text-red-500',
highlightMap[node.id]?.highlight ? 'dark:text-primary' : ''
]" :style="{
background: highlightMap[node.id]?.highlight
? 'var(--el-color-primary-light-7)'
: 'transparent'
}">
<!-- <IconifyIconOffline :icon="data.parentId === 0
? OfficeBuilding
: data.type === 2
? LocationCompany
: Dept
" /> -->
{{ node.label }}
</span>
</template>
</el-tree>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-divider) {
margin: 0;
}
:deep(.el-tree) {
height: 100%;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
</style>

View File

@ -21,9 +21,10 @@ import { reactive, ref, computed, onMounted, toRaw, h } from "vue";
import { CommonUtils } from "@/utils/common"; import { CommonUtils } from "@/utils/common";
import { addDialog } from "@/components/ReDialog"; import { addDialog } from "@/components/ReDialog";
import { handleTree, setDisabledForTreeOptions } from "@/utils/tree"; import { handleTree, setDisabledForTreeOptions } from "@/utils/tree";
import { getDeptListApi } from "@/api/system/dept"; import { getQyDeptListApi } from "@/api/system/dept";
import { getPostListApi } from "@/api/system/post"; import { getPostListApi } from "@/api/system/post";
import { getRoleListApi } from "@/api/system/role"; import { getRoleListApi } from "@/api/system/role";
import { useWxStore } from "@/store/modules/wx";
export function useHook() { export function useHook() {
const searchFormParams = reactive<UserQuery>({ const searchFormParams = reactive<UserQuery>({
@ -31,6 +32,7 @@ export function useHook() {
phoneNumber: undefined, phoneNumber: undefined,
status: undefined, status: undefined,
username: undefined, username: undefined,
nickname: undefined,
timeRangeColumn: "createTime" timeRangeColumn: "createTime"
}); });
@ -51,6 +53,8 @@ export function useHook() {
const postOptions = ref([]); const postOptions = ref([]);
const roleOptions = ref([]); const roleOptions = ref([]);
const wxStore = useWxStore();
const columns: TableColumnList = [ const columns: TableColumnList = [
{ {
label: "用户编号", label: "用户编号",
@ -153,10 +157,8 @@ export function useHook() {
function onChange({ row, index }) { function onChange({ row, index }) {
ElMessageBox.confirm( ElMessageBox.confirm(
`确认要<strong>${ `确认要<strong>${row.status === 0 ? "停用" : "启用"
row.status === 0 ? "停用" : "启用" }</strong><strong style='color:var(--el-color-primary)'>${row.username
}</strong><strong style='color:var(--el-color-primary)'>${
row.username
}</strong>?`, }</strong>?`,
"系统提示", "系统提示",
{ {
@ -356,7 +358,7 @@ export function useHook() {
onMounted(async () => { onMounted(async () => {
onSearch(); onSearch();
const deptResponse = await getDeptListApi(); const deptResponse = await getQyDeptListApi(wxStore.corpid);
deptTreeList.value = await setDisabledForTreeOptions( deptTreeList.value = await setDisabledForTreeOptions(
handleTree(deptResponse.data), handleTree(deptResponse.data),
"status" "status"

View File

@ -50,13 +50,12 @@ watch(
<div class="main"> <div class="main">
<tree class="w-[17%] float-left" v-model="searchFormParams.deptId" /> <tree class="w-[17%] float-left" v-model="searchFormParams.deptId" />
<div class="float-right w-[82%]"> <div class="float-right w-[82%]">
<el-form <el-form ref="formRef" :inline="true" :model="searchFormParams"
ref="formRef" class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]">
:inline="true" <el-form-item label="姓名:" prop="nickname">
:model="searchFormParams" <el-input v-model="searchFormParams.nickname" placeholder="请输入" clearable class="!w-[160px]" />
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]" </el-form-item>
> <!-- <el-form-item label="用户编号:" prop="userId">
<el-form-item label="用户编号:" prop="userId">
<el-input <el-input
v-model="searchFormParams.userId" v-model="searchFormParams.userId"
placeholder="请输入用户编号" placeholder="请输入用户编号"
@ -94,14 +93,9 @@ watch(
:value="dict.value" :value="dict.value"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item> -->
<el-form-item> <el-form-item>
<el-button <el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch">
type="primary"
:icon="useRenderIcon(Search)"
:loading="pageLoading"
@click="onSearch"
>
搜索 搜索
</el-button> </el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)"> <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
@ -112,90 +106,42 @@ watch(
<PureTableBar title="用户管理" :columns="columns" @refresh="onSearch"> <PureTableBar title="用户管理" :columns="columns" @refresh="onSearch">
<template #buttons> <template #buttons>
<el-button <el-button type="primary" :icon="useRenderIcon(AddFill)" @click="openDialog('新增')">
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增')"
>
新增用户 新增用户
</el-button> </el-button>
<el-button <el-button type="info" :icon="useRenderIcon(Upload)" @click="openUploadDialog">
type="info"
:icon="useRenderIcon(Upload)"
@click="openUploadDialog"
>
导入 导入
</el-button> </el-button>
<el-button <el-button type="warning" :icon="useRenderIcon(Download)" @click="exportAllExcel">
type="warning"
:icon="useRenderIcon(Download)"
@click="exportAllExcel"
>
导出 导出
</el-button> </el-button>
</template> </template>
<template v-slot="{ size, dynamicColumns }"> <template v-slot="{ size, dynamicColumns }">
<pure-table <pure-table border adaptive align-whole="center" table-layout="auto" :loading="pageLoading" :size="size"
border :data="dataList" :columns="dynamicColumns" :pagination="pagination"
adaptive :paginationSmall="size === 'small' ? true : false" :header-cell-style="{
align-whole="center"
table-layout="auto"
:loading="pageLoading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:pagination="pagination"
:paginationSmall="size === 'small' ? true : false"
:header-cell-style="{
background: 'var(--el-table-row-hover-bg-color)', background: 'var(--el-table-row-hover-bg-color)',
color: 'var(--el-text-color-primary)' color: 'var(--el-text-color-primary)'
}" }" @page-size-change="getList" @page-current-change="getList">
@page-size-change="getList"
@page-current-change="getList"
>
<template #operation="{ row }"> <template #operation="{ row }">
<el-button <el-button class="reset-margin" link type="primary" :size="size" @click="openDialog('编辑', row)"
class="reset-margin" :icon="useRenderIcon(EditPen)">
link
type="primary"
:size="size"
@click="openDialog('编辑', row)"
:icon="useRenderIcon(EditPen)"
>
修改 修改
</el-button> </el-button>
<el-popconfirm title="是否确认删除?" @confirm="handleDelete(row)"> <el-popconfirm title="是否确认删除?" @confirm="handleDelete(row)">
<template #reference> <template #reference>
<el-button <el-button class="reset-margin" link type="primary" :size="size" :icon="useRenderIcon(Delete)">
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除 删除
</el-button> </el-button>
</template> </template>
</el-popconfirm> </el-popconfirm>
<el-dropdown> <el-dropdown>
<el-button <el-button class="ml-3 mt-[2px]" link type="primary" :size="size" :icon="useRenderIcon(More)" />
class="ml-3 mt-[2px]"
link
type="primary"
:size="size"
:icon="useRenderIcon(More)"
/>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item> <el-dropdown-item>
<el-button <el-button :class="buttonClass" link type="primary" :size="size" :icon="useRenderIcon(Password)"
:class="buttonClass" @click="openResetPasswordDialog(row)">
link
type="primary"
:size="size"
:icon="useRenderIcon(Password)"
@click="openResetPasswordDialog(row)"
>
重置密码 重置密码
</el-button> </el-button>
</el-dropdown-item> </el-dropdown-item>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { handleTree } from "@/utils/tree"; import { handleTree } from "@/utils/tree";
import { getDeptListApi } from "@/api/system/dept"; import { getQyDeptListApi } from "@/api/system/dept";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, computed, watch, onMounted, getCurrentInstance } from "vue"; import { ref, computed, watch, onMounted, getCurrentInstance } from "vue";
@ -12,6 +12,7 @@ import OfficeBuilding from "@iconify-icons/ep/office-building";
import LocationCompany from "@iconify-icons/ep/add-location"; import LocationCompany from "@iconify-icons/ep/add-location";
import ExpandIcon from "./svg/expand.svg?component"; import ExpandIcon from "./svg/expand.svg?component";
import UnExpandIcon from "./svg/unexpand.svg?component"; import UnExpandIcon from "./svg/unexpand.svg?component";
import { useWxStore } from "@/store/modules/wx";
// TODO SideBar TreeSelect // TODO SideBar TreeSelect
interface Tree { interface Tree {
@ -28,6 +29,7 @@ defineProps({
} }
}); });
const wxStore = useWxStore();
const treeRef = ref(); const treeRef = ref();
const treeData = ref([]); const treeData = ref([]);
const isExpand = ref(true); const isExpand = ref(true);
@ -93,17 +95,14 @@ watch(searchValue, val => {
}); });
onMounted(async () => { onMounted(async () => {
const { data } = await getDeptListApi(); const { data } = await getQyDeptListApi(wxStore.corpid);
treeData.value = handleTree(data); treeData.value = handleTree(data);
}); });
</script> </script>
<template> <template>
<div <div class="h-full bg-bg_color overflow-y-auto" :style="{ height: `calc(100vh - 133px)` }">
class="h-full bg-bg_color overflow-auto" <!--<div class="flex items-center h-[56px]">
:style="{ minHeight: `calc(100vh - 133px)` }"
>
<div class="flex items-center h-[56px]">
<p class="flex-1 ml-2 font-bold text-base truncate" title="部门列表"> <p class="flex-1 ml-2 font-bold text-base truncate" title="部门列表">
部门列表 部门列表
</p> </p>
@ -124,11 +123,7 @@ onMounted(async () => {
</template> </template>
</el-input> </el-input>
<el-dropdown :hide-on-click="false"> <el-dropdown :hide-on-click="false">
<IconifyIconOffline <IconifyIconOffline class="w-[38px] cursor-pointer" width="20px" :icon="More2Fill" />
class="w-[38px] cursor-pointer"
width="20px"
:icon="More2Fill"
/>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item> <el-dropdown-item>
@ -157,21 +152,11 @@ onMounted(async () => {
</template> </template>
</el-dropdown> </el-dropdown>
</div> </div>
<el-divider /> <el-divider />-->
<el-tree <el-tree ref="treeRef" :data="treeData" node-key="id" size="default" :props="defaultProps" default-expand-all
ref="treeRef" :expand-on-click-node="false" :filter-node-method="filterNode" @node-click="nodeClick">
:data="treeData"
node-key="id"
size="default"
:props="defaultProps"
default-expand-all
:expand-on-click-node="false"
:filter-node-method="filterNode"
@node-click="nodeClick"
>
<template #default="{ node, data }"> <template #default="{ node, data }">
<span <span :class="[
:class="[
'text-base', 'text-base',
'flex', 'flex',
'items-center', 'items-center',
@ -182,22 +167,17 @@ onMounted(async () => {
node.label.includes(searchValue) && node.label.includes(searchValue) &&
'text-red-500', 'text-red-500',
highlightMap[node.id]?.highlight ? 'dark:text-primary' : '' highlightMap[node.id]?.highlight ? 'dark:text-primary' : ''
]" ]" :style="{
:style="{
background: highlightMap[node.id]?.highlight background: highlightMap[node.id]?.highlight
? 'var(--el-color-primary-light-7)' ? 'var(--el-color-primary-light-7)'
: 'transparent' : 'transparent'
}" }">
> <!-- <IconifyIconOffline :icon="data.parentId === 0
<IconifyIconOffline
:icon="
data.parentId === 0
? OfficeBuilding ? OfficeBuilding
: data.type === 2 : data.type === 2
? LocationCompany ? LocationCompany
: Dept : Dept
" " /> -->
/>
{{ node.label }} {{ node.label }}
</span> </span>
</template> </template>
@ -209,4 +189,22 @@ onMounted(async () => {
:deep(.el-divider) { :deep(.el-divider) {
margin: 0; margin: 0;
} }
:deep(.el-tree) {
height: 100%;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.5);
}
</style> </style>