29 changed files with 507 additions and 192 deletions
@ -0,0 +1,64 @@
|
||||
<template> |
||||
<CollapseContainer title="账号绑定" :canExpan="false"> |
||||
<List> |
||||
<template v-for="item in accountBindList" :key="item.key"> |
||||
<ListItem> |
||||
<ListItemMeta> |
||||
<template #avatar> |
||||
<Icon v-if="item.avatar" class="avatar" :icon="item.avatar" :color="item.color" /> |
||||
</template> |
||||
<template #title> |
||||
{{ item.title }} |
||||
<a-button type="link" size="small" v-if="item.extra" class="extra"> |
||||
{{ item.extra }} |
||||
</a-button> |
||||
</template> |
||||
<template #description> |
||||
<div>{{ item.description }}</div> |
||||
</template> |
||||
</ListItemMeta> |
||||
</ListItem> |
||||
</template> |
||||
</List> |
||||
</CollapseContainer> |
||||
</template> |
||||
<script setup lang="ts"> |
||||
import { List } from 'ant-design-vue' |
||||
import { CollapseContainer } from '@/components/Container/index' |
||||
import { accountBindList } from './data' |
||||
import { getUserProfileApi } from '@/api/base/profile' |
||||
import { onMounted } from 'vue' |
||||
|
||||
const ListItem = List.Item |
||||
const ListItemMeta = List.Item.Meta |
||||
|
||||
async function init() { |
||||
const userInfo = await getUserProfileApi() |
||||
// TODO |
||||
for (const i in accountBindList) { |
||||
if (userInfo.socialUsers) { |
||||
for (const j in userInfo.socialUsers) { |
||||
if (accountBindList[i].key === userInfo.socialUsers[j].type) { |
||||
accountBindList[i].title = '已綁定' |
||||
break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
onMounted(async () => { |
||||
await init() |
||||
}) |
||||
</script> |
||||
<style lang="less" scoped> |
||||
.avatar { |
||||
font-size: 40px !important; |
||||
} |
||||
|
||||
.extra { |
||||
float: right; |
||||
margin-top: 10px; |
||||
margin-right: 30px; |
||||
cursor: pointer; |
||||
} |
||||
</style> |
@ -0,0 +1,80 @@
|
||||
<template> |
||||
<CollapseContainer title="基本设置" :canExpan="false"> |
||||
<Row :gutter="24"> |
||||
<Col :span="14"> |
||||
<BasicForm @register="register" /> |
||||
</Col> |
||||
<Col :span="10"> |
||||
<div class="change-avatar"> |
||||
<div class="mb-2">头像</div> |
||||
<CropperAvatar |
||||
:value="avatar" |
||||
btnText="更换头像" |
||||
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }" |
||||
@change="updateAvatar" |
||||
width="150" |
||||
/> |
||||
</div> |
||||
</Col> |
||||
</Row> |
||||
<Button type="primary" @click="handleSubmit"> 更新基本信息 </Button> |
||||
</CollapseContainer> |
||||
</template> |
||||
<script setup lang="ts"> |
||||
import { Button, Row, Col } from 'ant-design-vue' |
||||
import { computed, onMounted } from 'vue' |
||||
import { BasicForm, useForm } from '@/components/Form/index' |
||||
import { CollapseContainer } from '@/components/Container' |
||||
import { CropperAvatar } from '@/components/Cropper' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import headerImg from '@/assets/images/header.jpg' |
||||
import { baseSetschemas } from './data' |
||||
import { useUserStore } from '@/store/modules/user' |
||||
import { getUserProfileApi, updateUserProfileApi, uploadAvatarApi } from '@/api/base/profile' |
||||
|
||||
const { createMessage } = useMessage() |
||||
const userStore = useUserStore() |
||||
|
||||
const [register, { setFieldsValue, validate }] = useForm({ |
||||
labelWidth: 120, |
||||
schemas: baseSetschemas, |
||||
showActionButtonGroup: false |
||||
}) |
||||
|
||||
onMounted(async () => { |
||||
const data = await getUserProfileApi() |
||||
setFieldsValue(data) |
||||
}) |
||||
|
||||
const avatar = computed(() => { |
||||
const { avatar } = userStore.getUserInfo.user |
||||
return avatar || headerImg |
||||
}) |
||||
|
||||
async function updateAvatar({ src, data }) { |
||||
await uploadAvatarApi({ avatarFile: data }) |
||||
const userinfo = userStore.getUserInfo |
||||
userinfo.user.avatar = src |
||||
userStore.setUserInfo(userinfo) |
||||
console.log('data', data) |
||||
} |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate() |
||||
await updateUserProfileApi(values) |
||||
} finally { |
||||
createMessage.success('更新成功!') |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="less" scoped> |
||||
.change-avatar { |
||||
img { |
||||
display: block; |
||||
margin-bottom: 15px; |
||||
border-radius: 50%; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,34 @@
|
||||
<template> |
||||
<CollapseContainer title="新消息通知" :canExpan="false"> |
||||
<List> |
||||
<template v-for="item in msgNotifyList" :key="item.key"> |
||||
<ListItem> |
||||
<ListItemMeta> |
||||
<template #title> |
||||
{{ item.title }} |
||||
<Switch class="extra" checked-children="开" un-checked-children="关" default-checked /> |
||||
</template> |
||||
<template #description> |
||||
<div>{{ item.description }}</div> |
||||
</template> |
||||
</ListItemMeta> |
||||
</ListItem> |
||||
</template> |
||||
</List> |
||||
</CollapseContainer> |
||||
</template> |
||||
<script setup lang="ts"> |
||||
import { List, Switch } from 'ant-design-vue' |
||||
import { CollapseContainer } from '@/components/Container/index' |
||||
import { msgNotifyList } from './data' |
||||
|
||||
const ListItem = List.Item |
||||
const ListItemMeta = List.Item.Meta |
||||
</script> |
||||
<style lang="less" scoped> |
||||
.extra { |
||||
float: right; |
||||
margin-top: 10px; |
||||
margin-right: 30px; |
||||
} |
||||
</style> |
@ -0,0 +1,43 @@
|
||||
<template> |
||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit"> |
||||
<BasicForm @register="registerForm" /> |
||||
</BasicModal> |
||||
</template> |
||||
<script lang="ts" setup name="PasswordModel"> |
||||
import { ref } from 'vue' |
||||
import { BasicModal, useModalInner } from '@/components/Modal' |
||||
import { BasicForm, useForm } from '@/components/Form' |
||||
import { passwordSchema } from './data' |
||||
import { updateUserPwdApi } from '@/api/base/profile' |
||||
|
||||
const emit = defineEmits(['success', 'register']) |
||||
|
||||
const title = ref('修改密码') |
||||
|
||||
const [registerForm, { resetFields, validate }] = useForm({ |
||||
labelWidth: 100, |
||||
baseColProps: { span: 24 }, |
||||
schemas: passwordSchema, |
||||
showActionButtonGroup: false, |
||||
actionColOptions: { |
||||
span: 23 |
||||
} |
||||
}) |
||||
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(() => { |
||||
resetFields() |
||||
setModalProps({ confirmLoading: false }) |
||||
}) |
||||
|
||||
async function handleSubmit() { |
||||
try { |
||||
const values = await validate() |
||||
await updateUserPwdApi(values.oldPassword, values.newPassword) |
||||
setModalProps({ confirmLoading: true }) |
||||
closeModal() |
||||
emit('success') |
||||
} finally { |
||||
setModalProps({ confirmLoading: false }) |
||||
} |
||||
} |
||||
</script> |
@ -0,0 +1,55 @@
|
||||
<template> |
||||
<CollapseContainer title="安全设置" :canExpan="false"> |
||||
<List> |
||||
<template v-for="item in secureSettingList" :key="item.key"> |
||||
<ListItem> |
||||
<ListItemMeta> |
||||
<template #title> |
||||
{{ item.title }} |
||||
<div class="extra" v-if="item.extra"> |
||||
<a-button type="link" @click="handleEdit(item.title)">{{ item.extra }}</a-button> |
||||
</div> |
||||
</template> |
||||
<template #description> |
||||
<div>{{ item.description }}</div> |
||||
</template> |
||||
</ListItemMeta> |
||||
</ListItem> |
||||
</template> |
||||
</List> |
||||
</CollapseContainer> |
||||
<PasswordModel @register="registerModal" @success="handleSuccess" /> |
||||
</template> |
||||
<script setup lang="ts"> |
||||
import { List } from 'ant-design-vue' |
||||
import { CollapseContainer } from '@/components/Container/index' |
||||
import { secureSettingList } from './data' |
||||
import { useModal } from '@/components/Modal' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import PasswordModel from './PasswordModel.vue' |
||||
|
||||
const ListItem = List.Item |
||||
const ListItemMeta = List.Item.Meta |
||||
|
||||
const { createMessage } = useMessage() |
||||
const [registerModal, { openModal }] = useModal() |
||||
|
||||
function handleEdit(title: string) { |
||||
if (title == '账户密码') { |
||||
openModal(true, {}) |
||||
} |
||||
} |
||||
function handleSuccess() { |
||||
createMessage.success('更新成功!') |
||||
} |
||||
</script> |
||||
<style lang="less" scoped> |
||||
.extra { |
||||
float: right; |
||||
margin-top: 10px; |
||||
margin-right: 30px; |
||||
font-weight: normal; |
||||
color: #1890ff; |
||||
cursor: pointer; |
||||
} |
||||
</style> |
@ -0,0 +1,189 @@
|
||||
import { FormSchema } from '@/components/Form/index' |
||||
import { useI18n } from '@/hooks/web/useI18n' |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
export interface ListItem { |
||||
key: string |
||||
title: string |
||||
description: string |
||||
extra?: string |
||||
avatar?: string |
||||
color?: string |
||||
} |
||||
|
||||
// tab的list
|
||||
export const settingList = [ |
||||
{ |
||||
key: '1', |
||||
name: '基本设置', |
||||
component: 'BaseSetting' |
||||
}, |
||||
{ |
||||
key: '2', |
||||
name: '安全设置', |
||||
component: 'SecureSetting' |
||||
}, |
||||
{ |
||||
key: '3', |
||||
name: '账号绑定', |
||||
component: 'AccountBind' |
||||
}, |
||||
{ |
||||
key: '4', |
||||
name: '新消息通知', |
||||
component: 'MsgNotify' |
||||
} |
||||
] |
||||
|
||||
// 基础设置 form
|
||||
export const baseSetschemas: FormSchema[] = [ |
||||
{ |
||||
field: 'nickname', |
||||
component: 'Input', |
||||
label: t('profile.user.nickname'), |
||||
colProps: { span: 18 } |
||||
}, |
||||
{ |
||||
field: 'mobile', |
||||
component: 'Input', |
||||
label: t('profile.user.mobile'), |
||||
colProps: { span: 18 } |
||||
}, |
||||
{ |
||||
field: 'email', |
||||
component: 'Input', |
||||
label: t('profile.user.email'), |
||||
colProps: { span: 18 } |
||||
}, |
||||
{ |
||||
field: 'sex', |
||||
component: 'RadioGroup', |
||||
componentProps: { |
||||
options: [ |
||||
{ label: '男', value: 1 }, |
||||
{ label: '女', value: 2 } |
||||
] |
||||
}, |
||||
label: t('profile.user.sex'), |
||||
colProps: { span: 18 } |
||||
} |
||||
] |
||||
|
||||
// 安全设置 list
|
||||
export const secureSettingList: ListItem[] = [ |
||||
{ |
||||
key: '1', |
||||
title: '账户密码', |
||||
description: '当前密码强度::强', |
||||
extra: '修改' |
||||
}, |
||||
{ |
||||
key: '2', |
||||
title: '密保手机', |
||||
description: '已绑定手机::138****8293', |
||||
extra: '修改' |
||||
}, |
||||
{ |
||||
key: '3', |
||||
title: '密保问题', |
||||
description: '未设置密保问题,密保问题可有效保护账户安全', |
||||
extra: '修改' |
||||
}, |
||||
{ |
||||
key: '4', |
||||
title: '备用邮箱', |
||||
description: '已绑定邮箱::ant***sign.com', |
||||
extra: '修改' |
||||
}, |
||||
{ |
||||
key: '5', |
||||
title: 'MFA 设备', |
||||
description: '未绑定 MFA 设备,绑定后,可以进行二次确认', |
||||
extra: '修改' |
||||
} |
||||
] |
||||
|
||||
// 账号绑定 list
|
||||
export const accountBindList: ListItem[] = [ |
||||
{ |
||||
key: '20', |
||||
title: '钉钉', |
||||
description: '当前未绑定钉钉账号', |
||||
extra: '绑定', |
||||
avatar: 'ri:dingding-fill', |
||||
color: '#2eabff' |
||||
}, |
||||
{ |
||||
key: '30', |
||||
title: '企业微信', |
||||
description: '当前未绑定企业微信', |
||||
extra: '绑定', |
||||
avatar: 'ri:wechat-line', |
||||
color: '#2eabff' |
||||
} |
||||
] |
||||
|
||||
// 新消息通知 list
|
||||
export const msgNotifyList: ListItem[] = [ |
||||
{ |
||||
key: '1', |
||||
title: '账户密码', |
||||
description: '其他用户的消息将以站内信的形式通知' |
||||
}, |
||||
{ |
||||
key: '2', |
||||
title: '系统消息', |
||||
description: '系统消息将以站内信的形式通知' |
||||
}, |
||||
{ |
||||
key: '3', |
||||
title: '待办任务', |
||||
description: '待办任务将以站内信的形式通知' |
||||
} |
||||
] |
||||
|
||||
export const passwordSchema: FormSchema[] = [ |
||||
{ |
||||
field: 'oldPassword', |
||||
label: '当前密码', |
||||
component: 'InputPassword', |
||||
required: true |
||||
}, |
||||
{ |
||||
field: 'newPassword', |
||||
label: '新密码', |
||||
component: 'StrengthMeter', |
||||
componentProps: { |
||||
placeholder: '新密码' |
||||
}, |
||||
rules: [ |
||||
{ |
||||
required: true, |
||||
message: '请输入新密码' |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
field: 'confirmPassword', |
||||
label: '确认密码', |
||||
component: 'InputPassword', |
||||
|
||||
dynamicRules: ({ values }) => { |
||||
return [ |
||||
{ |
||||
required: true, |
||||
validator: (_, value) => { |
||||
if (!value) { |
||||
return Promise.reject('密码不能为空') |
||||
} |
||||
if (value !== values.newPassword) { |
||||
return Promise.reject('两次输入的密码不一致!') |
||||
} |
||||
return Promise.resolve() |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
||||
] |
@ -0,0 +1,40 @@
|
||||
<template> |
||||
<ScrollContainer> |
||||
<div ref="wrapperRef" class="account-setting"> |
||||
<Tabs tab-position="left" :tabBarStyle="tabBarStyle"> |
||||
<template v-for="item in settingList" :key="item.key"> |
||||
<TabPane :tab="item.name"> |
||||
<BaseSetting v-if="item.component == 'BaseSetting'" /> |
||||
<SecureSetting v-if="item.component == 'SecureSetting'" /> |
||||
<AccountBind v-if="item.component == 'AccountBind'" /> |
||||
<MsgNotify v-if="item.component == 'MsgNotify'" /> |
||||
</TabPane> |
||||
</template> |
||||
</Tabs> |
||||
</div> |
||||
</ScrollContainer> |
||||
</template> |
||||
<script setup lang="ts"> |
||||
import { Tabs, TabPane } from 'ant-design-vue' |
||||
import { ScrollContainer } from '@/components/Container/index' |
||||
import { settingList } from './data' |
||||
import BaseSetting from './BaseSetting.vue' |
||||
import SecureSetting from './SecureSetting.vue' |
||||
import AccountBind from './AccountBind.vue' |
||||
import MsgNotify from './MsgNotify.vue' |
||||
const tabBarStyle = { width: '220px' } |
||||
</script> |
||||
<style lang="less"> |
||||
.account-setting { |
||||
margin: 12px; |
||||
background-color: @component-background; |
||||
|
||||
.base-title { |
||||
padding-left: 0; |
||||
} |
||||
|
||||
.ant-tabs-tab-active { |
||||
background-color: @item-active-bg; |
||||
} |
||||
} |
||||
</style> |
@ -1,26 +0,0 @@
|
||||
<template> |
||||
<BasicModal :width="800" :title="t('sys.errorLog.tableActionDesc')" v-bind="$attrs"> |
||||
<Description :data="info" @register="register" /> |
||||
</BasicModal> |
||||
</template> |
||||
<script lang="ts" setup> |
||||
import type { ErrorLogInfo } from '@/types/store' |
||||
import { BasicModal } from '@/components/Modal' |
||||
import { Description, useDescription } from '@/components/Description' |
||||
import { useI18n } from '@/hooks/web/useI18n' |
||||
import { getDescSchema } from './data' |
||||
|
||||
defineProps({ |
||||
info: { |
||||
type: Object as PropType<ErrorLogInfo>, |
||||
default: null |
||||
} |
||||
}) |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const [register] = useDescription({ |
||||
column: 2, |
||||
schema: getDescSchema()! |
||||
}) |
||||
</script> |
@ -1,67 +0,0 @@
|
||||
import { Tag } from 'ant-design-vue' |
||||
import { BasicColumn } from '@/components/Table' |
||||
import { ErrorTypeEnum } from '@/enums/exceptionEnum' |
||||
import { useI18n } from '@/hooks/web/useI18n' |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
export function getColumns(): BasicColumn[] { |
||||
return [ |
||||
{ |
||||
dataIndex: 'type', |
||||
title: t('sys.errorLog.tableColumnType'), |
||||
width: 80, |
||||
customRender: ({ text }) => { |
||||
const color = |
||||
text === ErrorTypeEnum.VUE |
||||
? 'green' |
||||
: text === ErrorTypeEnum.RESOURCE |
||||
? 'cyan' |
||||
: text === ErrorTypeEnum.PROMISE |
||||
? 'blue' |
||||
: ErrorTypeEnum.AJAX |
||||
? 'red' |
||||
: 'purple' |
||||
return <Tag color={color}>{() => text}</Tag> |
||||
} |
||||
}, |
||||
{ |
||||
dataIndex: 'url', |
||||
title: 'URL', |
||||
width: 200 |
||||
}, |
||||
{ |
||||
dataIndex: 'time', |
||||
title: t('sys.errorLog.tableColumnDate'), |
||||
width: 160 |
||||
}, |
||||
{ |
||||
dataIndex: 'file', |
||||
title: t('sys.errorLog.tableColumnFile'), |
||||
width: 200 |
||||
}, |
||||
{ |
||||
dataIndex: 'name', |
||||
title: 'Name', |
||||
width: 200 |
||||
}, |
||||
{ |
||||
dataIndex: 'message', |
||||
title: t('sys.errorLog.tableColumnMsg'), |
||||
width: 300 |
||||
}, |
||||
{ |
||||
dataIndex: 'stack', |
||||
title: t('sys.errorLog.tableColumnStackMsg') |
||||
} |
||||
] |
||||
} |
||||
|
||||
export function getDescSchema(): any { |
||||
return getColumns().map((column) => { |
||||
return { |
||||
field: column.dataIndex!, |
||||
label: column.title |
||||
} |
||||
}) |
||||
} |
@ -1,97 +0,0 @@
|
||||
<template> |
||||
<div class="p-4"> |
||||
<template v-for="src in imgList" :key="src"> |
||||
<img :src="src" v-show="false" alt="" /> |
||||
</template> |
||||
<DetailModal :info="rowInfo" @register="registerModal" /> |
||||
<BasicTable @register="register" class="error-handle-table"> |
||||
<template #toolbar> |
||||
<a-button @click="fireVueError" type="primary"> |
||||
{{ t('sys.errorLog.fireVueError') }} |
||||
</a-button> |
||||
<a-button @click="fireResourceError" type="primary"> |
||||
{{ t('sys.errorLog.fireResourceError') }} |
||||
</a-button> |
||||
<a-button @click="fireAjaxError" type="primary"> |
||||
{{ t('sys.errorLog.fireAjaxError') }} |
||||
</a-button> |
||||
</template> |
||||
<template #bodyCell="{ column, record }"> |
||||
<template v-if="column.key === 'action'"> |
||||
<TableAction |
||||
:actions="[ |
||||
{ |
||||
label: t('sys.errorLog.tableActionDesc'), |
||||
onClick: handleDetail.bind(null, record) |
||||
} |
||||
]" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</BasicTable> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import type { ErrorLogInfo } from '@/types/store' |
||||
import { watch, ref, nextTick } from 'vue' |
||||
import DetailModal from './DetailModal.vue' |
||||
import { BasicTable, useTable, TableAction } from '@/components/Table' |
||||
import { useModal } from '@/components/Modal' |
||||
import { useMessage } from '@/hooks/web/useMessage' |
||||
import { useI18n } from '@/hooks/web/useI18n' |
||||
import { useErrorLogStore } from '@/store/modules/errorLog' |
||||
import { fireErrorApi } from '@/api/demo/error' |
||||
import { getColumns } from './data' |
||||
import { cloneDeep } from 'lodash-es' |
||||
|
||||
const rowInfo = ref<ErrorLogInfo>() |
||||
const imgList = ref<string[]>([]) |
||||
|
||||
const { t } = useI18n() |
||||
const errorLogStore = useErrorLogStore() |
||||
const [register, { setTableData }] = useTable({ |
||||
title: t('sys.errorLog.tableTitle'), |
||||
columns: getColumns(), |
||||
actionColumn: { |
||||
width: 80, |
||||
title: 'Action', |
||||
dataIndex: 'action' |
||||
// slots: { customRender: 'action' }, |
||||
} |
||||
}) |
||||
const [registerModal, { openModal }] = useModal() |
||||
|
||||
watch( |
||||
() => errorLogStore.getErrorLogInfoList, |
||||
(list) => { |
||||
nextTick(() => { |
||||
setTableData(cloneDeep(list)) |
||||
}) |
||||
}, |
||||
{ |
||||
immediate: true |
||||
} |
||||
) |
||||
const { createMessage } = useMessage() |
||||
if (import.meta.env.DEV) { |
||||
createMessage.info(t('sys.errorLog.enableMessage')) |
||||
} |
||||
// 查看详情 |
||||
function handleDetail(row: ErrorLogInfo) { |
||||
rowInfo.value = row |
||||
openModal(true) |
||||
} |
||||
|
||||
function fireVueError() { |
||||
throw new Error('fire vue error!') |
||||
} |
||||
|
||||
function fireResourceError() { |
||||
imgList.value.push(`${new Date().getTime()}.png`) |
||||
} |
||||
|
||||
async function fireAjaxError() { |
||||
await fireErrorApi() |
||||
} |
||||
</script> |
Reference in new issue