feat: initial commit

This commit is contained in:
gin
2026-05-07 18:39:00 +08:00
commit cdee21ee8e
653 changed files with 63946 additions and 0 deletions
+209
View File
@@ -0,0 +1,209 @@
<script setup lang="ts">
import { ref } from "vue";
import ReCol from "@/components/ReCol";
import { formRules } from "./rule";
import { UserRequest } from "@/api/system/user";
import { PostPageResponse } from "@/api/system/post";
import { RoleDTO } from "@/api/system/role";
import { useUserStoreHook } from "@/store/modules/user";
interface FormProps {
formInline: UserRequest;
deptOptions: any[];
postOptions: PostPageResponse[];
roleOptions: RoleDTO[];
}
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
userId: 0,
username: "",
nickname: "",
deptId: 0,
phone: "",
email: "",
password: "",
sex: 0,
status: 1,
postId: 0,
roleId: 0,
remark: ""
}),
deptOptions: () => [],
postOptions: () => [],
roleOptions: () => []
});
const newFormInline = ref(props.formInline);
const deptOptions = ref(props.deptOptions);
const roleOptions = ref(props.roleOptions);
const postOptions = ref(props.postOptions);
const formRuleRef = ref();
function getFormRuleRef() {
return formRuleRef.value;
}
defineExpose({ getFormRuleRef });
</script>
<template>
<el-form
ref="formRuleRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<el-row :gutter="30">
<re-col :value="12">
<el-form-item label="用户名" prop="username">
<el-input
v-model="newFormInline.username"
clearable
placeholder="请输入用户名"
/>
</el-form-item>
</re-col>
<re-col :value="12">
<el-form-item label="部门">
<el-tree-select
class="w-full"
v-model="newFormInline.deptId"
:data="deptOptions"
:show-all-levels="false"
value-key="id"
:props="{
value: 'id',
label: 'deptName',
emitPath: false,
checkStrictly: true
}"
clearable
placeholder="请选择部门"
/>
</el-form-item>
</re-col>
<re-col :value="12">
<el-form-item label="手机号码" prop="phoneNumber">
<el-input
v-model="newFormInline.phoneNumber"
clearable
placeholder="请输入手机号码"
/>
</el-form-item>
</re-col>
<re-col :value="12">
<el-form-item label="邮箱" prop="email">
<el-input
v-model="newFormInline.email"
clearable
placeholder="请输入邮箱"
/>
</el-form-item>
</re-col>
<re-col :value="12">
<el-form-item label="昵称" prop="nickname">
<el-input
v-model="newFormInline.nickname"
clearable
placeholder="请输入昵称"
/>
</el-form-item>
</re-col>
<re-col :value="12">
<el-form-item label="性别" prop="sex">
<el-select
class="w-full"
v-model="newFormInline.sex"
placeholder="请选择性别"
clearable
>
<el-option
v-for="dict in useUserStoreHook().dictionaryList['sysUser.sex']"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</re-col>
<re-col :value="12">
<el-form-item label="岗位" prop="postId">
<el-select
class="w-full"
v-model="newFormInline.postId"
placeholder="请选择岗位"
clearable
>
<el-option
v-for="item in postOptions"
:key="item.postId"
:label="item.postName"
:value="item.postId"
:disabled="item.status == 0"
/>
</el-select>
</el-form-item>
</re-col>
<re-col :value="12">
<el-form-item label="角色" prop="roleId">
<el-select
class="w-full"
v-model="newFormInline.roleId"
placeholder="请选择角色"
clearable
>
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
:disabled="item.status == 0"
/>
</el-select>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="newFormInline.status">
<el-radio
v-for="item in useUserStoreHook().dictionaryList['common.status']"
:key="item.value"
:label="item.value"
>{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</re-col>
<re-col :value="12" v-if="newFormInline.password !== undefined">
<el-form-item label="密码" prop="password">
<el-input
v-model="newFormInline.password"
clearable
placeholder="请输入密码"
/>
</el-form-item>
</re-col>
<re-col :value="24">
<el-form-item label="备注" prop="remark">
<el-input
v-model="newFormInline.remark"
clearable
placeholder="请输入备注内容"
rows="6"
type="textarea"
/>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>
+389
View File
@@ -0,0 +1,389 @@
import dayjs from "dayjs";
import { message } from "@/utils/message";
import {
UserQuery,
getUserListApi,
addUserApi,
updateUserStatusApi,
updateUserApi,
exportUserExcelApi,
UserRequest,
deleteUserApi,
PasswordRequest,
updateUserPasswordApi
} from "@/api/system/user";
import editForm from "./form.vue";
import passwordForm from "./passwordForm.vue";
import uploadForm from "./uploadForm.vue";
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 { handleTree, setDisabledForTreeOptions } from "@/utils/tree";
import { getDeptListApi } from "@/api/system/dept";
import { getPostListApi } from "@/api/system/post";
import { getRoleListApi } from "@/api/system/role";
export function useHook() {
const searchFormParams = reactive<UserQuery>({
deptId: null,
phoneNumber: undefined,
status: undefined,
username: undefined,
timeRangeColumn: "createTime"
});
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: 10,
currentPage: 1,
background: true
});
const deptTreeList = ref([]);
const postOptions = ref([]);
const roleOptions = ref([]);
const columns: TableColumnList = [
{
label: "用户编号",
prop: "userId",
width: 90,
fixed: "left"
},
{
label: "用户名",
prop: "username",
minWidth: 130
},
{
label: "昵称",
prop: "nickname",
minWidth: 130
},
{
label: "性别",
prop: "sex",
minWidth: 90,
cellRenderer: ({ row, props }) => (
<el-tag
size={props.size}
type={row.sex === 1 ? "" : "danger"}
effect="plain"
>
{row.sex === 1 ? "男" : "女"}
</el-tag>
)
},
{
label: "部门ID",
prop: "deptId",
minWidth: 130,
hide: true
},
{
label: "部门",
prop: "deptName",
minWidth: 130
},
{
label: "手机号码",
prop: "phoneNumber",
minWidth: 90
},
{
label: "角色ID",
prop: "roleId",
minWidth: 90,
hide: true
},
{
label: "角色",
prop: "roleName",
minWidth: 90
},
{
label: "状态",
prop: "status",
minWidth: 90,
cellRenderer: scope => (
<el-switch
size={scope.props.size === "small" ? "small" : "default"}
loading={switchLoadMap.value[scope.index]?.loading}
v-model={scope.row.status}
active-value={1}
inactive-value={0}
active-text="正常"
inactive-text="停用"
inline-prompt
onChange={() => onChange(scope as any)}
/>
)
},
{
label: "创建时间",
minWidth: 70,
prop: "createTime",
formatter: ({ createTime }) =>
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
},
{
label: "操作",
fixed: "right",
width: 180,
slot: "operation"
}
];
const buttonClass = computed(() => {
return [
"!h-[20px]",
"reset-margin",
"!text-gray-500",
"dark:!text-white",
"dark:hover:!text-primary"
];
});
function onChange({ row, index }) {
ElMessageBox.confirm(
`确认要<strong>${
row.status === 0 ? "停用" : "启用"
}</strong><strong style='color:var(--el-color-primary)'>${
row.username
}</strong>用户吗?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
draggable: true
}
)
.then(async () => {
switchLoading(index, true);
await updateUserStatusApi(row.userId, row.status).finally(() => {
switchLoading(index, false);
});
message("已成功修改用户状态", {
type: "success"
});
})
.catch(() => {
message("取消操作", {
type: "info"
});
// 如果取消的话 恢复更改前的状态
row.status === 0 ? (row.status = 1) : (row.status = 0);
});
}
function switchLoading(index: number, loading: boolean) {
switchLoadMap.value[index] = Object.assign({}, switchLoadMap.value[index], {
loading: loading
});
}
async function exportAllExcel() {
CommonUtils.fillPaginationParams(searchFormParams, pagination);
exportUserExcelApi(toRaw(searchFormParams), "用户列表.xls");
}
async function handleAdd(row, done) {
await addUserApi(row as UserRequest).then(() => {
message(`您新增了用户${row.username}的这条数据`, {
type: "success"
});
// 关闭弹框
done();
// 刷新列表
getList();
});
}
async function handleUpdate(row, done) {
await updateUserApi(row.userId, row as UserRequest).then(() => {
message(`您修改了用户${row.username}的这条数据`, {
type: "success"
});
// 关闭弹框
done();
// 刷新列表
getList();
});
}
async function handleDelete(row) {
await deleteUserApi(row.userId).then(() => {
message(`您删除了用户${row.username}的这条数据`, { type: "success" });
// 刷新列表
getList();
});
}
async function handleResetPassword(row, request, done) {
await updateUserPasswordApi(request).then(() => {
message(`您修改了用户${row.username}的密码`, { type: "success" });
// 刷新列表
done();
getList();
});
}
async function onSearch() {
// 点击搜索的时候 需要重置分页
pagination.currentPage = 1;
getList();
}
async function openDialog(title = "新增", row?: UserRequest) {
// TODO 如果是编辑的话 通过获取用户详情接口来获取数据
addDialog({
title: `${title}用户`,
props: {
formInline: {
userId: row?.userId ?? 0,
username: row?.username ?? "",
nickname: row?.nickname ?? "",
deptId: row?.deptId ?? undefined,
phoneNumber: row?.phoneNumber ?? "",
email: row?.email ?? "",
password: title == "新增" ? "" : undefined,
sex: row?.sex ?? undefined,
status: row?.status ?? undefined,
postId: row?.postId ?? undefined,
roleId: row?.roleId ?? undefined,
remark: row?.remark ?? ""
},
deptOptions: deptTreeList,
postOptions: postOptions,
roleOptions: roleOptions
},
width: "40%",
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef }),
beforeSure: (done, { options }) => {
const formRuleRef = formRef.value.getFormRuleRef();
const curData = options.props.formInline as UserRequest;
formRuleRef.validate(valid => {
if (valid) {
// 表单规则校验通过
if (title === "新增") {
handleAdd(curData, done);
} else {
handleUpdate(curData, done);
}
}
});
}
});
}
async function openResetPasswordDialog(row) {
const passwordFormRef = ref();
addDialog({
title: `重置密码`,
props: {
formInline: {
userId: row.userId ?? 0,
password: ""
}
},
width: "30%",
closeOnClickModal: false,
contentRenderer: () => h(passwordForm, { ref: passwordFormRef }),
beforeSure: (done, { options }) => {
const formRef = passwordFormRef.value.getFormRuleRef();
const curData = options.props.formInline as PasswordRequest;
formRef.validate(valid => {
if (valid) {
handleResetPassword(row, curData, done);
}
});
}
});
}
async function openUploadDialog() {
const uploadFormRef = ref();
addDialog({
title: `导入用户`,
props: {},
width: "30%",
closeOnClickModal: false,
contentRenderer: () => h(uploadForm, { ref: uploadFormRef }),
beforeSure: done => {
console.log("上传文件");
uploadFormRef.value.getFormRef().submit();
done();
getList();
}
});
}
async function getList() {
CommonUtils.fillPaginationParams(searchFormParams, pagination);
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
pageLoading.value = true;
const { data } = await getUserListApi(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 getDeptListApi();
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;
});
return {
searchFormParams,
pageLoading,
columns,
dataList,
pagination,
buttonClass,
onSearch,
openDialog,
exportAllExcel,
resetForm,
handleUpdate,
getList,
handleDelete,
openResetPasswordDialog,
openUploadDialog
};
}
@@ -0,0 +1,223 @@
<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: "SystemUser"
});
const formRef = ref();
const {
searchFormParams,
pageLoading,
columns,
dataList,
pagination,
buttonClass,
onSearch,
resetForm,
exportAllExcel,
openResetPasswordDialog,
handleDelete,
openDialog,
getList,
openUploadDialog
} = useHook();
watch(
() => searchFormParams.deptId,
() => {
onSearch();
}
);
</script>
<template>
<div class="main">
<tree class="w-[17%] float-left" v-model="searchFormParams.deptId" />
<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="userId">
<el-input
v-model="searchFormParams.userId"
placeholder="请输入用户编号"
clearable
class="!w-[160px]"
/>
</el-form-item>
<el-form-item label="用户名称:" prop="username">
<el-input
v-model="searchFormParams.username"
placeholder="请输入用户名称"
clearable
class="!w-[160px]"
/>
</el-form-item>
<el-form-item label="手机号码:" prop="phoneNumber">
<el-input
v-model="searchFormParams.phoneNumber"
placeholder="请输入手机号码"
clearable
class="!w-[160px]"
/>
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-select
v-model="searchFormParams.status"
placeholder="请选择"
clearable
class="!w-[160px]"
>
<el-option
v-for="dict in useUserStoreHook().dictionaryList['common.status']"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</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>
<PureTableBar title="用户管理" :columns="columns" @refresh="onSearch">
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog('新增')"
>
新增用户
</el-button>
<el-button
type="info"
:icon="useRenderIcon(Upload)"
@click="openUploadDialog"
>
导入
</el-button>
<el-button
type="warning"
:icon="useRenderIcon(Download)"
@click="exportAllExcel"
>
导出
</el-button>
</template>
<template v-slot="{ size, dynamicColumns }">
<pure-table
border
adaptive
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)',
color: 'var(--el-text-color-primary)'
}"
@page-size-change="getList"
@page-current-change="getList"
>
<template #operation="{ row }">
<el-button
class="reset-margin"
link
type="primary"
:size="size"
@click="openDialog('编辑', row)"
:icon="useRenderIcon(EditPen)"
>
修改
</el-button>
<el-popconfirm title="是否确认删除?" @confirm="handleDelete(row)">
<template #reference>
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
<el-dropdown>
<el-button
class="ml-3 mt-[2px]"
link
type="primary"
:size="size"
:icon="useRenderIcon(More)"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Password)"
@click="openResetPasswordDialog(row)"
>
重置密码
</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref } from "vue";
import ReCol from "@/components/ReCol";
import { formRules } from "./rule";
import { PasswordRequest } from "@/api/system/user";
interface FormProps {
formInline: PasswordRequest;
}
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
userId: 0,
password: ""
})
});
const newFormInline = ref(props.formInline);
const formRuleRef = ref();
function getFormRuleRef() {
return formRuleRef.value;
}
defineExpose({ getFormRuleRef });
</script>
<template>
<el-form
ref="formRuleRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<el-row :gutter="30">
<re-col :value="24">
<el-form-item label="新密码" prop="password">
<el-input
v-model="newFormInline.password"
clearable
placeholder="请输入新密码"
/>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>
@@ -0,0 +1,99 @@
<script setup lang="ts">
import resetPwd from "./resetPwd.vue";
import userInfo from "./userInfo.vue";
import userAvatar from "./userAvatar.vue";
// import userAvatar from "./userAvatar";
// import { getUserProfile } from '@/api/system/user';
// import * as userApi from "@/api/system/userApi";
import { reactive, ref } from "vue";
import dayjs from "dayjs";
import { useUserStoreHook } from "@/store/modules/user";
const activeTab = ref("userinfo");
const state = reactive({
user: {},
roleName: {},
postName: {}
});
/** 用户名 */
const currentUserInfo = useUserStoreHook()?.currentUserInfo;
state.user = currentUserInfo;
console.log(currentUserInfo);
function getUser() {
// userApi.getUserProfile().then(response => {
// state.user = response.user;
// state.roleName = response.roleName;
// state.postName = response.postName;
// });
}
getUser();
</script>
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="6" :xs="24">
<el-card class="box-card">
<template v-slot:header>
<div class="clearfix">
<span>个人信息</span>
</div>
</template>
<div>
<div class="text-center">
<userAvatar :user="state.user" />
</div>
<el-row>
<el-descriptions :column="1">
<el-descriptions-item label="用户名称">{{
currentUserInfo.username
}}</el-descriptions-item>
<el-descriptions-item label="手机号码">{{
currentUserInfo.phoneNumber
}}</el-descriptions-item>
<el-descriptions-item label="用户邮箱">{{
currentUserInfo.email
}}</el-descriptions-item>
<el-descriptions-item label="部门 / 职位">
{{ currentUserInfo.deptName }} /
{{ currentUserInfo.postName }}
</el-descriptions-item>
<el-descriptions-item label="角色">
{{ currentUserInfo.roleName }}
</el-descriptions-item>
<el-descriptions-item label="创建日期">
{{
dayjs(currentUserInfo.createTime).format(
"YYYY-MM-DD HH:mm:ss"
)
}}
</el-descriptions-item>
</el-descriptions>
</el-row>
</div>
</el-card>
</el-col>
<el-col :span="18" :xs="24">
<el-card>
<template v-slot:header>
<div class="clearfix">
<span>基本资料</span>
</div>
</template>
<el-tabs v-model="activeTab">
<el-tab-pane label="基本资料" name="userinfo">
<userInfo :user="state.user" />
</el-tab-pane>
<el-tab-pane label="修改密码" name="resetPwd">
<resetPwd :user="state.user" />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</div>
</template>
@@ -0,0 +1,89 @@
<script setup lang="ts">
import { reactive, ref, toRaw } from "vue";
import {
updateCurrentUserPasswordApi,
ResetPasswordRequest
} from "@/api/system/user";
import { FormInstance } from "element-plus";
import { message } from "@/utils/message";
// const { proxy } = getCurrentInstance();
const user = reactive<ResetPasswordRequest>({
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
});
const pwdRef = ref<FormInstance>();
const equalToPassword = (rule, value, callback) => {
if (user.newPassword !== value) {
callback(new Error("两次输入的密码不一致"));
} else {
callback();
}
};
const rules = ref({
oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
newPassword: [
{ required: true, message: "新密码不能为空", trigger: "blur" },
{
pattern:
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/,
message: "新密码格式应为8-18位数字、字母、符号的任意两种组合",
trigger: "blur"
}
],
confirmPassword: [
{ required: true, message: "确认密码不能为空", trigger: "blur" },
{ required: true, validator: equalToPassword, trigger: "blur" }
]
});
/** 提交按钮 */
function submit() {
console.log(user);
pwdRef.value.validate(valid => {
if (valid) {
updateCurrentUserPasswordApi(toRaw(user)).then(() => {
message("修改成功", {
type: "success"
});
});
}
});
}
</script>
<template>
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input
v-model="user.oldPassword"
placeholder="请输入旧密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
v-model="user.newPassword"
placeholder="请输入新密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="user.confirmPassword"
placeholder="请确认密码"
type="password"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">保存</el-button>
</el-form-item>
</el-form>
</template>
@@ -0,0 +1,136 @@
<script setup lang="ts">
import ReCropper from "@/components/ReCropper";
import { formatBytes } from "@pureadmin/utils";
import { ref } from "vue";
import { uploadUserAvatarApi } from "@/api/system/user";
import { useUserStoreHook } from "@/store/modules/user";
// import * as userApi from "@/api/system/userApi";
import { message } from "@/utils/message";
const currentUser = useUserStoreHook().currentUserInfo;
const infos = ref();
const imgBlob = ref();
const refCropper = ref();
const showPopover = ref(false);
const cropperImg = ref<string>("");
cropperImg.value = import.meta.env.VITE_APP_BASE_API + currentUser.avatar;
function onCropper({ base64, blob, info }) {
console.log(blob);
infos.value = info;
imgBlob.value = blob;
cropperImg.value = base64;
}
const open = ref(false);
const visible = ref(false);
// 图片裁剪数据
// const options = reactive({
// img: avatarUrl, // 裁剪图片的地址
// autoCrop: true, // 是否默认生成截图框
// autoCropWidth: 200, // 默认生成截图框宽度
// autoCropHeight: 200, // 默认生成截图框高度
// fixedBox: true, // 固定截图框大小 不允许改变
// previews: {} // 预览数据
// });
/** 上传图片 */
function uploadImg() {
const formData = new FormData();
formData.append("avatarfile", imgBlob.value);
uploadUserAvatarApi(formData).then(() => {
open.value = false;
message("上传图片成功", {
type: "success"
});
visible.value = false;
});
}
</script>
<template>
<div class="user-info-head" @click="open = true">
<el-avatar :size="120" :src="cropperImg" />
</div>
<el-dialog
title="修改头像"
v-model="open"
width="900px"
append-to-body
@opened="visible = true"
@close="visible = false"
>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="font-medium"> 右键下面左侧裁剪区开启功能菜单 </span>
</div>
</template>
<el-popover
:visible="showPopover"
placement="right"
width="300px"
:teleported="false"
>
<template #reference>
<ReCropper
ref="refCropper"
class="w-[500px]"
:src="cropperImg"
circled
@cropper="onCropper"
@readied="showPopover = true"
/>
</template>
<div class="flex flex-wrap justify-center items-center text-center">
<el-image
v-if="cropperImg"
:src="cropperImg"
:preview-src-list="Array.of(cropperImg)"
fit="cover"
/>
<div v-if="infos" class="mt-1">
<p>
图像大小{{ parseInt(infos.width) }} ×
{{ parseInt(infos.height) }}像素
</p>
<p>
文件大小{{ formatBytes(infos.size) }}{{ infos.size }} 字节
</p>
</div>
</div>
</el-popover>
</el-card>
<template #footer>
<div>
<el-button @click="open = false">取消</el-button>
<el-button type="primary" @click="uploadImg">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.user-info-head {
position: relative;
display: inline-block;
height: 120px;
}
.user-info-head:hover::after {
position: absolute;
inset: 0;
font-size: 24px;
font-style: normal;
line-height: 110px;
color: #eee;
cursor: pointer;
content: "+";
background: rgb(0 0 0 / 50%);
border-radius: 50%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
@@ -0,0 +1,89 @@
<script setup lang="ts">
// import { updateUserProfile } from '@/api/system/userApi';
// import * as userApi from "@/api/system/userApi";
import { ref, reactive } from "vue";
import { updateUserProfileApi, UserProfileRequest } from "@/api/system/user";
import { message } from "@/utils/message";
import { FormInstance } from "element-plus";
defineOptions({
name: "SystemUserProfile"
});
const userRef = ref<FormInstance>();
const props = defineProps({
user: {
type: Object
}
});
const userModel = reactive<UserProfileRequest>({
nickname: props.user.nickname,
phoneNumber: props.user.phoneNumber,
email: props.user.email,
sex: props.user.sex
});
console.log(userModel);
console.log(props.user);
// const { proxy } = getCurrentInstance();
const rules = ref({
nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
email: [
{ required: true, message: "邮箱地址不能为空", trigger: "blur" },
{
type: "email",
message: "请输入正确的邮箱地址",
trigger: ["blur", "change"]
}
],
phoneNumber: [
{ required: true, message: "手机号码不能为空", trigger: "blur" },
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur"
}
]
});
/** 提交按钮 */
function submit() {
console.log(userRef.value);
userRef.value.validate(valid => {
if (valid) {
updateUserProfileApi(userModel).then(() => {
message("修改成功", {
type: "success"
});
});
}
});
}
</script>
<template>
<el-form ref="userRef" :model="userModel" :rules="rules" label-width="80px">
<el-form-item label="用户昵称">
<el-input v-model="userModel.nickname" maxlength="30" />
</el-form-item>
<el-form-item label="手机号码">
<el-input v-model="userModel.phoneNumber" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="userModel.email" maxlength="50" />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="userModel.sex">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">保存</el-button>
</el-form-item>
</el-form>
</template>
@@ -0,0 +1,37 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
import { isPhone, isEmail } from "@pureadmin/utils";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
name: [{ required: true, message: "部门名称为必填项", trigger: "blur" }],
phone: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback();
} else if (!isPhone(value)) {
callback(new Error("请输入正确的手机号码格式"));
} else {
callback();
}
},
trigger: "blur"
// trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
}
],
email: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback();
} else if (!isEmail(value)) {
callback(new Error("请输入正确的邮箱格式"));
} else {
callback();
}
},
trigger: "blur"
}
]
});
@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4h9Z"/></svg>

After

Width:  |  Height:  |  Size: 163 B

@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 2H2v20h2v-9h14.17l-5.5 5.5l1.41 1.42L22 12l-7.92-7.92l-1.41 1.42l5.5 5.5H4V2Z"/></svg>

After

Width:  |  Height:  |  Size: 166 B

+212
View File
@@ -0,0 +1,212 @@
<script setup lang="ts">
import { handleTree } from "@/utils/tree";
import { getDeptListApi } 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";
// TODO 这个类可以抽取作为SideBar TreeSelect组件
interface Tree {
id: number;
deptName: string;
highlight?: boolean;
children?: Tree[];
}
defineProps({
modelValue: {
type: Number,
required: true
}
});
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 getDeptListApi();
treeData.value = handleTree(data);
});
</script>
<template>
<div
class="h-full bg-bg_color overflow-auto"
: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>
<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;
}
</style>
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { getToken } from "@/utils/auth";
import { http } from "@/utils/http";
import { message } from "@/utils/message";
import { useHook } from "./hook";
const { getList } = useHook();
/** * 用户导入参数 */
const upload = reactive({
// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: "",
// 是否禁用上传
loading: false,
// 设置上传的请求头部
headers: { Authorization: `Bearer ${getToken().token}` },
// 上传的地址
url: `${import.meta.env.VITE_APP_BASE_API}/system/users/excel`
});
/** 下载模板操作 */
function downloadTemplate() {
http.download(
"system/users/excelTemplate",
`user_template_${new Date().getTime()}.xls`
);
}
/** 文件上传中处理 */
const handleFileUploadProgress = () => {
upload.loading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = () => {
upload.open = false;
upload.loading = false;
formRef.value.clearFiles();
message("导入成功", { type: "success" });
getList();
};
const formRef = ref();
function getFormRef() {
return formRef.value;
}
defineExpose({ getFormRef });
</script>
<template>
<el-upload
ref="formRef"
:limit="1"
accept=".xlsx,.xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.loading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入xlsxlsx格式文件</span>
<el-link
type="primary"
:underline="false"
style="font-size: 12px; vertical-align: baseline"
@click="downloadTemplate"
>下载模板</el-link
>
</div>
</template>
</el-upload>
</template>
@@ -0,0 +1,234 @@
import dayjs from "dayjs";
import { message } from "@/utils/message";
import { ElMessageBox, Sort } from "element-plus";
import {
getLoginLogListApi,
deleteLoginLogApi,
exportLoginLogExcelApi,
LoginLogQuery
} from "@/api/system/log";
import { reactive, ref, onMounted, toRaw } from "vue";
import { useUserStoreHook } from "@/store/modules/user";
import { CommonUtils } from "@/utils/common";
import { PaginationProps } from "@pureadmin/table";
const loginLogStatusMap =
useUserStoreHook().dictionaryMap["sysLoginLog.status"];
export function useLoginLogHook() {
const defaultSort: Sort = {
prop: "loginTime",
order: "descending"
};
const pagination: PaginationProps = {
total: 0,
pageSize: 10,
currentPage: 1,
background: true
};
const timeRange = ref([]);
const searchFormParams = reactive<LoginLogQuery>({
ipAddress: undefined,
username: undefined,
status: undefined,
beginTime: undefined,
endTime: undefined,
timeRangeColumn: defaultSort.prop
});
const dataList = ref([]);
const pageLoading = ref(true);
const multipleSelection = ref([]);
const columns: TableColumnList = [
{
type: "selection",
align: "left"
},
{
label: "日志编号",
prop: "logId",
minWidth: 100
},
{
label: "用户名",
prop: "username",
minWidth: 120,
sortable: "custom"
},
{
label: "IP地址",
prop: "ipAddress",
minWidth: 120
},
{
label: "登录地点",
prop: "loginLocation",
minWidth: 120
},
{
label: "操作系统",
prop: "operationSystem",
minWidth: 120
},
{
label: "浏览器",
prop: "browser",
minWidth: 120
},
{
label: "状态",
prop: "status",
minWidth: 120,
cellRenderer: ({ row, props }) => (
<el-tag
size={props.size}
type={loginLogStatusMap[row.status].cssTag}
effect="plain"
>
{loginLogStatusMap[row.status].label}
</el-tag>
)
},
{
label: "状态名",
prop: "statusStr",
minWidth: 120,
hide: true
},
{
label: "登录时间",
minWidth: 160,
prop: "loginTime",
sortable: "custom",
formatter: ({ loginTime }) =>
dayjs(loginTime).format("YYYY-MM-DD HH:mm:ss")
},
{
label: "操作",
fixed: "right",
width: 140,
slot: "operation"
}
];
async function onSearch() {
// 点击搜索的时候 需要重置分页
pagination.currentPage = 1;
getLoginLogList();
}
function resetForm(formEl, tableRef) {
if (!formEl) return;
// 清空查询参数
formEl.resetFields();
// 清空排序
searchFormParams.orderColumn = undefined;
searchFormParams.orderDirection = undefined;
// 清空时间查询 TODO 这块有点繁琐 有可以优化的地方吗?
// Form组件的resetFields方法无法清除datepicker里面的数据。
timeRange.value = [];
searchFormParams.beginTime = undefined;
searchFormParams.endTime = undefined;
tableRef.getTableRef().clearSort();
// 重置分页并查询
onSearch();
}
async function getLoginLogList(sort: Sort = defaultSort) {
pageLoading.value = true;
if (sort != null) {
CommonUtils.fillSortParams(searchFormParams, sort);
}
CommonUtils.fillPaginationParams(searchFormParams, pagination);
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
const { data } = await getLoginLogListApi(toRaw(searchFormParams)).finally(
() => {
pageLoading.value = false;
}
);
dataList.value = data.rows;
pagination.total = data.total;
}
async function exportAllExcel(sort: Sort = defaultSort) {
if (sort != null) {
CommonUtils.fillSortParams(searchFormParams, sort);
}
CommonUtils.fillPaginationParams(searchFormParams, pagination);
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
exportLoginLogExcelApi(toRaw(searchFormParams), "登录日志.xls");
}
async function handleDelete(row) {
await deleteLoginLogApi([row.logId]).then(() => {
message(`您删除了操作编号为${row.logId}的这条数据`, {
type: "success"
});
// 刷新列表
getLoginLogList();
});
}
async function handleBulkDelete(tableRef) {
if (multipleSelection.value.length === 0) {
message("请选择需要删除的数据", { type: "warning" });
return;
}
ElMessageBox.confirm(
`确认要<strong>删除</strong>编号为<strong style='color:var(--el-color-primary)'>[ ${multipleSelection.value} ]</strong>的日志吗?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
draggable: true
}
)
.then(async () => {
await deleteLoginLogApi(multipleSelection.value).then(() => {
message(`您删除了日志编号为[ ${multipleSelection.value} ]的数据`, {
type: "success"
});
// 刷新列表
getLoginLogList();
});
})
.catch(() => {
message("取消删除", {
type: "info"
});
// 清空checkbox选择的数据
tableRef.getTableRef().clearSelection();
});
}
onMounted(() => {
getLoginLogList();
});
return {
searchFormParams,
pageLoading,
columns,
dataList,
pagination,
defaultSort,
timeRange,
multipleSelection,
onSearch,
exportAllExcel,
// exportExcel,
getLoginLogList,
resetForm,
handleDelete,
handleBulkDelete
};
}