版本预发布

This commit is contained in:
孟帅
2023-02-08 20:29:34 +08:00
parent f11c7c5bf2
commit 2068d05c93
269 changed files with 16122 additions and 12075 deletions

View File

@@ -0,0 +1,206 @@
<template>
<div>
<n-spin :show="show" description="请稍候...">
<n-card
v-show="showInfo"
title="😋 个人信息"
embedded
:bordered="false"
closable
hoverable
@close="handleClose"
>
<n-row>
<n-thing content-indented>
<template #header>
{{ timeFix() }}{{ formValue.realName }}今天又是充满活力的一天
</template>
<template #header-extra> </template>
<template #description>
<n-descriptions
label-placement="left"
style="margin-top: 15px"
column="2"
content-style="padding-right: 20px;"
>
<n-descriptions-item label="用户ID">{{ formValue.id }}</n-descriptions-item>
<n-descriptions-item label="用户名"> {{ formValue.username }} </n-descriptions-item>
<n-descriptions-item label="登录IP">{{
formValue.lastLoginIp
}}</n-descriptions-item>
<n-descriptions-item label="登录时间"
>{{ formValue.lastLoginAt }}
</n-descriptions-item>
<n-descriptions-item label="累计登录">
{{ formValue.loginCount }} </n-descriptions-item
>
<n-descriptions-item label="注册时间">
{{ formValue.createdAt }}
</n-descriptions-item>
<n-descriptions-item label="所属部门">
<n-tag size="small" type="success" strong round :bordered="false">
{{ formValue.deptName }}
<template #icon>
<n-icon :component="CheckmarkCircle" />
</template>
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="所属角色">
<n-tag size="small" type="success" strong round :bordered="false">
{{ formValue.roleName }}
<template #icon>
<n-icon :component="CheckmarkCircle" />
</template>
</n-tag>
</n-descriptions-item>
</n-descriptions>
</template>
</n-thing>
</n-row>
</n-card>
<n-form
:label-width="80"
:model="formValue"
:rules="rules"
ref="formRef"
style="margin-top: 15px"
>
<n-form-item label="头像" path="avatar">
<UploadImage :maxNumber="1" v-model:value="formValue.avatar" />
</n-form-item>
<n-form-item label="姓名" path="realName">
<n-input v-model:value="formValue.realName" />
</n-form-item>
<n-form-item label="QQ号码" path="qq">
<n-input v-model:value="formValue.qq" placeholder="请输入QQ号码" />
</n-form-item>
<n-form-item label="生日" path="birthday">
<DatePicker v-model:formValue="formValue.birthday" type="date" />
</n-form-item>
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formValue.sex" name="sex">
<n-space>
<n-radio :value="1"></n-radio>
<n-radio :value="2"></n-radio>
<n-radio :value="3">保密</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="所在省市区" path="cityId">
<CitySelector v-model:value="formValue.cityId" />
</n-form-item>
<n-form-item label="联系地址" path="address">
<n-input type="textarea" v-model:value="formValue.address" placeholder="联系地址" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" :loading="formBtnLoading" @click="formSubmit"
>保存更新</n-button
>
<n-button :loading="formBtnLoading" @click="resetForm">重置</n-button>
</n-space>
</div>
</n-form>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import UploadImage from '@/components/Upload/uploadImage.vue';
import CitySelector from '@/components/CitySelector/citySelector.vue';
import DatePicker from '@/components/DatePicker/datePicker.vue';
import { getUserInfo, updateMemberProfile } from '@/api/system/user';
import { CheckmarkCircle } from '@vicons/ionicons5';
import { timeFix } from '@/utils/hotgo';
import { UserInfoState, useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
const show = ref(false);
const formRef: any = ref(null);
const message = useMessage();
const formBtnLoading = ref(false);
const rules = {
basicName: {
required: true,
message: '请输入网站名称',
trigger: 'blur',
},
};
const formValue = ref<UserInfoState>({
id: 0,
deptName: '',
roleName: '',
cityLabel: '',
permissions: [],
username: '',
realName: '',
avatar: '',
balance: 0,
sex: 1,
qq: '',
email: '',
mobile: '',
birthday: '',
cityId: 0,
address: '',
cash: {
name: '',
account: '',
payeeCode: '',
},
createdAt: '',
loginCount: 0,
lastLoginAt: '',
lastLoginIp: '',
});
function formSubmit() {
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
updateMemberProfile(formValue.value)
.then((_res) => {
message.success('更新成功');
load();
userStore.GetInfo();
})
.finally(() => {
formBtnLoading.value = false;
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
load();
}
onMounted(() => {
load();
});
async function load() {
show.value = true;
formValue.value = await getUserInfo();
show.value = false;
}
const showInfo = ref(true);
function handleClose() {
showInfo.value = false;
}
</script>

View File

@@ -0,0 +1,121 @@
<template>
<div>
<n-spin :show="show" description="请稍候...">
<n-grid cols="2 s:2 m:2 l:2 xl:2 2xl:2" responsive="screen">
<n-grid-item>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="支付宝姓名" path="name">
<n-input v-model:value="formValue.name" />
</n-form-item>
<n-form-item label="支付宝账号" path="account ">
<n-input v-model:value="formValue.account" />
</n-form-item>
<n-form-item label="支付宝收款码" path="payeeCode">
<UploadImage
:maxNumber="1"
:helpText="'请上传清晰有效的收款码图片大小不超过2M'"
v-model:value="formValue.payeeCode"
/>
</n-form-item>
<n-form-item label="登录密码" path="password">
<n-input
type="password"
v-model:value="formValue.password"
placeholder="请输入登录密码验证身份"
/>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, unref } from 'vue';
import { useMessage } from 'naive-ui';
import UploadImage from '@/components/Upload/uploadImage.vue';
import { BasicUpload } from '@/components/Upload';
import { useGlobSetting } from '@/hooks/setting';
import { useUserStoreWidthOut } from '@/store/modules/user';
import { getUserInfo, updateMemberCash } from '@/api/system/user';
const show = ref(false);
const useUserStore = useUserStoreWidthOut();
const globSetting = useGlobSetting();
const { uploadUrl } = globSetting;
const uploadHeaders = reactive({
Authorization: useUserStore.token,
});
const rules = {
password: {
required: true,
message: '请输入登录密码',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const message = useMessage();
const formValue = ref({
password: '',
payeeCode: '',
account: '',
name: '',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateMemberCash({
name: formValue.value.name,
account: formValue.value.account,
payeeCode: formValue.value.payeeCode,
password: formValue.value.password,
})
.then((_res) => {
message.success('更新成功');
load();
})
.finally(() => {});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function uploadChange(list: string[]) {
// 单图模式,只需要第一个索引
if (list.length > 0) {
formValue.value.payeeCode = unref(list[0]);
} else {
formValue.value.payeeCode = unref('');
}
}
onMounted(() => {
load();
});
function load() {
show.value = true;
getUserInfo()
.then((res) => {
formValue.value = res.cash;
formValue.value.password = '';
})
.finally(() => {
show.value = false;
});
}
</script>

View File

@@ -0,0 +1,317 @@
<template>
<n-grid cols="1" responsive="screen" class="-mt-5">
<n-grid-item>
<n-list>
<n-list-item>
<template #suffix>
<n-button type="primary" text @click="openUpdatePassForm">修改</n-button>
</template>
<n-thing title="账户密码">
<template #description
><span class="text-gray-400">绑定手机和邮箱并设置密码帐号更安全</span></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text @click="openUpdateMobileForm">修改</n-button>
</template>
<n-thing title="绑定手机">
<template #description
><span class="text-gray-400"
>已绑定手机号+86{{ userStore.info?.mobile }}</span
></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text @click="openUpdateEmailForm">修改</n-button>
</template>
<n-thing title="绑定邮箱">
<template #description
><span class="text-gray-400">已绑定邮箱{{ userStore.info?.email }}</span></template
>
</n-thing>
</n-list-item>
</n-list>
</n-grid-item>
</n-grid>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="修改登录密码"
:style="{
width: dialogWidth,
}"
>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="当前密码" path="oldPassword">
<n-input
type="password"
v-model:value="formValue.oldPassword"
placeholder="请输入当前密码"
/>
</n-form-item>
<n-form-item label="新密码" path="newPassword">
<n-input type="password" v-model:value="formValue.newPassword" placeholder="请输入新密码" />
</n-form-item>
<div>
<n-space justify="end">
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" @click="formSubmit">修改并重新登录</n-button>
</n-space>
</div>
</n-form>
</n-modal>
<n-modal
:block-scroll="false"
:mask-closable="false"
v-model:show="showMobileModal"
:show-icon="false"
preset="dialog"
title="修改手机号"
:style="{
width: dialogWidth,
}"
>
<n-form :label-width="80" :model="formMobileValue" ref="formMobileRef">
<n-form-item label="短信验证码" path="code" v-if="userStore.info?.mobile !== ''">
<n-input-group>
<n-input v-model:value="formMobileValue.code" placeholder="请输入验证码" />
<n-button
type="primary"
ghost
@click="sendMobileCode"
:disabled="isCounting"
:loading="sendLoading"
>
{{ sendLabel }}
</n-button>
</n-input-group>
<template #feedback> 接收号码+86{{ userStore.info?.mobile }} </template>
</n-form-item>
<n-form-item label="换绑手机号" path="mobile">
<n-input v-model:value="formMobileValue.mobile" placeholder="请输入换绑手机号" />
</n-form-item>
<div>
<n-space justify="end">
<n-button @click="showMobileModal = false">取消</n-button>
<n-button type="primary" :loading="formMobileBtnLoading" @click="formMobileSubmit"
>保存更新</n-button
>
</n-space>
</div>
</n-form>
</n-modal>
<n-modal
:block-scroll="false"
:mask-closable="false"
v-model:show="showEmailModal"
:show-icon="false"
preset="dialog"
title="修改邮箱"
:style="{
width: dialogWidth,
}"
>
<n-form :label-width="80" :model="formEmailValue" ref="formEmailRef">
<n-form-item label="邮箱验证码" path="code" v-if="userStore.info?.email !== ''">
<n-input-group>
<n-input v-model:value="formEmailValue.code" placeholder="请输入验证码" />
<n-button
type="primary"
ghost
@click="sendEmailCode"
:disabled="isCounting"
:loading="sendLoading"
>
{{ sendLabel }}
</n-button>
</n-input-group>
<template #feedback> 接收邮箱{{ userStore.info?.email }} </template>
</n-form-item>
<n-form-item label="换绑邮箱" path="email">
<n-input v-model:value="formEmailValue.email" placeholder="请输入换绑邮箱" />
</n-form-item>
<div>
<n-space justify="end">
<n-button @click="showEmailModal = false">取消</n-button>
<n-button type="primary" :loading="formEmailBtnLoading" @click="formEmailSubmit"
>保存更新</n-button
>
</n-space>
</div>
</n-form>
</n-modal>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { useRouter, useRoute } from 'vue-router';
import { useSendCode } from '@/hooks/common';
import { adaModalWidth } from '@/utils/hotgo';
import {
updateMemberPwd,
updateMemberMobile,
updateMemberEmail,
SendBindEmail,
SendBindSms,
} from '@/api/system/user';
import { TABS_ROUTES } from '@/store/mutation-types';
import { useUserStore } from '@/store/modules/user';
const { sendLabel, isCounting, loading: sendLoading, activateSend } = useSendCode();
const userStore = useUserStore();
const dialogWidth = ref('75%');
const rules = {
basicName: {
required: true,
message: '请输入网站名称',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const message = useMessage();
const router = useRouter();
const route = useRoute();
const showModal = ref(false);
const formValue = ref({
oldPassword: '',
newPassword: '',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateMemberPwd({
oldPassword: formValue.value.oldPassword,
newPassword: formValue.value.newPassword,
})
.then((_res) => {
message.success('更新成功');
userStore.logout().then(() => {
message.success('成功退出登录');
// 移除标签页
localStorage.removeItem(TABS_ROUTES);
router
.replace({
name: 'Login',
query: {
redirect: route.fullPath,
},
})
.finally(() => location.reload());
});
})
.finally(() => {
showModal.value = false;
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function openUpdatePassForm() {
showModal.value = true;
formValue.value.newPassword = '';
formValue.value.oldPassword = '';
}
const formMobileBtnLoading = ref(false);
const formMobileRef: any = ref(null);
const showMobileModal = ref(false);
const formMobileValue = ref({
mobile: '',
code: '',
});
function formMobileSubmit() {
formMobileRef.value.validate((errors) => {
if (!errors) {
formMobileBtnLoading.value = true;
updateMemberMobile({
mobile: formMobileValue.value.mobile,
code: formMobileValue.value.code,
})
.then((_res) => {
message.success('更新成功');
showMobileModal.value = false;
userStore.GetInfo();
})
.finally(() => {
formMobileBtnLoading.value = false;
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function openUpdateMobileForm() {
showMobileModal.value = true;
formMobileValue.value.mobile = '';
formMobileValue.value.code = '';
}
const formEmailBtnLoading = ref(false);
const formEmailRef: any = ref(null);
const showEmailModal = ref(false);
const formEmailValue = ref({
email: '',
code: '',
});
function formEmailSubmit() {
formEmailRef.value.validate((errors) => {
if (!errors) {
formEmailBtnLoading.value = true;
updateMemberEmail({
email: formEmailValue.value.email,
code: formEmailValue.value.code,
})
.then((_res) => {
message.success('更新成功');
showEmailModal.value = false;
userStore.GetInfo();
})
.finally(() => {
formEmailBtnLoading.value = false;
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function openUpdateEmailForm() {
showEmailModal.value = true;
formEmailValue.value.email = '';
formEmailValue.value.code = '';
}
function sendMobileCode() {
activateSend(SendBindSms());
}
function sendEmailCode() {
activateSend(SendBindEmail());
}
onMounted(async () => {
adaModalWidth(dialogWidth, 580);
});
</script>

View File

@@ -0,0 +1,83 @@
<template>
<div>
<n-grid cols="24 300:1 600:24" :x-gap="24">
<n-grid-item span="6">
<n-card :bordered="false" class="proCard">
<n-thing
class="thing-cell"
v-for="item in typeTabList"
:key="item.key"
:class="{ 'thing-cell-on': type === item.key }"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
<template #description>{{ item.desc }}</template>
</n-thing>
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1" />
<SafetySetting v-if="type === 2" />
<CashSetting v-if="type === 3" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import BasicSetting from './BasicSetting.vue';
import SafetySetting from './SafetySetting.vue';
import CashSetting from './CashSetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '个人账户信息设置',
key: 1,
},
{
name: '安全设置',
desc: '密码、手机号、邮箱等设置',
key: 2,
},
{
name: '提现设置',
desc: '提现收款账号支付宝设置',
key: 3,
},
];
const type = ref(1);
const typeTitle = ref('基本设置');
function switchType(e) {
type.value = e.key;
typeTitle.value = e.name;
}
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer;
}
}
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0;
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<n-spin :show="loading">
<n-empty v-show="dataSource.list?.length === 0" description="无数据" />
<n-list hoverable clickable class="list-item">
<n-list-item v-for="item in dataSource.list" :key="item.id" @click="UnRead(item)">
<n-thing
content-indented
:title="item.title"
:description="item.createdAt"
:content-style="{ padding: '10px' }"
>
<template #avatar>
<n-badge v-bind="getBadgePops(item)">
<n-avatar v-if="item.senderAvatar !== ''" round :size="28" :src="item.senderAvatar" />
<n-icon-wrapper v-else :size="28" :border-radius="10">
<n-icon :size="20" :component="getIcon(item)" />
</n-icon-wrapper>
</n-badge>
</template>
<template #header-extra>
<n-tag
v-if="item.tagTitle !== '' && item.tagTitle !== undefined"
v-bind="item.tagProps"
size="large"
strong
>
{{ item.tagTitle }}
</n-tag>
</template>
<template #footer>
<span v-html="item.content"></span>
</template>
</n-thing>
</n-list-item>
</n-list>
</n-spin>
<n-space justify="end" style="margin-top: 30px">
<n-pagination
v-model:page="dataSource.page"
:page-count="dataSource.pageCount"
:page-slot="5"
:page-sizes="[5, 10, 50, 100]"
size="medium"
show-quick-jumper
show-size-picker
:on-update:page="onUpdatePage"
:on-update:page-size="onUpdatePageSize"
/>
</n-space>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { MessageRow, parseMessage } from '@/enums/systemMessageEnum';
import { getIcon } from '@/enums/systemMessageEnum';
import { MessageList, UpRead } from '@/api/apply/notice';
import { debounce } from 'throttle-debounce';
import { notificationStoreWidthOut } from '@/store/modules/notification';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '1',
});
interface dataList {
page: number;
pageSize: number;
pageCount: number;
list: null | MessageRow[];
}
const dataSource = ref<dataList>({
page: 1,
pageSize: 5,
pageCount: 1,
list: [],
});
const loading = ref(false);
const notificationStore = notificationStoreWidthOut();
function loadDataSource() {
loading.value = true;
MessageList({
type: props.type,
page: dataSource.value.page,
pageSize: dataSource.value.pageSize,
})
.then((res) => {
if (res.list?.length > 0) {
for (let i = 0; i < res.list.length; i++) {
res.list[i] = parseMessage(res.list[i]);
}
}
dataSource.value = res as dataList;
})
.finally(() => {
loading.value = false;
});
}
function UnRead(item: MessageRow) {
UpRead({ id: item.id })
.then(() => {
item.isRead = true;
debounceCallback();
})
.finally(() => {
loading.value = false;
});
}
const debounceCallback = debounce(1000, function () {
notificationStore.pullMessages();
});
function getBadgePops(item: MessageRow) {
if (item.isRead) {
return {};
}
return { dot: true, processing: true, offset: [-2, 2] };
}
function onUpdatePage(page: number) {
dataSource.value.page = page;
loadDataSource();
}
function onUpdatePageSize(pageSize: number) {
dataSource.value.pageSize = pageSize;
loadDataSource();
}
onMounted(() => {
loadDataSource();
});
</script>
<style lang="less" scoped>
::v-deep(.list-item) {
margin-left: calc(1vw);
margin-right: calc(1vw);
}
:deep(img, video, audio) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="我的消息">
在这里你可以查看平台中通知公告和与你相关的私信
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
size="large"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane name="1" tab="通知"> <List :type="defaultTab" /></n-tab-pane>
<n-tab-pane name="2" tab="公告"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="3" tab="私信"> <List :type="defaultTab" /> </n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const defaultTab = ref('1');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
});
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
</script>