mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-11-11 02:57:09 +08:00
feat: 重构个人消息中心,支持展示个人公告,并优化相关地址
This commit is contained in:
289
src/views/user/components/VerifyModel.vue
Normal file
289
src/views/user/components/VerifyModel.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible" :title="title" :mask-closable="false" :esc-to-close="false"
|
||||
:width="width >= 500 ? 500 : '100%'" draggable @before-ok="save" @ok="saveAfter" @close="reset"
|
||||
>
|
||||
<GiForm ref="formRef" v-model="form" :columns="columns">
|
||||
<template #captcha>
|
||||
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="6" allow-clear style="flex: 1 1" />
|
||||
<a-button
|
||||
class="captcha-btn" :loading="captchaLoading" :disabled="captchaDisable" size="large"
|
||||
@click="onCaptcha"
|
||||
>
|
||||
{{ captchaBtnName }}
|
||||
</a-button>
|
||||
</template>
|
||||
</GiForm>
|
||||
<Verify
|
||||
ref="VerifyRef" :captcha-type="captchaType" :mode="captchaMode"
|
||||
:img-size="{ width: '330px', height: '155px' }" @success="getCaptcha"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import NProgress from 'nprogress'
|
||||
import { type BehaviorCaptchaReq, getEmailCaptcha, updateUserEmail, updateUserPassword, updateUserPhone } from '@/apis'
|
||||
import { encryptByRsa } from '@/utils/encrypt'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { type ColumnItem, GiForm } from '@/components/GiForm'
|
||||
import { useResetReactive } from '@/hooks'
|
||||
import * as Regexp from '@/utils/regexp'
|
||||
import modalErrorWrapper from '@/utils/modal-error-wrapper'
|
||||
import router from '@/router'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
const verifyType = ref()
|
||||
const title = computed(
|
||||
() => `修改${verifyType.value === 'phone' ? '手机号' : verifyType.value === 'email' ? '邮箱' : '密码'}`,
|
||||
)
|
||||
const formRef = ref<InstanceType<typeof GiForm>>()
|
||||
|
||||
const [form, resetForm] = useResetReactive({
|
||||
phone: '',
|
||||
email: '',
|
||||
captcha: '',
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
rePassword: '',
|
||||
})
|
||||
|
||||
const columns: ColumnItem[] = reactive([
|
||||
{
|
||||
label: '手机号',
|
||||
field: 'phone',
|
||||
type: 'input',
|
||||
span: 24,
|
||||
props: {
|
||||
showWordLimit: false,
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ match: Regexp.Phone, message: '请输入正确的手机号' },
|
||||
],
|
||||
hide: () => {
|
||||
return verifyType.value !== 'phone'
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '邮箱',
|
||||
field: 'email',
|
||||
type: 'input',
|
||||
span: 24,
|
||||
rules: [
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ match: Regexp.Email, message: '请输入正确的邮箱' },
|
||||
],
|
||||
hide: () => {
|
||||
return verifyType.value !== 'email'
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '验证码',
|
||||
field: 'captcha',
|
||||
type: 'input',
|
||||
span: 24,
|
||||
rules: [{ required: true, message: '请输入验证码' }],
|
||||
hide: () => {
|
||||
return !['phone', 'email'].includes(verifyType.value)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '当前密码',
|
||||
field: 'oldPassword',
|
||||
type: 'input-password',
|
||||
span: 24,
|
||||
rules: [{ required: true, message: '请输入当前密码' }],
|
||||
hide: () => {
|
||||
return !userInfo.value.pwdResetTime
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '新密码',
|
||||
field: 'newPassword',
|
||||
type: 'input-password',
|
||||
span: 24,
|
||||
rules: [
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{
|
||||
validator: (value, callback) => {
|
||||
if (value === form.oldPassword) {
|
||||
callback('新密码不能与当前密码相同')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
hide: () => {
|
||||
return verifyType.value !== 'password'
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '确认新密码',
|
||||
field: 'rePassword',
|
||||
type: 'input-password',
|
||||
span: 24,
|
||||
props: {
|
||||
placeholder: '请再次输入新密码',
|
||||
},
|
||||
rules: [
|
||||
{ required: true, message: '请再次输入新密码' },
|
||||
{
|
||||
validator: (value, callback) => {
|
||||
if (value !== form.newPassword) {
|
||||
callback('两次输入的密码不一致')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
hide: () => {
|
||||
return verifyType.value !== 'password'
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const VerifyRef = ref<InstanceType<any>>()
|
||||
const captchaType = ref('blockPuzzle')
|
||||
const captchaMode = ref('pop')
|
||||
const captchaLoading = ref(false)
|
||||
// 弹出行为验证码
|
||||
const onCaptcha = async () => {
|
||||
if (captchaLoading.value) return
|
||||
const isInvalid = await formRef.value?.formRef?.validateField(verifyType.value === 'phone' ? 'phone' : 'email')
|
||||
if (isInvalid) return
|
||||
VerifyRef.value.show()
|
||||
}
|
||||
|
||||
const captchaTimer = ref()
|
||||
const captchaTime = ref(60)
|
||||
const captchaBtnName = ref('获取验证码')
|
||||
const captchaDisable = ref(false)
|
||||
// 重置验证码
|
||||
const resetCaptcha = () => {
|
||||
window.clearInterval(captchaTimer.value)
|
||||
captchaTime.value = 60
|
||||
captchaBtnName.value = '获取验证码'
|
||||
captchaDisable.value = false
|
||||
}
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
formRef.value?.formRef?.resetFields()
|
||||
resetForm()
|
||||
resetCaptcha()
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
const getCaptcha = async (captchaReq: BehaviorCaptchaReq) => {
|
||||
// 发送验证码
|
||||
try {
|
||||
captchaLoading.value = true
|
||||
captchaBtnName.value = '发送中...'
|
||||
if (verifyType.value === 'phone') {
|
||||
// await getSmsCaptcha(form.phone, captchaReq)
|
||||
} else if (verifyType.value === 'email') {
|
||||
await getEmailCaptcha(form.email, captchaReq)
|
||||
}
|
||||
captchaLoading.value = false
|
||||
captchaDisable.value = true
|
||||
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
|
||||
// Message.success('发送成功')
|
||||
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
|
||||
captchaTimer.value = window.setInterval(() => {
|
||||
captchaTime.value -= 1
|
||||
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
|
||||
if (captchaTime.value <= 0) {
|
||||
resetCaptcha()
|
||||
}
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
resetCaptcha()
|
||||
} finally {
|
||||
captchaLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
const save = async () => {
|
||||
const isInvalid = await formRef.value?.formRef?.validate()
|
||||
if (isInvalid) return false
|
||||
try {
|
||||
if (verifyType.value === 'phone') {
|
||||
await updateUserPhone({
|
||||
phone: form.phone,
|
||||
captcha: form.captcha,
|
||||
oldPassword: encryptByRsa(form.oldPassword) as string,
|
||||
})
|
||||
Message.success('修改成功')
|
||||
} else if (verifyType.value === 'email') {
|
||||
await updateUserEmail({
|
||||
email: form.email,
|
||||
captcha: form.captcha,
|
||||
oldPassword: encryptByRsa(form.oldPassword) as string,
|
||||
})
|
||||
Message.success('修改成功')
|
||||
} else if (verifyType.value === 'password') {
|
||||
if (form.newPassword !== form.rePassword) {
|
||||
Message.error('两次新密码不一致')
|
||||
return false
|
||||
}
|
||||
if (form.newPassword === form.oldPassword) {
|
||||
Message.error('新密码与旧密码不能相同')
|
||||
return false
|
||||
}
|
||||
await updateUserPassword({
|
||||
oldPassword: encryptByRsa(form.oldPassword) || '',
|
||||
newPassword: encryptByRsa(form.newPassword) || '',
|
||||
})
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const saveAfter = async () => {
|
||||
if (verifyType.value === 'password') {
|
||||
modalErrorWrapper({
|
||||
title: '提示',
|
||||
content: '密码修改成功! 请保存好新密码,并使用新密码重新登录',
|
||||
maskClosable: false,
|
||||
escToClose: false,
|
||||
okText: '重新登录',
|
||||
async onOk() {
|
||||
NProgress.done()
|
||||
const userStore = useUserStore()
|
||||
await userStore.logoutCallBack()
|
||||
await router.replace('/login')
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 修改成功后,重新获取用户信息
|
||||
await userStore.getInfo()
|
||||
}
|
||||
}
|
||||
|
||||
const visible = ref(false)
|
||||
// 打开弹框
|
||||
const open = (type: string) => {
|
||||
verifyType.value = type
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.captcha-btn {
|
||||
margin-left: 12px;
|
||||
min-width: 98px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
140
src/views/user/message/components/MyMessage.vue
Normal file
140
src/views/user/message/components/MyMessage.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<GiTable
|
||||
row-key="id"
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
|
||||
:pagination="pagination"
|
||||
:disabled-tools="['size', 'setting']"
|
||||
:row-selection="{ type: 'checkbox', showCheckedAll: true }"
|
||||
:selected-keys="selectedKeys"
|
||||
@select-all="selectAll"
|
||||
@select="select"
|
||||
@refresh="search"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-input-search v-model="queryForm.title" placeholder="搜索标题" allow-clear @search="search" />
|
||||
<a-select
|
||||
v-model="queryForm.isRead"
|
||||
placeholder="全部状态"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
@change="search"
|
||||
>
|
||||
<a-option :value="false">未读</a-option>
|
||||
<a-option :value="true">已读</a-option>
|
||||
</a-select>
|
||||
<a-button @click="reset">
|
||||
<template #icon><icon-refresh /></template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #toolbar-right>
|
||||
<a-button type="primary" status="danger" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onDelete">
|
||||
<template #icon><icon-delete /></template>
|
||||
<template #default>删除</template>
|
||||
</a-button>
|
||||
<a-button type="primary" :disabled="!selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onRead">
|
||||
<template #default>标记为已读</template>
|
||||
</a-button>
|
||||
<a-button type="primary" :disabled="selectedKeys.length" :title="!selectedKeys.length ? '请选择' : ''" @click="onReadAll">
|
||||
<template #default>全部已读</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #title="{ record }">
|
||||
<a-tooltip :content="record.content"><span>{{ record.title }}</span></a-tooltip>
|
||||
</template>
|
||||
<template #isRead="{ record }">
|
||||
<a-tag :color="record.isRead ? '' : 'arcoblue'">
|
||||
{{ record.isRead ? '已读' : '未读' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #type="{ record }">
|
||||
<GiCellTag :value="record.type" :dict="message_type" />
|
||||
</template>
|
||||
</GiTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { type MessageQuery, deleteMessage, listMessage, readMessage } from '@/apis'
|
||||
import { useTable } from '@/hooks'
|
||||
import { useDict } from '@/hooks/app'
|
||||
|
||||
defineOptions({ name: 'SystemMessage' })
|
||||
|
||||
const { message_type } = useDict('message_type')
|
||||
|
||||
const queryForm = reactive<MessageQuery>({
|
||||
sort: ['createTime,desc'],
|
||||
})
|
||||
|
||||
const {
|
||||
tableData: dataList,
|
||||
loading,
|
||||
pagination,
|
||||
selectedKeys,
|
||||
select,
|
||||
selectAll,
|
||||
search,
|
||||
handleDelete,
|
||||
} = useTable((page) => listMessage({ ...queryForm, ...page }), { immediate: true })
|
||||
|
||||
const columns: TableInstance['collumns'] = [
|
||||
{
|
||||
title: '序号',
|
||||
width: 66,
|
||||
align: 'center',
|
||||
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
|
||||
},
|
||||
{ title: '标题', dataIndex: 'title', slotName: 'title', minWidth: 100, ellipsis: true, tooltip: true },
|
||||
{ title: '状态', dataIndex: 'isRead', slotName: 'isRead', align: 'center' },
|
||||
{ title: '时间', dataIndex: 'createTime', width: 180 },
|
||||
{ title: '类型', dataIndex: 'type', slotName: 'type', width: 180, ellipsis: true, tooltip: true },
|
||||
]
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
queryForm.title = undefined
|
||||
queryForm.type = undefined
|
||||
queryForm.isRead = undefined
|
||||
search()
|
||||
}
|
||||
|
||||
// 删除
|
||||
const onDelete = () => {
|
||||
if (!selectedKeys.value.length) {
|
||||
return Message.warning('请选择数据')
|
||||
}
|
||||
return handleDelete(() => deleteMessage(selectedKeys.value), { showModal: false, multiple: true })
|
||||
}
|
||||
|
||||
// 标记为已读
|
||||
const onRead = async () => {
|
||||
if (!selectedKeys.value.length) {
|
||||
return Message.warning('请选择数据')
|
||||
}
|
||||
await readMessage(selectedKeys.value)
|
||||
Message.success('操作成功')
|
||||
search()
|
||||
}
|
||||
|
||||
// 全部已读
|
||||
const onReadAll = async () => {
|
||||
Modal.warning({
|
||||
title: '全部已读',
|
||||
content: '确定要标记全部消息为已读吗?',
|
||||
hideCancel: false,
|
||||
maskClosable: false,
|
||||
onOk: async () => {
|
||||
await readMessage([])
|
||||
Message.success('操作成功')
|
||||
search()
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
95
src/views/user/message/components/MyNotice.vue
Normal file
95
src/views/user/message/components/MyNotice.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<GiTable
|
||||
row-key="id"
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%' }"
|
||||
:pagination="pagination"
|
||||
:disabled-tools="['size', 'setting']"
|
||||
@refresh="search"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-input-search v-model="queryForm.title" placeholder="搜索标题" allow-clear @search="search" />
|
||||
<a-select
|
||||
v-model="queryForm.type"
|
||||
:options="notice_type"
|
||||
placeholder="全部类型"
|
||||
allow-clear
|
||||
style="width: 150px"
|
||||
@change="search"
|
||||
/>
|
||||
<a-button @click="reset">
|
||||
<template #icon><icon-refresh /></template>
|
||||
<template #default>重置</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<template #title="{ record }">
|
||||
<a-link @click="onDetail(record)">
|
||||
<a-typography-paragraph
|
||||
class="link-text"
|
||||
:ellipsis="{
|
||||
rows: 1,
|
||||
showTooltip: true,
|
||||
css: true,
|
||||
}"
|
||||
>
|
||||
{{ record.title }}
|
||||
</a-typography-paragraph>
|
||||
</a-link>
|
||||
</template>
|
||||
<template #type="{ record }">
|
||||
<GiCellTag :value="record.type" :dict="notice_type" />
|
||||
</template>
|
||||
</GiTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import { type NoticeQuery, type NoticeResp, listUserNotice } from '@/apis/system'
|
||||
import { useTable } from '@/hooks'
|
||||
import { useDict } from '@/hooks/app'
|
||||
|
||||
defineOptions({ name: 'SystemMessage' })
|
||||
|
||||
const { notice_type } = useDict('notice_type')
|
||||
|
||||
const queryForm = reactive<NoticeQuery>({
|
||||
sort: ['createTime,desc'],
|
||||
})
|
||||
|
||||
const {
|
||||
tableData: dataList,
|
||||
loading,
|
||||
pagination,
|
||||
search,
|
||||
} = useTable((page) => listUserNotice({ ...queryForm, ...page }), { immediate: true })
|
||||
|
||||
const columns: TableInstance['columns'] = [
|
||||
{
|
||||
title: '序号',
|
||||
width: 66,
|
||||
align: 'center',
|
||||
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
|
||||
},
|
||||
{ title: '标题', dataIndex: 'title', slotName: 'title', ellipsis: true, tooltip: true },
|
||||
{ title: '类型', dataIndex: 'type', slotName: 'type', align: 'center' },
|
||||
{ title: '发布人', dataIndex: 'createUserString', ellipsis: true, tooltip: true },
|
||||
{ title: '发布时间', dataIndex: 'createTime', width: 180 },
|
||||
]
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
queryForm.title = undefined
|
||||
queryForm.type = undefined
|
||||
search()
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
// 详情
|
||||
const onDetail = (record: NoticeResp) => {
|
||||
router.push({ path: '/user/notice', query: { id: record.id } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
123
src/views/user/message/components/detail/components/index.vue
Normal file
123
src/views/user/message/components/detail/components/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<!-- 未完善 -->
|
||||
<template>
|
||||
<div ref="divRef" class="container">
|
||||
<div class="aie-container">
|
||||
<div class="aie-header-panel" style="display: none;">
|
||||
<div class="aie-container-header"></div>
|
||||
</div>
|
||||
<div class="aie-main">
|
||||
<div class="aie-container-panel">
|
||||
<div class="aie-container-main"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aie-container-footer" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AiEditor, type AiEditorOptions } from 'aieditor'
|
||||
import 'aieditor/dist/style.css'
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
defineOptions({ name: 'AiEditor' })
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
options?: AiEditorOptions
|
||||
}>()
|
||||
const aieditor = ref<AiEditor | null>(null)
|
||||
const appStore = useAppStore()
|
||||
const divRef = ref<any>()
|
||||
|
||||
const editorConfig = reactive<AiEditorOptions>({
|
||||
element: '',
|
||||
theme: appStore.theme,
|
||||
placeholder: '请输入内容',
|
||||
content: '',
|
||||
editable: false,
|
||||
})
|
||||
const init = () => {
|
||||
aieditor.value?.destroy()
|
||||
aieditor.value = new AiEditor(editorConfig)
|
||||
}
|
||||
watch(() => props.modelValue, (value) => {
|
||||
if (value !== aieditor.value?.getHtml()) {
|
||||
editorConfig.content = value
|
||||
init()
|
||||
}
|
||||
})
|
||||
watch(() => appStore.theme, (value) => {
|
||||
editorConfig.theme = value
|
||||
init()
|
||||
})
|
||||
|
||||
// 挂载阶段
|
||||
onMounted(() => {
|
||||
editorConfig.element = divRef.value
|
||||
init()
|
||||
})
|
||||
// 销毁阶段
|
||||
onUnmounted(() => {
|
||||
aieditor.value?.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aie-header-panel {
|
||||
position: sticky;
|
||||
// top: 51px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.aie-header-panel aie-header>div {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.aie-container {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.aie-container-panel {
|
||||
width: calc(100% - 2rem - 2px);
|
||||
max-width: 826.77px;
|
||||
margin: 0rem auto;
|
||||
border: 1px solid var(--color-border-1);
|
||||
background-color: var() rgba($color: var(--color-bg-1), $alpha: 1.0);
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
z-index: 99;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aie-main {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem 0px;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
.aie-directory {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 10px;
|
||||
width: 260px;
|
||||
z-index: 0;
|
||||
|
||||
}
|
||||
|
||||
.aie-title1 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
94
src/views/user/message/components/detail/index.vue
Normal file
94
src/views/user/message/components/detail/index.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="detail">
|
||||
<div class="detail_header">
|
||||
<a-affix :target="(containerRef as HTMLElement)">
|
||||
<a-page-header title="通知公告" subtitle="查看" @back="onBack">
|
||||
</a-page-header>
|
||||
</a-affix>
|
||||
</div>
|
||||
<div class="detail_content">
|
||||
<h1 class="title">{{ form?.title }}</h1>
|
||||
<div class="info">
|
||||
<a-space>
|
||||
<span>
|
||||
<icon-user class="icon" />
|
||||
<span class="label">发布人:</span>
|
||||
<span>{{ form?.createUserString }}</span>
|
||||
</span>
|
||||
<a-divider direction="vertical" />
|
||||
<span>
|
||||
<icon-history class="icon" />
|
||||
<span class="label">发布时间:</span>
|
||||
<span>{{ form?.effectiveTime ? form?.effectiveTime : form?.createTime
|
||||
}}</span>
|
||||
</span>
|
||||
<a-divider v-if="form?.updateTime" direction="vertical" />
|
||||
<span v-if="form?.updateTime">
|
||||
<icon-schedule class="icon" />
|
||||
<span>更新时间:</span>
|
||||
<span>{{ form?.updateTime }}</span>
|
||||
</span>
|
||||
</a-space>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<AiEditor v-model="form.content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AiEditor from './components/index.vue'
|
||||
import { getUserNotice } from '@/apis/system/user-message'
|
||||
import { useTabsStore } from '@/stores'
|
||||
import { useResetReactive } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'UserNotice' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const tabsStore = useTabsStore()
|
||||
|
||||
const { id } = route.query
|
||||
const containerRef = ref<HTMLElement | null>()
|
||||
const [form, resetForm] = useResetReactive({
|
||||
title: '',
|
||||
createUserString: '',
|
||||
effectiveTime: '',
|
||||
createTime: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
// 回退
|
||||
const onBack = () => {
|
||||
router.back()
|
||||
tabsStore.closeCurrent(route.path)
|
||||
}
|
||||
|
||||
// 打开
|
||||
const onOpen = async (id: string) => {
|
||||
resetForm()
|
||||
const { data } = await getUserNotice(id)
|
||||
Object.assign(form, data)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onOpen(id as string)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail_content {
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
83
src/views/user/message/index.vue
Normal file
83
src/views/user/message/index.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<GiPageLayout :margin="true" :default-collapsed="false">
|
||||
<template v-if="isDesktop" #left>
|
||||
<a-tabs v-model:active-key="activeKey" position="left" hide-content @change="change">
|
||||
<a-tab-pane v-for="(item) in menuList" :key="item.key" :title="item.name"></a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
<a-tabs v-if="!isDesktop" v-model:active-key="activeKey" type="card-gutter" style="margin-bottom: 10px" position="top" hide-content @change="change">
|
||||
<a-tab-pane v-for="(item) in menuList" :key="item.key" :title="item.name"></a-tab-pane>
|
||||
</a-tabs>
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<component :is="menuList.find((item) => item.key === activeKey)?.value"></component>
|
||||
</transition>
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import MyMessage from './components/MyMessage.vue'
|
||||
import MyNotice from './components/MyNotice.vue'
|
||||
import { useDevice } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'UserMessage' })
|
||||
|
||||
const { isDesktop } = useDevice()
|
||||
|
||||
const menuList = [
|
||||
{ name: '我的消息', key: 'msg', value: MyMessage },
|
||||
{ name: '我的公告', key: 'notice', value: MyNotice },
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const activeKey = ref('msg')
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
if (route.query.tab) {
|
||||
activeKey.value = String(route.query.tab)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const change = (key: string | number) => {
|
||||
activeKey.value = key as string
|
||||
router.replace({ path: route.path, query: { tab: key } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.arco-tabs-nav-vertical.arco-tabs-nav-type-line .arco-tabs-tab) {
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-1);
|
||||
|
||||
.arco-tabs-tab-title {
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.arco-tabs-tab-active {
|
||||
background: rgba(var(--primary-6), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-vertical::before) {
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-vertical .arco-tabs-nav-ink) {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-vertical) {
|
||||
float: none;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
245
src/views/user/profile/BasicInfo.vue
Normal file
245
src/views/user/profile/BasicInfo.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<a-card title="基本信息" bordered class="gradient-card">
|
||||
<div class="body">
|
||||
<section>
|
||||
<a-upload
|
||||
:file-list="avatarList"
|
||||
accept="image/*"
|
||||
:show-file-list="false"
|
||||
list-type="picture-card"
|
||||
:show-upload-button="true"
|
||||
:on-before-upload="onBeforeUpload"
|
||||
>
|
||||
<template #upload-button>
|
||||
<Avatar :src="avatarList[0].url" :name="userStore.nickname" :size="100" trigger>
|
||||
<template #trigger-icon><icon-camera /></template>
|
||||
</Avatar>
|
||||
</template>
|
||||
</a-upload>
|
||||
<div class="name">
|
||||
<span style="margin-right: 10px">{{ userInfo.nickname }}</span>
|
||||
<icon-edit :size="16" class="btn" @click="onUpdate" />
|
||||
</div>
|
||||
<div class="id">
|
||||
<GiSvgIcon name="id" :size="16" />
|
||||
<span>{{ userInfo.id }}</span>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<a-descriptions :column="4" size="large">
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-user /><span style="margin-left: 5px">用户名</span></template>
|
||||
{{ userInfo.username }}
|
||||
<icon-man v-if="userInfo.gender === 1" style="color: #19bbf1" />
|
||||
<icon-woman v-else-if="userInfo.gender === 2" style="color: #fa7fa9" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-phone /><span style="margin-left: 5px">手机</span></template>
|
||||
{{ userInfo.phone || '暂无' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-email /><span style="margin-left: 5px">邮箱</span></template>
|
||||
{{ userInfo.email || '暂无' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-mind-mapping /><span style="margin-left: 5px">部门</span></template>
|
||||
{{ userInfo.deptName }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item :span="4">
|
||||
<template #label> <icon-user-group /><span style="margin-left: 5px">角色</span></template>
|
||||
{{ userInfo.roles.join(',') }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="footer">注册于 {{ userInfo.registrationDate }}</div>
|
||||
</a-card>
|
||||
|
||||
<a-modal v-model:visible="visible" title="上传头像" :width="width >= 400 ? 400 : '100%'" :footer="false" draggable @close="reset">
|
||||
<a-row>
|
||||
<a-col :span="14" style="width: 200px; height: 200px">
|
||||
<VueCropper
|
||||
ref="cropperRef"
|
||||
:img="options.img"
|
||||
:info="true"
|
||||
:auto-crop="options.autoCrop"
|
||||
:auto-crop-width="options.autoCropWidth"
|
||||
:auto-crop-height="options.autoCropHeight"
|
||||
:fixed-box="options.fixedBox"
|
||||
:fixed="options.fixed"
|
||||
:full="options.full"
|
||||
:center-box="options.centerBox"
|
||||
:can-move="options.canMove"
|
||||
:output-type="options.outputType"
|
||||
:output-size="options.outputSize"
|
||||
@real-time="handleRealTime"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="10" style="display: flex; justify-content: center">
|
||||
<div :style="previewStyle">
|
||||
<div :style="previews.div">
|
||||
<img :src="previews.url" :style="previews.img" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div style="text-align: center; padding-top: 30px">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleUpload">确定</a-button>
|
||||
<a-button @click="reset">取消</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
<BasicInfoUpdateModal ref="BasicInfoUpdateModalRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { type FileItem, Message } from '@arco-design/web-vue'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import BasicInfoUpdateModal from './BasicInfoUpdateModal.vue'
|
||||
import { uploadAvatar } from '@/apis/system'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { useUserStore } from '@/stores'
|
||||
import getAvatar from '@/utils/avatar'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
const avatar = {
|
||||
uid: '-2',
|
||||
name: 'avatar.png',
|
||||
url: userInfo.value.avatar,
|
||||
}
|
||||
const avatarList = ref<FileItem[]>([avatar])
|
||||
const fileRef = ref(reactive({ name: 'avatar.png' }))
|
||||
const options: cropperOptions = reactive({
|
||||
img: '',
|
||||
autoCrop: true,
|
||||
autoCropWidth: 160,
|
||||
autoCropHeight: 160,
|
||||
fixedBox: true,
|
||||
fixed: true,
|
||||
full: false,
|
||||
centerBox: true,
|
||||
canMove: true,
|
||||
outputSize: 1,
|
||||
outputType: 'png',
|
||||
})
|
||||
const visible = ref(false)
|
||||
// 打开裁剪框
|
||||
const onBeforeUpload = (file: File): boolean => {
|
||||
fileRef.value = file
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => {
|
||||
options.img = reader.result
|
||||
}
|
||||
visible.value = true
|
||||
return false
|
||||
}
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
fileRef.value = { name: '' }
|
||||
options.img = ''
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const previews: any = ref({})
|
||||
const previewStyle: any = ref({})
|
||||
// 实时预览
|
||||
const handleRealTime = (data: any) => {
|
||||
previewStyle.value = {
|
||||
width: `${data.w}px`,
|
||||
height: `${data.h}px`,
|
||||
overflow: 'hidden',
|
||||
margin: '0',
|
||||
zoom: 100 / data.h,
|
||||
borderRadius: '50%',
|
||||
}
|
||||
previews.value = data
|
||||
}
|
||||
|
||||
const cropperRef = ref()
|
||||
// 上传头像
|
||||
const handleUpload = async () => {
|
||||
cropperRef.value.getCropBlob((data: any) => {
|
||||
const formData = new FormData()
|
||||
formData.append('avatarFile', data, fileRef.value?.name)
|
||||
uploadAvatar(formData).then((res) => {
|
||||
userInfo.value.avatar = res.data.avatar
|
||||
avatarList.value[0].url = getAvatar(res.data.avatar, undefined)
|
||||
reset()
|
||||
Message.success('更新成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const BasicInfoUpdateModalRef = ref<InstanceType<typeof BasicInfoUpdateModal>>()
|
||||
// 修改基本信息
|
||||
const onUpdate = async () => {
|
||||
BasicInfoUpdateModalRef.value?.onUpdate()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.arco-avatar-trigger-icon-button) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: #e8f3ff;
|
||||
.arco-icon-camera {
|
||||
margin-top: 8px;
|
||||
color: rgb(var(--arcoblue-6));
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 28px 10px 20px 10px;
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& > section {
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 32px 0 50px;
|
||||
.name {
|
||||
font-size: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.id {
|
||||
span {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
padding: 0 6px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > footer .footer_item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 0 -16px;
|
||||
padding-top: 16px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
}
|
||||
</style>
|
||||
88
src/views/user/profile/BasicInfoUpdateModal.vue
Normal file
88
src/views/user/profile/BasicInfoUpdateModal.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="修改基本信息"
|
||||
:mask-closable="false"
|
||||
:esc-to-close="false"
|
||||
:width="width >= 500 ? 500 : '100%'"
|
||||
draggable
|
||||
@before-ok="save"
|
||||
@close="reset"
|
||||
>
|
||||
<GiForm ref="formRef" v-model="form" :columns="columns" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { updateUserBaseInfo } from '@/apis/system'
|
||||
import { type ColumnItem, GiForm } from '@/components/GiForm'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { useResetReactive } from '@/hooks'
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
const visible = ref(false)
|
||||
const formRef = ref<InstanceType<typeof GiForm>>()
|
||||
|
||||
const [form, resetForm] = useResetReactive({
|
||||
nickname: userInfo.value.nickname,
|
||||
gender: userInfo.value.gender,
|
||||
})
|
||||
|
||||
const columns: ColumnItem[] = reactive([
|
||||
{
|
||||
label: '昵称',
|
||||
field: 'nickname',
|
||||
type: 'input',
|
||||
span: 24,
|
||||
rules: [{ required: true, message: '请输入昵称' }],
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
field: 'gender',
|
||||
type: 'radio-group',
|
||||
span: 24,
|
||||
props: {
|
||||
options: [
|
||||
{ label: '男', value: 1 },
|
||||
{ label: '女', value: 2 },
|
||||
{ label: '未知', value: 0, disabled: true },
|
||||
],
|
||||
},
|
||||
rules: [{ required: true, message: '请选择性别' }],
|
||||
},
|
||||
])
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
formRef.value?.formRef?.resetFields()
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 保存
|
||||
const save = async () => {
|
||||
const isInvalid = await formRef.value?.formRef?.validate()
|
||||
if (isInvalid) return false
|
||||
try {
|
||||
await updateUserBaseInfo(form)
|
||||
Message.success('修改成功')
|
||||
// 修改成功后,重新获取用户信息
|
||||
await userStore.getInfo()
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = async () => {
|
||||
reset()
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ onUpdate })
|
||||
</script>
|
||||
85
src/views/user/profile/Security.vue
Normal file
85
src/views/user/profile/Security.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<a-card title="安全设置" bordered class="gradient-card">
|
||||
<div v-for="item in modeList" :key="item.title">
|
||||
<div class="item">
|
||||
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
|
||||
<div class="info">
|
||||
<div class="info-top">
|
||||
<span class="label">{{ item.title }}</span>
|
||||
<span class="bind">
|
||||
<icon-check-circle-fill v-if="item.status" :size="14" class="success" />
|
||||
<icon-exclamation-circle-fill v-else :size="14" class="warning" />
|
||||
<span style="font-size: 12px" :class="item.status ? 'success' : 'warning'">{{ item.statusString }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-desc">
|
||||
<span class="value">{{ item.value }}</span>
|
||||
{{ item.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-wrapper">
|
||||
<a-button
|
||||
v-if="item.jumpMode === 'modal'"
|
||||
class="btn"
|
||||
:type="item.status ? 'secondary' : 'primary'"
|
||||
@click="onUpdate(item.type, item.status)"
|
||||
>
|
||||
{{ ['password'].includes(item.type) || item.status ? '修改' : '绑定' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<VerifyModel ref="verifyModelRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ModeItem } from '../type'
|
||||
import VerifyModel from '../components/VerifyModel.vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
const modeList = ref<ModeItem[]>([])
|
||||
modeList.value = [
|
||||
{
|
||||
title: '安全手机',
|
||||
icon: 'phone-color',
|
||||
value: userInfo.value.phone,
|
||||
subtitle: `${userInfo.value.phone ? '' : '手机号'}可用于登录、身份验证、密码找回、通知接收`,
|
||||
type: 'phone',
|
||||
jumpMode: 'modal',
|
||||
status: !!userInfo.value.phone,
|
||||
statusString: userInfo.value.phone ? '已绑定' : '未绑定',
|
||||
},
|
||||
{
|
||||
title: '安全邮箱',
|
||||
icon: 'email-color',
|
||||
value: userInfo.value.email,
|
||||
subtitle: `${userInfo.value.email ? '' : '邮箱'}可用于登录、身份验证、密码找回、通知接收`,
|
||||
type: 'email',
|
||||
jumpMode: 'modal',
|
||||
status: !!userInfo.value.email,
|
||||
statusString: userInfo.value.email ? '已绑定' : '未绑定',
|
||||
},
|
||||
{
|
||||
title: '登录密码',
|
||||
icon: 'password-color',
|
||||
subtitle: userInfo.value.pwdResetTime ? `为了您的账号安全,建议定期修改密码` : '请设置密码,可通过账号+密码登录',
|
||||
type: 'password',
|
||||
jumpMode: 'modal',
|
||||
status: !!userInfo.value.pwdResetTime,
|
||||
statusString: userInfo.value.pwdResetTime ? '已设置' : '未设置',
|
||||
},
|
||||
]
|
||||
|
||||
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
|
||||
// 修改
|
||||
const onUpdate = (type: string) => {
|
||||
verifyModelRef.value?.open(type)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
105
src/views/user/profile/Social.vue
Normal file
105
src/views/user/profile/Social.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<a-card title="第三方账号" bordered class="gradient-card">
|
||||
<div v-for="item in modeList" :key="item.title">
|
||||
<div class="item">
|
||||
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
|
||||
<div class="info">
|
||||
<div class="info-top">
|
||||
<span class="label">{{ item.title }}</span>
|
||||
<span class="bind">
|
||||
<icon-check-circle-fill v-if="item.status" :size="14" class="success" />
|
||||
<icon-exclamation-circle-fill v-else :size="14" class="warning" />
|
||||
<span style="font-size: 12px" :class="item.status ? 'success' : 'warning'">{{
|
||||
item.status ? '已绑定' : '未绑定'
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-desc">
|
||||
<span class="value">{{ item.value }}</span>
|
||||
{{ item.subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-wrapper">
|
||||
<a-button
|
||||
v-if="item.jumpMode === 'modal'"
|
||||
class="btn"
|
||||
:type="item.status ? 'secondary' : 'primary'"
|
||||
@click="onUpdate(item.type, item.status)"
|
||||
>
|
||||
{{ item.status ? '修改' : '绑定' }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-else-if="item.jumpMode === 'link'"
|
||||
class="btn"
|
||||
:type="item.status ? 'secondary' : 'primary'"
|
||||
@click="onBinding(item.type, item.status)"
|
||||
>
|
||||
{{ item.status ? '解绑' : '绑定' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
<VerifyModel ref="verifyModelRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { ModeItem } from '../type'
|
||||
import VerifyModel from '../components/VerifyModel.vue'
|
||||
import { listUserSocial, socialAuth, unbindSocialAccount } from '@/apis'
|
||||
|
||||
const socialList = ref<any>([])
|
||||
const modeList = ref<ModeItem[]>([])
|
||||
|
||||
// 初始化数据
|
||||
const initData = () => {
|
||||
listUserSocial().then((res) => {
|
||||
socialList.value = res.data.map((el) => el.source)
|
||||
modeList.value = [
|
||||
{
|
||||
title: '绑定 Gitee',
|
||||
icon: 'gitee',
|
||||
subtitle: `${socialList.value.includes('GITEE') ? '' : '绑定后,'}可通过 Gitee 进行登录`,
|
||||
jumpMode: 'link',
|
||||
type: 'gitee',
|
||||
status: socialList.value.includes('GITEE'),
|
||||
},
|
||||
{
|
||||
title: '绑定 GitHub',
|
||||
icon: 'github',
|
||||
subtitle: `${socialList.value.includes('GITHUB') ? '' : '绑定后,'}可通过 GitHub 进行登录`,
|
||||
type: 'github',
|
||||
jumpMode: 'link',
|
||||
status: socialList.value.includes('GITHUB'),
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// 绑定
|
||||
const onBinding = (type: string, status: boolean) => {
|
||||
if (!status) {
|
||||
socialAuth(type).then((res) => {
|
||||
window.open(res.data.authorizeUrl, '_self')
|
||||
})
|
||||
} else {
|
||||
unbindSocialAccount(type).then(() => {
|
||||
initData()
|
||||
Message.success('解绑成功')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
|
||||
// 修改
|
||||
const onUpdate = (type: string) => {
|
||||
verifyModelRef.value?.open(type)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
31
src/views/user/profile/index.vue
Normal file
31
src/views/user/profile/index.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="gi_page">
|
||||
<a-row wrap :gutter="16" align="stretch">
|
||||
<a-col :xs="24" :sm="24" :md="10" :lg="10" :xl="7" :xxl="7">
|
||||
<LeftBox />
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :md="14" :lg="14" :xl="17" :xxl="17">
|
||||
<div>
|
||||
<PasswordPolicy />
|
||||
</div>
|
||||
<div style="margin-top: 16px">
|
||||
<RightBox />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LeftBox from './BasicInfo.vue'
|
||||
import RightBox from './Social.vue'
|
||||
import PasswordPolicy from './Security.vue'
|
||||
|
||||
defineOptions({ name: 'UserProfile' })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gi_page {
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
</style>
|
||||
10
src/views/user/type.ts
Normal file
10
src/views/user/type.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface ModeItem {
|
||||
title: string
|
||||
icon: string
|
||||
subtitle: string
|
||||
value?: string
|
||||
type: 'phone' | 'email' | 'gitee' | 'github'
|
||||
jumpMode?: 'link' | 'modal'
|
||||
status: boolean
|
||||
statusString: string
|
||||
}
|
||||
Reference in New Issue
Block a user