Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions ui/src/api/admin/system/platform-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { get, post, put } from '../core/request'
import type { QrLoginPlatform, QrLoginPlatformRequest } from '@/api/types'

const prefix = '/platform'

/** 获取扫码登录平台配置。 */
const getQrLoginPlatforms = () => {
return get<QrLoginPlatform[]>(prefix)
}

/** 校验扫码登录平台配置是否可用。 */
const postQrLoginPlatformConnection = (platform: QrLoginPlatformRequest) => {
return post<QrLoginPlatformRequest, boolean>(`${prefix}/connection`, platform)
}

/** 保存扫码登录平台配置。 */
const putQrLoginPlatform = (platform: QrLoginPlatformRequest) => {
return put<QrLoginPlatformRequest, boolean>(prefix, platform)
}

export default {
getQrLoginPlatforms,
postQrLoginPlatformConnection,
putQrLoginPlatform,
}
15 changes: 15 additions & 0 deletions ui/src/api/types/system-authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,18 @@ export interface LoginAuthSetting {
system_options?: LoginMethodOption[]
workspace_id?: string
}

export type QrLoginPlatformType = 'wecom' | 'dingtalk' | 'lark'

export interface QrLoginPlatform {
auth_type: QrLoginPlatformType
config: Record<string, string>
is_active: boolean
is_valid: boolean
}

export interface QrLoginPlatformRequest {
config: Record<string, string>
isActive: boolean
key: QrLoginPlatformType
}
9 changes: 4 additions & 5 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ src/components/
│ │ └── index.vue # SVG Symbol 与 Element Plus 图标统一入口
│ ├── mk-view-layout/
│ │ ├── index.vue # 路由页面标题、操作区和内容区统一结构
│ │ ├── layout-aside.vue # 页面可选左侧栏结构
│ │ └── layout-main.ts # 右侧标题标记提取与滚动内容渲染
│ │ └── layout-aside.vue # 页面可选左侧栏结构
│ ├── mk-search-input/
│ │ └── index.vue # 带默认搜索图标的输入框
│ ├── mk-status-label/
Expand Down Expand Up @@ -253,9 +252,9 @@ Element Plus Dropdown 属性和事件通过 `$attrs` 传入,并暴露 `handleO

路由页面的通用内容结构,统一提供满高弹性布局及可选左侧栏。标题优先使用 `title` Prop,未传入
时读取当前路由的 `meta.title``aside` 和默认作用域插槽都提供 `title` 与对应区域的 `Header`
标记组件。默认插槽中的 `Header` 会由布局提取到右侧滚动区上方,其余节点统一放入
`el-scrollbar`;页面因此可以在同一个插槽中组织标题、内容和空状态,并只写一次业务状态判断。
传入 `aside` 插槽后才会渲染左侧栏;左右结构上方的独立内容放入 `top` 插槽。页面加载状态通过
包装组件,页面可以在同一个插槽中组织标题、内容和空状态,并只写一次业务状态判断。组件不统一
处理内容滚动,需要滚动的页面自行使用 `el-scrollbar`。传入 `aside` 插槽后才会渲染左侧栏;左右
结构上方的独立内容放入 `top` 插槽。页面加载状态通过
`loading` Prop 传入,由组件将 Element Plus Loading 遮罩绑定到整个布局根节点。默认插槽没有
声明作用域参数时会自动显示当前标题;使用 `#default="{ Header }"` 后则由页面通过
`<component :is="Header">` 控制标题,空状态分支不会自动补充标题。
Expand Down
42 changes: 31 additions & 11 deletions ui/src/components/global/mk-view-layout/index.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script setup lang="ts">
import type { Component, FunctionalComponent } from 'vue'
import { Fragment, h, isVNode, type FunctionalComponent, type VNode } from 'vue'
import { useRoute } from 'vue-router'
import LayoutAside from './layout-aside.vue'
import LayoutMain from './layout-main'

defineOptions({ name: 'MkViewLayout', inheritAttrs: false })

