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