init: 初始系统模板

This commit is contained in:
TsMask
2023-09-05 14:38:23 +08:00
parent a5bc16ae4f
commit 1075c8ae4f
130 changed files with 22531 additions and 1 deletions

View File

@@ -0,0 +1,248 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue/lib';
import { FileType } from 'ant-design-vue/lib/upload/interface';
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import { onMounted, reactive, ref, toRaw } from 'vue';
import { updateUserProfile, uploadAvatar } from '@/api/profile';
import { regExpEmail, regExpMobile, regExpNick } from '@/utils/regular-utils';
import useUserStore from '@/store/modules/user';
import useDictStore from '@/store/modules/dict';
const uerStore = useUserStore();
const { getDict } = useDictStore();
/**用户性别字典 */
let sysUserSex = ref<DictType[]>([
{ label: '未知', value: '0', elTagType: '', elTagClass: '' },
{ label: '男', value: '1', elTagType: '', elTagClass: '' },
{ label: '女', value: '2', elTagType: '', elTagClass: '' },
]);
/**表单数据状态 */
let stateForm = reactive({
/**表单属性 */
form: {
nickName: '',
email: '',
phonenumber: '',
sex: undefined,
},
/**表单提交点击状态 */
formClick: false,
});
/**表单数据状态初始化 */
function fnInitstateForm() {
stateForm.form = Object.assign(stateForm.form, uerStore.getBaseInfo);
stateForm.formClick = false;
}
/**表单验证通过 */
function fnFinish() {
Modal.confirm({
title: '提示',
content: `确认要提交修改用户基本信息吗?`,
onOk() {
stateForm.formClick = true;
// 发送请求
const hide = message.loading('请稍等...', 0);
const form = toRaw(stateForm.form);
updateUserProfile(form).then(res => {
hide();
stateForm.formClick = false;
if (res.code === 200) {
Modal.success({
title: '提示',
content: `用户基本信息修改成功!`,
okText: '我知道了',
onOk() {
uerStore.setBaseInfo(form);
},
});
} else {
message.error(`${res.msg}`, 3);
}
});
},
});
}
/**上传状态 */
let upState = ref<boolean>(false);
/**上传前检查或转换压缩 */
function fnBeforeUpload(file: FileType) {
if (upState.value) return false;
const isJpgOrPng = ['image/jpeg', 'image/png'].includes(file.type);
if (!isJpgOrPng) {
message.error('只支持上传图片格式jpg、png', 3);
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片文件大小必须小于 2MB', 3);
}
return isJpgOrPng && isLt2M;
}
/**上传变更 */
function fnUpload(up: UploadRequestOption) {
Modal.confirm({
title: '提示',
content: `确认要上传/变更用户头像吗?`,
onOk() {
// 发送请求
const hide = message.loading('请稍等...', 0);
upState.value = true;
let formData = new FormData();
formData.append('file', up.file);
uploadAvatar(formData).then(res => {
upState.value = false;
hide();
if (res.code === 200) {
message.success('头像上传/变更成功', 3);
uerStore.setAvatar(res.data);
} else {
message.error(res.msg, 3);
}
});
},
});
}
onMounted(() => {
// 初始字典数据
getDict('sys_user_sex').then(res => {
if (res.length > 0) {
sysUserSex.value = res;
}
});
// 初始表单值
fnInitstateForm();
});
</script>
<template>
<a-form
:model="stateForm.form"
name="stateForm"
layout="vertical"
:wrapper-col="{ span: 18 }"
@finish="fnFinish"
>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24" style="margin-bottom: 30px">
<a-form-item
label="用户昵称"
name="nickName"
:rules="[
{
required: true,
pattern: regExpNick,
message: '昵称只能包含字母、数字、中文和下划线且不少于2位',
},
]"
>
<a-input
v-model:value="stateForm.form.nickName"
allow-clear
:maxlength="26"
placeholder="请输入用户昵称"
></a-input>
</a-form-item>
<a-form-item
label="手机号码"
name="phonenumber"
:rules="[
{
required: false,
pattern: regExpMobile,
message: '请输入正确手机号码',
},
]"
>
<a-input
v-model:value="stateForm.form.phonenumber"
allow-clear
:maxlength="11"
placeholder="请输入手机号码"
></a-input>
</a-form-item>
<a-form-item
label="电子邮箱"
name="email"
:rules="[
{
required: false,
pattern: regExpEmail,
message: '请输入正确电子邮箱',
},
]"
>
<a-input
v-model:value="stateForm.form.email"
allow-clear
:maxlength="40"
placeholder="请输入电子邮箱"
></a-input>
</a-form-item>
<a-form-item
label="用户性别"
name="sex"
:rules="[
{
required: true,
message: '请选择用户性别',
},
]"
>
<a-select
v-model:value="stateForm.form.sex"
placeholder="用户性别"
:options="sysUserSex"
>
</a-select>
</a-form-item>
<a-space :size="8">
<a-button
block
type="primary"
html-type="submit"
:loading="stateForm.formClick"
>
确认修改
</a-button>
<a-button
type="default"
@click="fnInitstateForm"
:disabled="stateForm.formClick"
>
重置
</a-button>
</a-space>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-space direction="vertical" :size="16">
<a-image :width="128" :height="128" :src="uerStore.getAvatar" />
<span>请选择等比大小图片作为头像如200x200400x400</span>
<a-upload
name="file"
list-type="picture"
:max-count="1"
:show-upload-list="false"
:before-upload="fnBeforeUpload"
:custom-request="fnUpload"
>
<a-button type="default" :loading="upState">
上传/变更图片
</a-button>
</a-upload>
</a-space>
</a-col>
</a-row>
</a-form>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,158 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue/lib';
import { reactive } from 'vue';
import { updateUserPwd } from '@/api/profile';
import { regExpPasswd } from '@/utils/regular-utils';
import useUserStore from '@/store/modules/user';
import { useRouter } from 'vue-router';
const { userName, fnLogOut } = useUserStore();
const router = useRouter();
let state = reactive({
/**表单属性 */
form: {
oldPassword: '',
newPassword: '',
confirmPassword: '',
},
/**表单提交点击状态 */
formClick: false,
});
/**表单验证确认密码是否一致 */
function fnEqualToPassword(
rule: Record<string, any>,
value: string,
callback: (error?: string) => void
) {
if (!value) {
return Promise.reject('请输入确认新密码');
}
if (state.form.oldPassword === value) {
return Promise.reject('与旧密码一致,请重新输入新密码');
}
if (state.form.newPassword === value) {
return Promise.resolve();
}
return Promise.reject('两次输入的新密码不一致');
}
/**表单验证通过 */
function fnFinish() {
Modal.confirm({
title: '提示',
content: `确认要提交修改密码吗?`,
onOk() {
state.formClick = true;
// 发送请求
const hide = message.loading('请稍等...', 0);
updateUserPwd(state.form.oldPassword, state.form.confirmPassword)
.then(res => {
hide();
if (res.code === 200) {
Modal.success({
title: '提示',
content: `恭喜您,${userName} 账号密码修改成功!`,
okText: '重新登录',
onOk() {
fnLogOut().finally(() => router.push({ name: 'Login' }));
},
});
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
state.formClick = false;
});
},
});
}
</script>
<template>
<a-form
:model="state.form"
name="stateForm"
layout="vertical"
:wrapper-col="{ span: 9 }"
@finish="fnFinish"
>
<a-form-item
label="旧密码"
name="oldPassword"
:rules="[
{
required: true,
min: 6,
max: 26,
message: '旧密码不能为空且不少于6位',
},
]"
>
<a-input-password
v-model:value="state.form.oldPassword"
placeholder="请输入旧密码"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item
label="新密码"
name="newPassword"
:rules="[
{
required: true,
pattern: regExpPasswd,
message: '密码至少包含大小写字母、数字、特殊符号且不少于6位',
},
]"
>
<a-input-password
v-model:value="state.form.newPassword"
placeholder="请输入新密码"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item
label="确认新密码"
name="confirmPassword"
:rules="[
{
required: true,
validator: fnEqualToPassword,
},
]"
>
<a-input-password
v-model:value="state.form.confirmPassword"
placeholder="请确认新密码"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item :wrapper-col="{ span: 3 }">
<a-button
block
type="primary"
html-type="submit"
:loading="state.formClick"
>
提交修改
</a-button>
</a-form-item>
</a-form>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,165 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { getLocalColor, changePrimaryColor } from '@/hooks/useTheme';
import useLayoutStore from '@/store/modules/layout';
const { proConfig, changeConf } = useLayoutStore();
let color = ref<string>(getLocalColor());
/**改变主题色 */
function fnColorChange(e: Event) {
const target = e.target as HTMLInputElement;
if (target.nodeName === 'INPUT') {
changePrimaryColor(target.value ?? '#1890ff');
} else {
changePrimaryColor();
}
color.value = getLocalColor();
}
</script>
<template>
<a-divider orientation="left">布局属性</a-divider>
<a-list item-layout="vertical" size="large" row-key="title">
<a-list-item>
整体布局
<template #actions> 导航模式模块设置 </template>
<template #extra>
<a-radio-group
style="margin-bottom: 12px"
:value="proConfig.layout"
@change="(e:any) => changeConf('layout', e.target.value)"
>
<a-radio value="side">左侧菜单布局</a-radio>
<a-radio value="top">顶部菜单布局</a-radio>
<a-radio value="mix">混合菜单布局</a-radio>
</a-radio-group>
</template>
</a-list-item>
<a-list-item>
风格配色
<template #actions> 整体风格配色设置 </template>
<template #extra>
<a-space :size="16" align="end" direction="horizontal">
<a-button type="primary" size="small" @click="fnColorChange">
<BgColorsOutlined /> 随机
</a-button>
<input type="color" :value="color" @input="fnColorChange" />
</a-space>
</template>
</a-list-item>
<a-list-item>
深色菜单
<template #actions> 只能改变导航模式的菜单 </template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.navTheme === 'dark'"
@change="
(checked:any) => changeConf('navTheme', checked ? 'dark' : 'light')
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
固定顶部导航栏
<template #actions> 顶部导航栏是否固定不随滚动条移动 </template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.fixedHeader"
@change="(checked:any) => changeConf('fixedHeader', checked)"
></a-switch>
</template>
</a-list-item>
<a-list-item>
固定左侧菜单
<template #actions> 左侧菜单是否固定仅左侧菜单布局时有效 </template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.fixSiderbar"
@change="(checked:any) => changeConf('fixSiderbar', checked)"
></a-switch>
</template>
</a-list-item>
<a-list-item>
自动分割菜单
<template #actions>
顶部有多级菜单时显示左侧菜单仅混合菜单布局时有效
</template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.splitMenus"
@change="(checked:any) => changeConf('splitMenus', checked)"
></a-switch>
</template>
</a-list-item>
</a-list>
<a-divider orientation="left">内容区域</a-divider>
<a-list item-layout="vertical" size="large" row-key="title">
<a-list-item>
顶栏
<template #actions> 是否显示顶部导航栏 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.headerRender === undefined"
@change="
(checked:any) => changeConf('headerRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
页脚
<template #actions> 是否显示底部导航栏 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.footerRender === undefined"
@change="
(checked:any) => changeConf('footerRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
菜单头
<template #actions> 是否显示左侧菜单栏顶部LOGO区域 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.menuHeaderRender === undefined"
@change="
(checked:any) => changeConf('menuHeaderRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
导航标签项
<template #actions> 是否显示顶部Tab导航标签项 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.tabRender === undefined"
@change="
(checked:any) => changeConf('tabRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
</a-list>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,198 @@
<script lang="ts" setup>
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message } from 'ant-design-vue/lib';
import { getUserProfile } from '@/api/profile';
import { reactive, ref, onMounted } from 'vue';
import { parseDateToStr } from '@/utils/date-utils';
import useUserStore from '@/store/modules/user';
/**加载状态 */
let loading = ref<boolean>(true);
/**Tab标签激活 */
let activeKey = ref<string>('list');
/**个人信息数据状态 */
let state = reactive<{
user: Record<string, any>;
postGroup: string[];
roleGroup: string[];
}>({
user: {},
postGroup: [],
roleGroup: [],
});
/**列表数据 */
let listData = ref([
{
id: 'Vue',
title: 'Vue.js - 渐进式 JavaScript 框架 | Vue.js',
description:
'基于标准 HTML、CSS 和 JavaScript 构建,提供容易上手的 API 和一流的文档。 性能出色 经过编译器优化、完全响应式的渲染系统,几乎不需要手动优化。',
},
{
id: 'Vue Router',
title: 'Vue Router | Vue.js 的官方路由',
description:
'为Vue.js 提供富有表现力、可配置的、方便的路由,用直观且强大的语法来定义静态或动态路由。',
},
{
id: 'Pinia',
title: 'Pinia | The intuitive store for Vue.js',
description:
'Pinia hooks into Vue devtools to give you an enhanced development experience in both Vue 2 and Vue 3. ',
},
{
id: 'Vite',
title: 'Vite | 下一代的前端工具链',
description:
'Vite(法语意为 "快速的",发音 /vit/,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验',
},
]);
/**查询用户个人信息 */
function fnGetProfile() {
getUserProfile().then(res => {
if (res.code === 200 && res.data) {
const { user, roleGroup, postGroup } = res.data;
state.user = user;
state.roleGroup = roleGroup;
state.postGroup = postGroup;
// 头像解析
state.user.avatar = useUserStore().fnAvatar(user.avatar);
loading.value = false;
} else {
message.error(res.msg, 3);
}
});
}
onMounted(() => {
// 获取信息
fnGetProfile();
});
</script>
<template>
<PageContainer :loading="loading">
<a-row :gutter="16">
<a-col :lg="6" :md="6" :xs="24">
<a-card :body-style="{ padding: '0px' }" style="margin-bottom: 16px">
<template #title>
<div class="info-top">
<div class="info-top-no">No{{ state.user.userId }}</div>
<a-avatar
shape="circle"
:size="96"
:src="state.user.avatar"
:alt="state.user.userName"
></a-avatar>
<div class="info-top-nickname" :title="state.user.nickName">
{{ state.user.nickName }}
</div>
</div>
</template>
<a-descriptions
size="small"
layout="vertical"
:bordered="true"
:column="1"
>
<a-descriptions-item label="手机号码">
{{ state.user.phonenumber || '-' }}
</a-descriptions-item>
<a-descriptions-item label="用户邮箱">
{{ state.user.email || '-' }}
</a-descriptions-item>
<a-descriptions-item label="所属部门">
{{ state.user.dept?.deptName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="拥有岗位">
<span v-if="state.postGroup.length === 0">-</span>
<a-tag v-else v-for="v in state.postGroup" :key="v">
{{ v }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="拥有角色">
<span v-if="state.roleGroup.length === 0">-</span>
<a-tag v-else v-for="v in state.roleGroup" :key="v">
{{ v }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="登录地址">
{{ state.user.loginIp || '-' }}
</a-descriptions-item>
<a-descriptions-item label="登录时间">
<span v-if="+state.user.loginDate > 0">
{{ parseDateToStr(+state.user.loginDate) }}
</span>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<a-col :lg="18" :md="18" :xs="24">
<a-card>
<a-tabs
tab-position="top"
:destroy-inactive-tab-pane="true"
v-model:activeKey="activeKey"
>
<a-tab-pane key="list" tab="列表">
<a-list
item-layout="horizontal"
:data-source="listData"
row-key="id"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
{{ item.title }}
</template>
<template #description>
{{ item.description }}
</template>
<template #avatar>
<a-avatar>{{ item.id }}</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-tab-pane>
<a-tab-pane key="empty" tab="空状态">
<a-empty>
<template #description> 暂无数据尝试刷新看看 </template>
<a-button type="primary">刷新</a-button>
</a-empty>
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped>
.info-top {
display: flex;
flex-direction: column;
align-items: center;
&-no {
align-self: flex-start;
font-size: 14px;
margin-bottom: 16px;
}
&-nickname {
margin-top: 16px;
font-size: 24px;
align-self: flex-start;
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import BaseInfo from './components/base-info.vue';
import ResetPasswd from './components/reset-passwd.vue';
import StyleLayout from './components/style-layout.vue';
/**Tab标签激活 */
let activeKey = ref<string>('base-info');
</script>
<template>
<PageContainer>
<a-card>
<a-tabs tab-position="left" v-model:activeKey="activeKey">
<a-tab-pane key="base-info" tab="基础信息">
<BaseInfo></BaseInfo>
</a-tab-pane>
<a-tab-pane key="reset-passwd" tab="重置密码">
<ResetPasswd></ResetPasswd>
</a-tab-pane>
<a-tab-pane key="style-layout" tab="个性化">
<StyleLayout></StyleLayout>
</a-tab-pane>
</a-tabs>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>