Expand All @@ -18,16 +17,39 @@ const props = withDefaults(

defineSlots<{
aside?: (props: { Header: FunctionalComponent; title: string }) => unknown
default?: (props: { Header: Component; title: string }) => unknown
default?: (props: { Header: FunctionalComponent; title: string }) => unknown
top?: () => unknown
}>()

const route = useRoute()
const title = computed(() => props.title || route.meta.title || '')
const slots = useSlots()
const fallbackTitle = computed(() =>
!slots.default || slots.default.length === 0 ? title.value : '',
)
const LayoutHeader: FunctionalComponent = (_, { slots }) =>
h('header', { class: 'flex-between shrink-0 py-4' }, slots.default?.())

function flattenSlotNodes(children: unknown): VNode[] {
const childNodes = Array.isArray(children) ? children : [children]

return childNodes.flatMap((child) => {
if (!isVNode(child)) return []
if (child.type === Fragment) return flattenSlotNodes(child.children)
return [child]
})
}

const LayoutContent: FunctionalComponent = () => {
const contentNodes = flattenSlotNodes(
slots.default?.({ Header: LayoutHeader, title: title.value }),
)
const hasCustomHeader = contentNodes.some((node) => node.type === LayoutHeader)

return [
hasCustomHeader || !title.value
? null
: h('header', { class: 'flex-between shrink-0 py-4' }, [h('h4', title.value)]),
...contentNodes,
]
}
</script>

<template>
Expand All @@ -43,11 +65,9 @@ const fallbackTitle = computed(() =>
</template>
</LayoutAside>

<LayoutMain :fallback-title="fallbackTitle" :title="title">
<template #default="{ Header, title: mainTitle }">
<slot :Header="Header" :title="mainTitle" />
</template>
</LayoutMain>
<main class="mb-6 flex min-w-0 flex-1 flex-col px-6">
<LayoutContent />
</main>
</div>
</div>
</template>
95 changes: 0 additions & 95 deletions ui/src/components/global/mk-view-layout/layout-main.ts

This file was deleted.

3 changes: 3 additions & 0 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
- 页面脚本中的状态、计算属性和处理方法按照同一业务流程集中放置,并使用简短的业务备注划分
列表查询、批量操作等流程。流程较长的函数应在确认、请求、刷新等关键阶段添加说明,重点解释
Promise 返回、执行顺序和业务约束,不为含义明确的单行代码逐句添加注释。
- 页面模板的事件绑定不直接调用 Dialog、Drawer 等组件实例暴露的 `open()` 方法。应在对应业务
流程的方法区域定义语义明确的 `handleOpenXxx` 处理函数,由处理函数调用组件实例的 `open()`
模板只绑定该处理函数;创建和编辑共用同一浮层时,可通过处理函数的可选参数区分操作场景。

参考结构:

Expand Down
14 changes: 11 additions & 3 deletions ui/src/views/system/chat/user-groups/GroupsListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import CreateOrUpdateGroupDialog from './dialog/CreateOrUpdateGroupDialog.vue'
const groupDialogRef =
useTemplateRef<InstanceType<typeof CreateOrUpdateGroupDialog>>('groupDialogRef')

function handleOpenGroupDialog(group?: ListItem) {
groupDialogRef.value?.open(group)
}

/* 用户组列表与选择 */
const groupsLoading = ref(false)
const chatUserGroups = ref<ListItem[]>([])
Expand Down Expand Up @@ -101,6 +105,10 @@ function handleMemberSearch(query?: RequestParams) {
const memberDialogRef =
useTemplateRef<InstanceType<typeof CreateGroupMemberDialog>>('memberDialogRef')

function handleOpenMemberDialog() {
memberDialogRef.value?.open()
}

/* (批量)移除成员 */
const batchSelectedMembers = ref<ChatUserGroupMember[]>([])
function handleBatchSelectionChange(selection: unknown[]) {
Expand Down Expand Up @@ -140,7 +148,7 @@ onMounted(() => loadChatUserGroups())
<component :is="Header">
<h4>{{ title }}</h4>
<el-tooltip content="创建用户组" placement="top">
<el-button text type="primary" @click="groupDialogRef?.open()">
<el-button text type="primary" @click="handleOpenGroupDialog()">
<MkIcon name="icon_add_outlined" :size="18" />
</el-button>
</el-tooltip>
Expand All @@ -152,7 +160,7 @@ onMounted(() => loadChatUserGroups())
>
<template #action-dropdown="{ row }">
<MkDropdownMenu>
<MkDropdownItem @click="groupDialogRef?.open(row)">
<MkDropdownItem @click="handleOpenGroupDialog(row)">
<template #icon><MkIcon name="icon_edit_outlined" /></template>
<span>重命名</span>
</MkDropdownItem>
Expand All @@ -178,7 +186,7 @@ onMounted(() => loadChatUserGroups())
</component>

<div class="flex-between mb-4">
<el-button type="primary" @click="memberDialogRef?.open()">
<el-button type="primary" @click="handleOpenMemberDialog">
<MkIcon name="icon_add_outlined" />
<span>添加成员</span>
</el-button>
Expand Down
20 changes: 16 additions & 4 deletions ui/src/views/system/chat/users/UserListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ import BatchSetUserGroupDialog from './dialog/BatchSetUserGroupDialog.vue'
/* 添加编辑用户表单drawer */
const userFormDrawerRef = ref<InstanceType<typeof UserFromDrawer>>()

function handleOpenUserFormDrawer(chatUser?: ChatUser) {
userFormDrawerRef.value?.open(chatUser)
}

/* 导入用户 */
const importUsersDialogRef = ref<InstanceType<typeof ImportUsersDialog>>()

function handleOpenImportUsersDialog() {
importUsersDialogRef.value?.open()
}

/* 列表查询相关 */
const userTableRef = useTemplateRef('userTableRef')
const chatUsersLoading = ref(false)
Expand Down Expand Up @@ -72,6 +80,10 @@ function loadChatUsers(resetQuery = false) {
/* 密码修改dialog */
const userPwdDialogRef = ref<InstanceType<typeof UserPwdDialog>>()

function handleOpenUserPwdDialog(chatUser: ChatUser) {
userPwdDialogRef.value?.open(chatUser)
}

/* 修改用户状态 */
function handleChangeStatus(user: ChatUser) {
const nextActive = !user.is_active
Expand Down Expand Up @@ -144,11 +156,11 @@ onMounted(() => loadChatUsers())
<h4>{{ title }}</h4>
<div class="flex items-center">
<MkComplexSearch :fields="searchFields" @change="handleSearchChange" />
<el-button class="ml-3" @click="importUsersDialogRef?.open()">
<el-button class="ml-3" @click="handleOpenImportUsersDialog">
<MkIcon name="icon_import_outlined" />
<span>导入用户</span>
</el-button>
<el-button type="primary" @click="userFormDrawerRef?.open()">
<el-button type="primary" @click="handleOpenUserFormDrawer()">
<MkIcon name="icon_add_outlined" />
<span>创建用户</span>
</el-button>
Expand Down Expand Up @@ -217,12 +229,12 @@ onMounted(() => loadChatUsers())
<el-divider direction="vertical" />
<div class="flex gap-1">
<el-tooltip content="编辑" placement="top">
<el-button type="primary" text @click.stop="userFormDrawerRef?.open(row)">
<el-button type="primary" text @click.stop="handleOpenUserFormDrawer(row)">
<mk-icon name="icon_edit_outlined"></mk-icon>
</el-button>
</el-tooltip>
<el-tooltip content="修改用户密码" placement="top">
<el-button type="primary" text @click.stop="userPwdDialogRef?.open(row)">
<el-button type="primary" text @click.stop="handleOpenUserPwdDialog(row)">
<mk-icon name="icon-key_outlined"></mk-icon>
</el-button>
</el-tooltip>
Expand Down
Loading
Loading