feat
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LoginData } from '@/api/types'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { useUserStore } from '@/store'
|
||||
import { useUserStore, useAppStore } from '@/store'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { ValidatedError } from '@arco-design/web-vue/es/form/interface'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
@@ -59,6 +59,7 @@ const { t } = useI18n()
|
||||
const errorMessage = ref('')
|
||||
const { loading, setLoading } = useLoading()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
@@ -76,12 +77,10 @@ const handleSubmit = async ({ errors, values }: { errors: Record<string, Validat
|
||||
setLoading(true)
|
||||
try {
|
||||
await userStore.login(values as LoginData)
|
||||
await appStore.fetchServerMenuConfig()
|
||||
const { redirect, ...othersQuery } = router.currentRoute.value.query
|
||||
router.push({
|
||||
name: (redirect as string) || 'Workplace',
|
||||
query: {
|
||||
...othersQuery,
|
||||
},
|
||||
path: '/overview'
|
||||
})
|
||||
Message.success(t('login.form.login.success'))
|
||||
const { rememberPassword } = loginConfig.value
|
||||
|
||||
5
src/views/ops/pages/help/index.vue
Normal file
5
src/views/ops/pages/help/index.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="menu-container">
|
||||
帮助中心
|
||||
</div>
|
||||
</template>
|
||||
5
src/views/ops/pages/overview/index.vue
Normal file
5
src/views/ops/pages/overview/index.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="pmn-container">
|
||||
overview
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="修改用户密码"
|
||||
:mask-closable="false"
|
||||
:width="500"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="用户账号">
|
||||
<a-input :model-value="user?.account" disabled />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="password" label="新密码">
|
||||
<a-input-password
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="confirmPassword" label="确认新密码">
|
||||
<a-input-password
|
||||
v-model="formData.confirmPassword"
|
||||
placeholder="请确认密码"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="handleClose">取消</a-button>
|
||||
<a-button type="primary" :loading="loading" @click="handleSubmit">
|
||||
确定
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { adminResetUserPassword } from '@/api/module/user'
|
||||
import type { UserItem } from '@/api/types'
|
||||
|
||||
const props = defineProps<{
|
||||
user: UserItem | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => !!props.user,
|
||||
set: (val) => emit('update:visible', val),
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
password: [
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ minLength: 6, message: '密码至少6个字符' },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认密码' },
|
||||
{
|
||||
validator: (value: string, callback: (error?: string) => void) => {
|
||||
if (value !== formData.password) {
|
||||
callback('两次密码输入不一致')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
watch(
|
||||
() => props.user,
|
||||
(val) => {
|
||||
if (val) {
|
||||
formData.password = ''
|
||||
formData.confirmPassword = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
loading.value = true
|
||||
const res = await adminResetUserPassword({
|
||||
id: props.user!.id!,
|
||||
password: formData.password,
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(res.message || '密码修改成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PasswordChangeDialog',
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="设置权限"
|
||||
:mask-closable="false"
|
||||
:width="600"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<div class="permission-header">
|
||||
<span>用户账号: {{ user?.account }}</span>
|
||||
</div>
|
||||
<div class="permission-content">
|
||||
<PermissionTreeItem
|
||||
v-if="permissionTree.length > 0"
|
||||
:items="permissionTree"
|
||||
:selected-permissions="selectedPermissions"
|
||||
:expanded-keys="expandedKeys"
|
||||
@change="handlePermissionChange"
|
||||
/>
|
||||
<a-empty v-else description="暂无权限数据" />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="handleClose">取消</a-button>
|
||||
<a-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||
确定
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import PermissionTreeItem from './PermissionTree.vue'
|
||||
import {
|
||||
getUserPmnTree,
|
||||
userPmn,
|
||||
userModifyPmn,
|
||||
} from '@/api/module/user'
|
||||
import type { UserItem } from '@/api/types'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
const props = defineProps<{
|
||||
user: UserItem | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const visible = computed({
|
||||
get: () => !!props.user,
|
||||
set: (val) => emit('update:visible', val),
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const permissionTree = ref<any[]>([])
|
||||
const selectedPermissions = ref<number[]>([])
|
||||
const originalPermissions = ref<number[]>([])
|
||||
const expandedKeys = ref<Set<number>>(new Set())
|
||||
|
||||
// 获取权限数据
|
||||
const fetchPermissions = async () => {
|
||||
if (!props.user?.id) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 并行获取权限树和用户当前权限
|
||||
const [treeRes, pmnRes] = await Promise.all([
|
||||
getUserPmnTree({ id: props.user.id, workspace: import.meta.env.VITE_APP_WORKSPACE || '' }),
|
||||
userPmn({
|
||||
id: props.user.id,
|
||||
workspace: import.meta.env.VITE_APP_WORKSPACE || '',
|
||||
}),
|
||||
])
|
||||
|
||||
console.log('treeRes', treeRes)
|
||||
if (treeRes.details) {
|
||||
permissionTree.value = treeRes.data
|
||||
// 默认展开第一层
|
||||
treeRes.data.forEach((item: any) => {
|
||||
expandedKeys.value.add(item.id)
|
||||
})
|
||||
}
|
||||
|
||||
if (pmnRes.data) {
|
||||
selectedPermissions.value = pmnRes.data.map((item: any) => item.pmn_id || item)
|
||||
originalPermissions.value = [...selectedPermissions.value]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取权限数据失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 user 变化
|
||||
watch(
|
||||
() => props.user,
|
||||
(val) => {
|
||||
if (val?.id) {
|
||||
fetchPermissions()
|
||||
} else {
|
||||
permissionTree.value = []
|
||||
selectedPermissions.value = []
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 处理权限变化
|
||||
const handlePermissionChange = (permissions: number[]) => {
|
||||
selectedPermissions.value = permissions
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!props.user?.id) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await userModifyPmn({
|
||||
id: props.user.id,
|
||||
list: selectedPermissions.value,
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('权限设置成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置权限失败', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PermissionSettingDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.permission-header {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background-color: var(--color-fill-1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.permission-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="permission-tree">
|
||||
<div v-for="item in items" :key="item.id" class="tree-node">
|
||||
<div class="node-content" :style="{ paddingLeft: `${level * 24}px` }">
|
||||
<!-- 展开/折叠按钮 -->
|
||||
<span
|
||||
v-if="hasChildren(item)"
|
||||
class="expand-icon"
|
||||
@click="toggleExpand(item.id)"
|
||||
>
|
||||
<icon-right v-if="!isExpanded(item.id)" />
|
||||
<icon-down v-else />
|
||||
</span>
|
||||
<span v-else class="expand-icon-placeholder" />
|
||||
|
||||
<!-- 复选框 -->
|
||||
<a-checkbox
|
||||
:model-value="isChecked(item.id)"
|
||||
:indeterminate="isIndeterminate(item)"
|
||||
@change="handleCheck(item.id, $event)"
|
||||
/>
|
||||
|
||||
<!-- 名称 -->
|
||||
<span class="node-name">{{ item.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 子节点 -->
|
||||
<div v-if="hasChildren(item) && isExpanded(item.id)" class="children">
|
||||
<PermissionTree
|
||||
:items="getChildren(item)"
|
||||
:selected-permissions="selectedPermissions"
|
||||
:level="level + 1"
|
||||
:expanded-keys="expandedKeys"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export interface PermissionItem {
|
||||
id: number
|
||||
name: string
|
||||
children?: PermissionItem[]
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: PermissionItem[]
|
||||
selectedPermissions: number[]
|
||||
level?: number
|
||||
expandedKeys?: Set<number>
|
||||
}>(),
|
||||
{
|
||||
level: 0,
|
||||
expandedKeys: () => new Set(),
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', permissions: number[]): void
|
||||
}>()
|
||||
|
||||
// 本地展开状态
|
||||
const localExpandedKeys = ref<Set<number>>(new Set())
|
||||
const expandedKeys = computed(() => props.expandedKeys.size > 0 ? props.expandedKeys : localExpandedKeys.value)
|
||||
|
||||
// 检查是否有子节点
|
||||
const hasChildren = (item: PermissionItem) => {
|
||||
return item.children && item.children.length > 0
|
||||
}
|
||||
|
||||
// 获取子节点
|
||||
const getChildren = (item: PermissionItem): PermissionItem[] => {
|
||||
return item.children || []
|
||||
}
|
||||
|
||||
// 检查是否展开
|
||||
const isExpanded = (id: number) => {
|
||||
return expandedKeys.value.has(id)
|
||||
}
|
||||
|
||||
// 切换展开状态
|
||||
const toggleExpand = (id: number) => {
|
||||
if (expandedKeys.value.has(id)) {
|
||||
expandedKeys.value.delete(id)
|
||||
} else {
|
||||
expandedKeys.value.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否选中
|
||||
const isChecked = (id: number) => {
|
||||
return props.selectedPermissions.includes(id)
|
||||
}
|
||||
|
||||
// 检查是否半选状态
|
||||
const isIndeterminate = (item: PermissionItem) => {
|
||||
if (!hasChildren(item)) return false
|
||||
const childIds = getAllChildIds(item)
|
||||
const selectedCount = childIds.filter((id) => props.selectedPermissions.includes(id)).length
|
||||
return selectedCount > 0 && selectedCount < childIds.length
|
||||
}
|
||||
|
||||
// 递归获取所有子节点ID
|
||||
const getAllChildIds = (item: PermissionItem): number[] => {
|
||||
const ids: number[] = []
|
||||
if (item.children) {
|
||||
item.children.forEach((child) => {
|
||||
ids.push(child.id)
|
||||
ids.push(...getAllChildIds(child))
|
||||
})
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// 根据ID查找权限项
|
||||
const findItemById = (items: PermissionItem[], id: number): PermissionItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.id === id) return item
|
||||
if (item.children) {
|
||||
const found = findItemById(item.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取父节点ID
|
||||
const getParentIds = (
|
||||
items: PermissionItem[],
|
||||
targetId: number,
|
||||
parentIds: number[] = []
|
||||
): number[] => {
|
||||
for (const item of items) {
|
||||
if (item.children) {
|
||||
if (item.children.some((child) => child.id === targetId)) {
|
||||
return [...parentIds, item.id]
|
||||
}
|
||||
const result = getParentIds(item.children, targetId, [...parentIds, item.id])
|
||||
if (result.length > 0) return result
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 处理选中状态变化
|
||||
const handleCheck = (id: number, checked: boolean) => {
|
||||
let newPermissions = [...props.selectedPermissions]
|
||||
|
||||
if (checked) {
|
||||
// 添加当前权限
|
||||
if (!newPermissions.includes(id)) {
|
||||
newPermissions.push(id)
|
||||
}
|
||||
|
||||
// 添加所有子权限
|
||||
const item = findItemById(props.items, id)
|
||||
if (item) {
|
||||
const childIds = getAllChildIds(item)
|
||||
childIds.forEach((childId) => {
|
||||
if (!newPermissions.includes(childId)) {
|
||||
newPermissions.push(childId)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 移除当前权限
|
||||
newPermissions = newPermissions.filter((pId) => pId !== id)
|
||||
|
||||
// 移除所有子权限
|
||||
const item = findItemById(props.items, id)
|
||||
if (item) {
|
||||
const childIds = getAllChildIds(item)
|
||||
newPermissions = newPermissions.filter((pId) => !childIds.includes(pId))
|
||||
}
|
||||
|
||||
// 移除父权限
|
||||
const parentIds = getParentIds(props.items, id)
|
||||
parentIds.forEach((parentId) => {
|
||||
newPermissions = newPermissions.filter((pId) => pId !== parentId)
|
||||
})
|
||||
}
|
||||
|
||||
emit('change', newPermissions)
|
||||
}
|
||||
|
||||
// 处理子组件变化
|
||||
const handleChange = (permissions: number[]) => {
|
||||
emit('change', permissions)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'PermissionTree',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.permission-tree {
|
||||
.tree-node {
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
|
||||
.expand-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-2);
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon-placeholder {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.children {
|
||||
// 子节点样式
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
409
src/views/ops/pages/system-settings/account-management/index.vue
Normal file
409
src/views/ops/pages/system-settings/account-management/index.vue
Normal file
@@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['用户管理']" />
|
||||
|
||||
<SearchTable
|
||||
title="用户管理"
|
||||
:form-model="searchForm"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="fetchData"
|
||||
>
|
||||
<!-- 工具栏左侧 - 新增按钮 -->
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><icon-plus /></template>
|
||||
新增用户
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleSetPermission(record)">
|
||||
权限
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleChangePassword(record)">
|
||||
改密
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.status === 1 ? 'green' : 'red'">
|
||||
{{ record.status === 1 ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</SearchTable>
|
||||
|
||||
<!-- 新增/编辑用户弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="modalTitle"
|
||||
:mask-closable="false"
|
||||
:width="520"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 16 }"
|
||||
>
|
||||
<a-form-item field="account" label="账号" validate-trigger="blur">
|
||||
<a-input
|
||||
v-model="formData.account"
|
||||
placeholder="请输入账号"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item field="name" label="姓名" validate-trigger="blur">
|
||||
<a-input v-model="formData.name" placeholder="请输入姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item field="email" label="邮箱" validate-trigger="blur">
|
||||
<a-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item field="phone" label="手机号" validate-trigger="blur">
|
||||
<a-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item field="status" label="状态">
|
||||
<a-select v-model="formData.status" placeholder="请选择状态">
|
||||
<a-option :value="1">正常</a-option>
|
||||
<a-option :value="-1">禁用</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="deleteConfirmVisible"
|
||||
title="删除确认"
|
||||
@ok="handleConfirmDelete"
|
||||
@cancel="deleteConfirmVisible = false"
|
||||
>
|
||||
<p>确定要删除用户 "{{ userToDelete?.name || userToDelete?.account }}" 吗?</p>
|
||||
</a-modal>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<PasswordChangeDialog
|
||||
:user="passwordChangeUser"
|
||||
@update:visible="passwordChangeUser = null"
|
||||
@success="fetchData"
|
||||
/>
|
||||
|
||||
<!-- 权限设置弹窗 -->
|
||||
<PermissionSettingDialog
|
||||
:user="permissionUser"
|
||||
@update:visible="handlePermissionDialogClose"
|
||||
@success="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import type { UserItem } from '@/api/types'
|
||||
import {
|
||||
fetchUserList,
|
||||
createUser,
|
||||
modifyUser,
|
||||
deleteUser,
|
||||
} from '@/api/module/user'
|
||||
import PasswordChangeDialog from './components/PasswordChangeDialog.vue'
|
||||
import PermissionSettingDialog from './components/PermissionSettingDialog.vue'
|
||||
|
||||
// 搜索表单配置
|
||||
const formItems: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入用户名/昵称/手机号',
|
||||
span: 8,
|
||||
},
|
||||
]
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'account',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
slotName: 'operation',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
},
|
||||
]
|
||||
|
||||
// 搜索表单数据
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<UserItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 弹窗相关
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const formData = reactive<UserItem>({
|
||||
id: undefined,
|
||||
account: '',
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
status: 1,
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
account: [
|
||||
{ required: true, message: '请输入账号' },
|
||||
{ minLength: 3, message: '账号至少3个字符' },
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入姓名' },
|
||||
],
|
||||
email: [
|
||||
{ match: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: '请输入正确的邮箱格式' },
|
||||
],
|
||||
phone: [
|
||||
{ match: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
|
||||
],
|
||||
}
|
||||
|
||||
// 删除确认
|
||||
const deleteConfirmVisible = ref(false)
|
||||
const userToDelete = ref<UserItem | null>(null)
|
||||
|
||||
// 修改密码
|
||||
const passwordChangeUser = ref<UserItem | null>(null)
|
||||
|
||||
// 权限设置
|
||||
const permissionUser = ref<UserItem | null>(null)
|
||||
|
||||
// 关闭权限设置弹窗
|
||||
const handlePermissionDialogClose = (visible: boolean) => {
|
||||
if (!visible) {
|
||||
permissionUser.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗标题
|
||||
const modalTitle = computed(() => (isEdit.value ? '编辑用户' : '新增用户'))
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await fetchUserList({
|
||||
page: pagination.current,
|
||||
size: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
})
|
||||
if (res?.code === 0) {
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user list:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.keyword = ''
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
resetFormData()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (record: UserItem) => {
|
||||
isEdit.value = true
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
account: record.account,
|
||||
name: record.name,
|
||||
email: record.email,
|
||||
phone: record.phone,
|
||||
status: record.status ?? 1,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = (record: UserItem) => {
|
||||
userToDelete.value = record
|
||||
deleteConfirmVisible.value = true
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
const handleChangePassword = (record: UserItem) => {
|
||||
passwordChangeUser.value = record
|
||||
}
|
||||
|
||||
// 权限设置
|
||||
const handleSetPermission = (record: UserItem) => {
|
||||
permissionUser.value = record
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!userToDelete.value?.id) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
await deleteUser({ id: userToDelete.value.id })
|
||||
Message.success('删除成功')
|
||||
deleteConfirmVisible.value = false
|
||||
userToDelete.value = null
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete user:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗确认
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
loading.value = true
|
||||
if (isEdit.value && formData.id) {
|
||||
await modifyUser(formData)
|
||||
Message.success('修改成功')
|
||||
} else {
|
||||
await createUser(formData)
|
||||
Message.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
console.error('Failed to save user:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗取消
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 重置表单数据
|
||||
const resetFormData = () => {
|
||||
Object.assign(formData, {
|
||||
id: undefined,
|
||||
account: '',
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
status: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AccountManagement',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -43,19 +43,34 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="component" label="组件路径">
|
||||
<a-input v-model="formData.component" placeholder="请输入组件路径" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<!-- 根菜单才显示图标选择 -->
|
||||
<a-col v-if="!parentId" :span="12">
|
||||
<a-form-item field="menu_icon" label="菜单图标" :rules="[{ required: true, message: '请选择菜单图标' }]">
|
||||
<a-input
|
||||
v-model="formData.menu_icon"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
@click="iconPickerVisible = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<component v-if="formData.menu_icon" :is="getIconComponent(formData.menu_icon)" :size="18" />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-form-item field="menu_icon" label="菜单图标" style="width: 100%">
|
||||
<a-input-group style="width: 100%;">
|
||||
<a-input
|
||||
v-model="formData.menu_icon"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
style="cursor: pointer"
|
||||
@click="iconPickerVisible = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<component v-if="formData.menu_icon" :is="getIconComponent(formData.menu_icon)" :size="18" />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button
|
||||
v-if="formData.menu_icon"
|
||||
@click="clearIcon"
|
||||
>
|
||||
<template #icon><icon-close /></template>
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
@@ -66,16 +81,29 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 路由配置 -->
|
||||
<a-divider orientation="left">路由配置</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="is_new_tab">
|
||||
<a-switch v-model="formData.is_new_tab" />
|
||||
<span class="switch-label">在新窗口打开</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="is_web_page">
|
||||
<a-switch v-model="formData.is_web_page" />
|
||||
<span class="switch-label">嵌入网页</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 网页嵌入配置 -->
|
||||
<a-divider orientation="left">网页嵌入配置</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-form-item field="is_web_page">
|
||||
<a-switch v-model="formData.is_web_page" />
|
||||
<span class="switch-label">是否为嵌入网页</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col v-if="formData.is_web_page" :span="24">
|
||||
<a-form-item field="web_url" label="网页地址">
|
||||
@@ -154,12 +182,14 @@ const formData = ref<MenuRouteRequest>({
|
||||
description: '',
|
||||
menu_icon: '',
|
||||
menu_path: '',
|
||||
component: '',
|
||||
parent_id: undefined,
|
||||
title: '',
|
||||
title_en: '',
|
||||
type: 1,
|
||||
is_web_page: false,
|
||||
web_url: '',
|
||||
is_new_tab: false,
|
||||
// 编辑时需要的额外字段
|
||||
id: undefined,
|
||||
identity: undefined,
|
||||
@@ -194,12 +224,14 @@ const resetForm = () => {
|
||||
description: '',
|
||||
menu_icon: '',
|
||||
menu_path: '',
|
||||
component: '',
|
||||
parent_id: props.parentId || undefined,
|
||||
title: '',
|
||||
title_en: '',
|
||||
type: 1,
|
||||
is_web_page: false,
|
||||
web_url: '',
|
||||
is_new_tab: false,
|
||||
id: undefined,
|
||||
identity: undefined,
|
||||
app_id: undefined,
|
||||
@@ -227,12 +259,14 @@ watch(
|
||||
description: initialValues.description || '',
|
||||
menu_icon: initialValues.menu_icon || '',
|
||||
menu_path: initialValues.menu_path || '',
|
||||
component: initialValues.component || '',
|
||||
parent_id: initialValues.parent_id,
|
||||
title: initialValues.title || '',
|
||||
title_en: initialValues.title_en || '',
|
||||
type: initialValues.type || 1,
|
||||
is_web_page: initialValues.is_web_page || false,
|
||||
web_url: initialValues.web_url || '',
|
||||
is_new_tab: initialValues.is_new_tab || false,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -253,6 +287,11 @@ const selectIcon = (icon: string) => {
|
||||
iconSearch.value = ''
|
||||
}
|
||||
|
||||
// 清空图标
|
||||
const clearIcon = () => {
|
||||
formData.value.menu_icon = ''
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
@@ -296,10 +335,11 @@ export default {
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
|
||||
@@ -16,66 +16,29 @@
|
||||
@drag-end="$emit('drag-end')"
|
||||
@drop="$emit('drop', $event)"
|
||||
/>
|
||||
|
||||
<!-- 根级别拖放区域 -->
|
||||
<div
|
||||
v-if="draggingNode"
|
||||
class="root-drop-zone"
|
||||
:class="{ 'drag-over': isDragOver }"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop.prevent="handleDropToRoot"
|
||||
>
|
||||
<icon-plus-circle /> 拖放到此处设为根级菜单
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import MenuTreeItem from './MenuTreeItem.vue'
|
||||
import type { MenuNode } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
nodes: MenuNode[]
|
||||
expandedKeys: Set<number>
|
||||
selectedKey: number | null
|
||||
draggingNode?: MenuNode | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
defineEmits<{
|
||||
(e: 'toggle', nodeId: number): void
|
||||
(e: 'select', node: MenuNode): void
|
||||
(e: 'add-child', parentId: number): void
|
||||
(e: 'delete', node: MenuNode): void
|
||||
(e: 'drag-start', node: MenuNode): void
|
||||
(e: 'drag-end'): void
|
||||
(e: 'drop', data: { dragNode: MenuNode; targetNode: MenuNode | null; position: 'before' | 'after' | 'inside' | 'root' }): void
|
||||
(e: 'drop', data: { dragNode: MenuNode; targetNode: MenuNode; position: 'before' | 'after' | 'inside' }): void
|
||||
}>()
|
||||
|
||||
const isDragOver = ref(false)
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDropToRoot = () => {
|
||||
isDragOver.value = false
|
||||
|
||||
if (props.draggingNode) {
|
||||
// 拖放到根级别
|
||||
emit('drop', {
|
||||
dragNode: props.draggingNode,
|
||||
targetNode: null,
|
||||
position: 'root'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -88,26 +51,4 @@ export default {
|
||||
.menu-tree {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.root-drop-zone {
|
||||
margin-top: 8px;
|
||||
padding: 16px;
|
||||
border: 2px dashed var(--color-border-2);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: var(--color-text-3);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover, &.drag-over {
|
||||
border-color: rgb(var(--primary-6));
|
||||
color: rgb(var(--primary-6));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.ops', 'menu.ops.systemSettings', 'menu.ops.systemSettings.menuManagement']" />
|
||||
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.menuManagement']" />
|
||||
<a-row :gutter="16" class="menu-container">
|
||||
<!-- 左侧: 菜单树 -->
|
||||
<a-col :xs="24" :md="10" :lg="8">
|
||||
@@ -286,7 +286,7 @@ const handleDragEnd = () => {
|
||||
}
|
||||
|
||||
// 处理放置
|
||||
const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | null; position: 'before' | 'after' | 'inside' | 'root' }) => {
|
||||
const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode; position: 'before' | 'after' | 'inside' }) => {
|
||||
const { dragNode, targetNode, position } = data
|
||||
|
||||
if (!dragNode.id) return
|
||||
@@ -294,56 +294,47 @@ const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | nul
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
if (position === 'root' && !targetNode) {
|
||||
// 拖放到根级别
|
||||
if (position === 'inside') {
|
||||
// 拖放到某个节点内部,成为其子节点
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: null
|
||||
parent_id: targetNode.id
|
||||
} as MenuRouteRequest)
|
||||
} else if (targetNode) {
|
||||
if (position === 'inside') {
|
||||
// 拖放到某个节点内部,成为其子节点
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.id
|
||||
} as MenuRouteRequest)
|
||||
} else {
|
||||
// 拖放到某个节点前后,需要更新排序
|
||||
const sortList: { pmn_id: number; sort_key: number }[] = []
|
||||
let sortKey = 1
|
||||
|
||||
// 获取同级节点
|
||||
const siblings = menuItems.value
|
||||
.filter(item => item.parent_id === targetNode.parent_id)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
siblings.forEach((item, index) => {
|
||||
if (item.id === targetNode.id) {
|
||||
if (position === 'before') {
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
} else {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
}
|
||||
} else if (item.id !== dragNode.id) {
|
||||
} else {
|
||||
// 拖放到某个节点前后,需要更新排序
|
||||
const sortList: { pmn_id: number; sort_key: number }[] = []
|
||||
let sortKey = 1
|
||||
|
||||
// 获取同级节点
|
||||
const siblings = menuItems.value
|
||||
.filter(item => item.parent_id === targetNode.parent_id)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
siblings.forEach((item) => {
|
||||
if (item.id === targetNode.id) {
|
||||
if (position === 'before') {
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
} else {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新拖拽节点的 parent_id
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.parent_id || null
|
||||
} as MenuRouteRequest)
|
||||
|
||||
// 更新排序
|
||||
if (sortList.length > 0) {
|
||||
await updateMenuOrder(sortList)
|
||||
} else if (item.id !== dragNode.id) {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新拖拽节点的 parent_id
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.parent_id || null
|
||||
} as MenuRouteRequest)
|
||||
|
||||
// 更新排序
|
||||
if (sortList.length > 0) {
|
||||
await updateMenuOrder(sortList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.ops', 'menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
||||
<Breadcrumb :items="['menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
||||
|
||||
<!-- 使用 SearchTable 公共组件 -->
|
||||
<SearchTable
|
||||
|
||||
Reference in New Issue
Block a user