first commit
This commit is contained in:
46
src/App.ku.vue
Normal file
46
src/App.ku.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useThemeStore } from '@/store'
|
||||
import FgTabbar from '@/tabbar/index.vue'
|
||||
import { isPageTabbar } from './tabbar/store'
|
||||
import { currRoute } from './utils'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const isCurrentPageTabbar = ref(true)
|
||||
onShow(() => {
|
||||
console.log('App.ku.vue onShow', currRoute())
|
||||
const { path } = currRoute()
|
||||
// “蜡笔小开心”提到本地是 '/pages/index/index',线上是 '/' 导致线上 tabbar 不见了
|
||||
// 所以这里需要判断一下,如果是 '/' 就当做首页,也要显示 tabbar
|
||||
if (path === '/') {
|
||||
isCurrentPageTabbar.value = true
|
||||
}
|
||||
else {
|
||||
isCurrentPageTabbar.value = isPageTabbar(path)
|
||||
}
|
||||
})
|
||||
|
||||
const helloKuRoot = ref('Hello AppKuVue')
|
||||
|
||||
const exposeRef = ref('this is form app.Ku.vue')
|
||||
|
||||
defineExpose({
|
||||
exposeRef,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-config-provider :theme-vars="themeStore.themeVars" :theme="themeStore.theme">
|
||||
<!-- 这个先隐藏了,知道这样用就行 -->
|
||||
<view class="hidden text-center">
|
||||
{{ helloKuRoot }},这里可以配置全局的东西
|
||||
</view>
|
||||
|
||||
<KuRootView />
|
||||
|
||||
<FgTabbar v-if="isCurrentPageTabbar" />
|
||||
<wd-toast />
|
||||
<wd-message-box />
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
26
src/App.vue
Normal file
26
src/App.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||
import { navigateToInterceptor } from '@/router/interceptor'
|
||||
|
||||
onLaunch((options) => {
|
||||
console.log('App.vue onLaunch', options)
|
||||
})
|
||||
onShow((options) => {
|
||||
console.log('App.vue onShow', options)
|
||||
// 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
|
||||
// https://github.com/unibest-tech/unibest/issues/192
|
||||
if (options?.path) {
|
||||
navigateToInterceptor.invoke({ url: `/${options.path}`, query: options.query })
|
||||
}
|
||||
else {
|
||||
navigateToInterceptor.invoke({ url: '/' })
|
||||
}
|
||||
})
|
||||
onHide(() => {
|
||||
console.log('App Hide')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
51
src/api/bpm/category/index.ts
Normal file
51
src/api/bpm/category/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
const baseUrl = '/bpm/category'
|
||||
|
||||
/** 流程分类 */
|
||||
export interface Category {
|
||||
id?: number
|
||||
name: string // 分类名
|
||||
code: string // 分类标志
|
||||
status: number // 分类状态
|
||||
description?: string // 分类描述
|
||||
sort: number // 分类排序
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取流程分类分页列表 */
|
||||
export function getCategoryPage(params: PageParam) {
|
||||
return http.get<PageResult<Category>>(`${baseUrl}/page`, params)
|
||||
}
|
||||
|
||||
/** 获取流程分类详情 */
|
||||
export function getCategory(id: number) {
|
||||
return http.get<Category>(`${baseUrl}/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建流程分类 */
|
||||
export function createCategory(data: Category) {
|
||||
return http.post<number>(`${baseUrl}/create`, data)
|
||||
}
|
||||
|
||||
/** 更新流程分类 */
|
||||
export function updateCategory(data: Category) {
|
||||
return http.put<boolean>(`${baseUrl}/update`, data)
|
||||
}
|
||||
|
||||
/** 删除流程分类 */
|
||||
export function deleteCategory(id: number) {
|
||||
return http.delete<boolean>(`${baseUrl}/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 获取流程分类简单列表 */
|
||||
export function getCategorySimpleList() {
|
||||
return http.get<Category[]>(`${baseUrl}/simple-list`)
|
||||
}
|
||||
|
||||
/** 批量修改流程分类的排序 */
|
||||
export function updateCategorySortBatch(ids: number[]) {
|
||||
const params = ids.join(',')
|
||||
return http.put<boolean>(`${baseUrl}/update-sort-batch?ids=${params}`)
|
||||
}
|
||||
26
src/api/bpm/definition/index.ts
Normal file
26
src/api/bpm/definition/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 流程定义 */
|
||||
export interface ProcessDefinition {
|
||||
id: string
|
||||
key: string
|
||||
name: string
|
||||
description?: string
|
||||
icon?: string
|
||||
category: string
|
||||
formType?: number
|
||||
formId?: number
|
||||
formCustomCreatePath?: string
|
||||
formCustomViewPath?: string
|
||||
suspensionState: number
|
||||
}
|
||||
|
||||
/** 获取流程定义列表 */
|
||||
export function getProcessDefinitionList(params?: { suspensionState?: number }) {
|
||||
return http.get<ProcessDefinition[]>('/bpm/process-definition/list', params)
|
||||
}
|
||||
|
||||
/** 获取流程定义详情 */
|
||||
export function getProcessDefinition(id?: string, key?: string) {
|
||||
return http.get<ProcessDefinition>('/bpm/process-definition/get', { id, key })
|
||||
}
|
||||
30
src/api/bpm/oa/leave/index.ts
Normal file
30
src/api/bpm/oa/leave/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 请假申请 */
|
||||
export interface Leave {
|
||||
id: number
|
||||
status: number
|
||||
type: number
|
||||
reason: string
|
||||
processInstanceId: string
|
||||
startTime: Date | any
|
||||
endTime: Date | any
|
||||
createTime: Date
|
||||
startUserSelectAssignees?: Record<string, string[]>
|
||||
}
|
||||
|
||||
/** 创建请假申请 */
|
||||
export function createLeave(data: Partial<Leave>) {
|
||||
return http.post<number>('/bpm/oa/leave/create', data)
|
||||
}
|
||||
|
||||
/** 获得请假申请 */
|
||||
export function getLeave(id: number) {
|
||||
return http.get<Leave>(`/bpm/oa/leave/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 获得请假申请分页 */
|
||||
export function getLeavePage(params: PageParam) {
|
||||
return http.get<PageResult<Leave>>('/bpm/oa/leave/page', params)
|
||||
}
|
||||
38
src/api/bpm/process-expression/index.ts
Normal file
38
src/api/bpm/process-expression/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
const baseUrl = '/bpm/process-expression'
|
||||
|
||||
/** 流程表达式 */
|
||||
export interface ProcessExpression {
|
||||
id?: number
|
||||
name: string // 表达式名字
|
||||
status: number // 表达式状态
|
||||
expression: string // 表达式
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取流程表达式分页列表 */
|
||||
export function getProcessExpressionPage(params: PageParam) {
|
||||
return http.get<PageResult<ProcessExpression>>(`${baseUrl}/page`, params)
|
||||
}
|
||||
|
||||
/** 获取流程表达式详情 */
|
||||
export function getProcessExpression(id: number) {
|
||||
return http.get<ProcessExpression>(`${baseUrl}/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建流程表达式 */
|
||||
export function createProcessExpression(data: ProcessExpression) {
|
||||
return http.post<number>(`${baseUrl}/create`, data)
|
||||
}
|
||||
|
||||
/** 更新流程表达式 */
|
||||
export function updateProcessExpression(data: ProcessExpression) {
|
||||
return http.put<boolean>(`${baseUrl}/update`, data)
|
||||
}
|
||||
|
||||
/** 删除流程表达式 */
|
||||
export function deleteProcessExpression(id: number) {
|
||||
return http.delete<boolean>(`${baseUrl}/delete?id=${id}`)
|
||||
}
|
||||
41
src/api/bpm/process-listener/index.ts
Normal file
41
src/api/bpm/process-listener/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
const baseUrl = '/bpm/process-listener'
|
||||
|
||||
/** 流程监听器 */
|
||||
export interface ProcessListener {
|
||||
id?: number
|
||||
name: string // 监听器名字
|
||||
type: string // 监听器类型
|
||||
status: number // 监听器状态
|
||||
event: string // 监听事件
|
||||
valueType: string // 监听器值类型
|
||||
value: string // 监听器值
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取流程监听器分页列表 */
|
||||
export function getProcessListenerPage(params: PageParam) {
|
||||
return http.get<PageResult<ProcessListener>>(`${baseUrl}/page`, params)
|
||||
}
|
||||
|
||||
/** 获取流程监听器详情 */
|
||||
export function getProcessListener(id: number) {
|
||||
return http.get<ProcessListener>(`${baseUrl}/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建流程监听器 */
|
||||
export function createProcessListener(data: ProcessListener) {
|
||||
return http.post<number>(`${baseUrl}/create`, data)
|
||||
}
|
||||
|
||||
/** 更新流程监听器 */
|
||||
export function updateProcessListener(data: ProcessListener) {
|
||||
return http.put<boolean>(`${baseUrl}/update`, data)
|
||||
}
|
||||
|
||||
/** 删除流程监听器 */
|
||||
export function deleteProcessListener(id: number) {
|
||||
return http.delete<boolean>(`${baseUrl}/delete?id=${id}`)
|
||||
}
|
||||
141
src/api/bpm/processInstance/index.ts
Normal file
141
src/api/bpm/processInstance/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import type {
|
||||
BpmCandidateStrategyEnum,
|
||||
BpmNodeTypeEnum,
|
||||
} from '@/utils/constants'
|
||||
import { http } from '@/http/http'
|
||||
/** 流程实例用户信息 */
|
||||
export interface User {
|
||||
id: number
|
||||
nickname: string
|
||||
avatar?: string
|
||||
deptName?: string
|
||||
}
|
||||
|
||||
/** 流程定义 */
|
||||
export interface ProcessDefinition {
|
||||
id: string
|
||||
key: string
|
||||
name: string
|
||||
description?: string
|
||||
icon?: string
|
||||
category: string
|
||||
formType?: number
|
||||
formId?: number
|
||||
formCustomCreatePath?: string
|
||||
formCustomViewPath?: string
|
||||
suspensionState: number
|
||||
}
|
||||
|
||||
/** 流程实例 */
|
||||
export interface ProcessInstance {
|
||||
id: string
|
||||
name: string
|
||||
status: number
|
||||
category?: string
|
||||
categoryName?: string
|
||||
createTime?: number
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
startUser?: User
|
||||
businessKey?: string
|
||||
processDefinition?: ProcessDefinition
|
||||
summary?: {
|
||||
key: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/** 审批详情 */
|
||||
export interface ApprovalDetail {
|
||||
processInstance: ProcessInstance
|
||||
processDefinition: ProcessDefinition
|
||||
activityNodes: ApprovalNodeInfo[]
|
||||
todoTask: Task
|
||||
}
|
||||
|
||||
/** 审批详情的节点信息 */
|
||||
export interface ApprovalNodeInfo {
|
||||
candidateStrategy?: BpmCandidateStrategyEnum
|
||||
candidateUsers?: User[]
|
||||
endTime?: Date
|
||||
id: string
|
||||
name: string
|
||||
nodeType: BpmNodeTypeEnum
|
||||
startTime?: Date
|
||||
status: number
|
||||
processInstanceId?: string
|
||||
tasks: ApprovalTaskInfo[]
|
||||
}
|
||||
|
||||
/** 审批详情的节点的任务 */
|
||||
export interface ApprovalTaskInfo {
|
||||
id: number
|
||||
assigneeUser: User
|
||||
ownerUser: User
|
||||
reason: string
|
||||
signPicUrl: string
|
||||
status: number
|
||||
}
|
||||
|
||||
/** 抄送流程实例 */
|
||||
export interface ProcessInstanceCopy {
|
||||
id: string
|
||||
processInstanceId: string
|
||||
processInstanceName: string
|
||||
startUser: User
|
||||
createTime: number
|
||||
summary?: {
|
||||
key: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
|
||||
/** 查询我发起的流程分页列表 */
|
||||
export function getProcessInstanceMyPage(params: PageParam) {
|
||||
return http.get<PageResult<ProcessInstance>>('/bpm/process-instance/my-page', params)
|
||||
}
|
||||
|
||||
/** 查询抄送我的流程分页列表 */
|
||||
export function getProcessInstanceCopyPage(params: PageParam) {
|
||||
return http.get<PageResult<ProcessInstanceCopy>>('/bpm/process-instance/copy/page', params)
|
||||
}
|
||||
|
||||
/** 查询流程实例详情 */
|
||||
export function getProcessInstance(id: string) {
|
||||
return http.get<ProcessInstance>(`/bpm/process-instance/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 获取审批详情 */
|
||||
export function getApprovalDetail(params: { processDefinitionId?: string, processInstanceId?: string, activityId?: string, taskId?: string, processVariablesStr?: string }) {
|
||||
return http.get<ApprovalDetail>('/bpm/process-instance/get-approval-detail', params)
|
||||
}
|
||||
|
||||
/** 新增流程实例 */
|
||||
export function createProcessInstance(data: {
|
||||
processDefinitionId: string
|
||||
variables: Record<string, any>
|
||||
}) {
|
||||
return http.post<string>('/bpm/process-instance/create', data)
|
||||
}
|
||||
|
||||
/** 申请人取消流程实例 */
|
||||
export function cancelProcessInstanceByStartUser(id: string, reason: string) {
|
||||
return http.delete<boolean>('/bpm/process-instance/cancel-by-start-user', { id, reason })
|
||||
}
|
||||
|
||||
/** 查询管理员流程实例分页 */
|
||||
export function getProcessInstanceManagerPage(params: PageParam) {
|
||||
return http.get<PageResult<ProcessInstance>>('/bpm/process-instance/manager-page', params)
|
||||
}
|
||||
|
||||
/** 管理员取消流程实例 */
|
||||
export function cancelProcessInstanceByAdmin(id: string, reason: string) {
|
||||
return http.delete<boolean>('/bpm/process-instance/cancel-by-admin', { id, reason })
|
||||
}
|
||||
|
||||
/** 获取下一个节点审批人 */
|
||||
export function getNextApproveNodes(params) {
|
||||
return http.get<ApprovalNodeInfo[]>('/bpm/process-instance/get-next-approval-nodes', params)
|
||||
}
|
||||
102
src/api/bpm/task/index.ts
Normal file
102
src/api/bpm/task/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { ProcessInstance } from '@/api/bpm/processInstance'
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 任务处理人 */
|
||||
// TODO @芋艿:貌似暂时不需要这个?!
|
||||
export interface TaskUser {
|
||||
id: number
|
||||
nickname: string
|
||||
avatar?: string
|
||||
deptName?: string
|
||||
}
|
||||
|
||||
/** 操作按钮设置 */
|
||||
export interface OperationButtonSetting {
|
||||
displayName: string // 按钮名称
|
||||
enable: boolean // 是否启用
|
||||
}
|
||||
|
||||
/** 流程任务 */
|
||||
export interface Task {
|
||||
id: string
|
||||
name: string
|
||||
status: number
|
||||
createTime: Date
|
||||
endTime?: Date
|
||||
durationInMillis?: number // 持续时间
|
||||
reason?: string
|
||||
assigneeUser?: TaskUser
|
||||
ownerUser?: TaskUser
|
||||
processInstanceId?: string // 流程实例 ID
|
||||
processInstance: ProcessInstance
|
||||
reasonRequire?: boolean // 是否填写审批意见
|
||||
signEnable?: boolean // 是否需要签名
|
||||
buttonsSetting?: Record<number, OperationButtonSetting> // 按钮设置
|
||||
children?: Task[] // 由加签生成,包含多层子任务
|
||||
}
|
||||
|
||||
/** 查询待办任务分页列表 */
|
||||
export function getTaskTodoPage(params: PageParam) {
|
||||
return http.get<PageResult<Task>>('/bpm/task/todo-page', params)
|
||||
}
|
||||
|
||||
/** 查询已办任务分页列表 */
|
||||
export function getTaskDonePage(params: PageParam) {
|
||||
return http.get<PageResult<Task>>('/bpm/task/done-page', params)
|
||||
}
|
||||
|
||||
/** 审批通过 */
|
||||
export function approveTask(data: {
|
||||
id: string
|
||||
reason: string
|
||||
signPicUrl?: string // 签名图片 URL
|
||||
nextAssignees?: Record<string, number[]> // 下一个节点审批人
|
||||
}) {
|
||||
return http.put<boolean>('/bpm/task/approve', data)
|
||||
}
|
||||
|
||||
/** 审批拒绝 */
|
||||
export function rejectTask(data: { id: string, reason: string }) {
|
||||
return http.put<boolean>('/bpm/task/reject', data)
|
||||
}
|
||||
|
||||
/** 根据流程实例 ID 查询任务列表 */
|
||||
export function getTaskListByProcessInstanceId(processInstanceId: string) {
|
||||
return http.get<Task[]>(`/bpm/task/list-by-process-instance-id?processInstanceId=${processInstanceId}`)
|
||||
}
|
||||
|
||||
/** 查询任务管理分页 */
|
||||
export function getTaskManagerPage(params: PageParam) {
|
||||
return http.get<PageResult<Task>>('/bpm/task/manager-page', params)
|
||||
}
|
||||
|
||||
/** 委派任务 */
|
||||
export function delegateTask(data: { id: string, delegateUserId: string, reason: string }) {
|
||||
return http.put<boolean>('/bpm/task/delegate', data)
|
||||
}
|
||||
|
||||
/** 转办任务 */
|
||||
export function transferTask(data: { id: string, assigneeUserId: string, reason: string }) {
|
||||
return http.put<boolean>('/bpm/task/transfer', data)
|
||||
}
|
||||
|
||||
/** 退回任务 */
|
||||
export function returnTask(data: { id: string, targetTaskDefinitionKey: string, reason: string }) {
|
||||
return http.put<boolean>('/bpm/task/return', data)
|
||||
}
|
||||
|
||||
/** 获取可退回的节点列表 */
|
||||
export function getTaskListByReturn(taskId: string) {
|
||||
return http.get<any[]>(`/bpm/task/list-by-return?id=${taskId}`)
|
||||
}
|
||||
|
||||
/** 加签任务 */
|
||||
export function signCreateTask(data: { id: string, type: string, userIds: number[], reason: string }) {
|
||||
return http.put<boolean>('/bpm/task/create-sign', data)
|
||||
}
|
||||
|
||||
/** 减签任务 */
|
||||
export function signDeleteTask(data: { id: string, reason: string }) {
|
||||
return http.delete<boolean>('/bpm/task/delete-sign', data)
|
||||
}
|
||||
45
src/api/bpm/user-group/index.ts
Normal file
45
src/api/bpm/user-group/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
const baseUrl = '/bpm/user-group'
|
||||
|
||||
/** 用户组 */
|
||||
export interface UserGroup {
|
||||
id?: number
|
||||
name: string // 组名
|
||||
description: string // 描述
|
||||
userIds: number[] // 成员用户编号数组
|
||||
status: number // 状态
|
||||
remark: string // 备注
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取用户组分页列表 */
|
||||
export function getUserGroupPage(params: PageParam) {
|
||||
return http.get<PageResult<UserGroup>>(`${baseUrl}/page`, params)
|
||||
}
|
||||
|
||||
/** 获取用户组详情 */
|
||||
export function getUserGroup(id: number) {
|
||||
return http.get<UserGroup>(`${baseUrl}/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建用户组 */
|
||||
export function createUserGroup(data: UserGroup) {
|
||||
return http.post<number>(`${baseUrl}/create`, data)
|
||||
}
|
||||
|
||||
/** 更新用户组 */
|
||||
export function updateUserGroup(data: UserGroup) {
|
||||
return http.put<boolean>(`${baseUrl}/update`, data)
|
||||
}
|
||||
|
||||
/** 删除用户组 */
|
||||
export function deleteUserGroup(id: number) {
|
||||
return http.delete<boolean>(`${baseUrl}/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 获取用户组简单列表 */
|
||||
export function getUserGroupSimpleList() {
|
||||
return http.get<UserGroup[]>(`${baseUrl}/simple-list`)
|
||||
}
|
||||
36
src/api/infra/api-access-log/index.ts
Normal file
36
src/api/infra/api-access-log/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** API 访问日志信息 */
|
||||
export interface ApiAccessLog {
|
||||
id: number
|
||||
traceId: string
|
||||
userId: number
|
||||
userType: number
|
||||
applicationName: string
|
||||
requestMethod: string
|
||||
requestParams: string
|
||||
responseBody: string
|
||||
requestUrl: string
|
||||
userIp: string
|
||||
userAgent: string
|
||||
operateModule: string
|
||||
operateName: string
|
||||
operateType: number
|
||||
beginTime: Date
|
||||
endTime: Date
|
||||
duration: number
|
||||
resultCode: number
|
||||
resultMsg: string
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
/** 获取 API 访问日志分页列表 */
|
||||
export function getApiAccessLogPage(params: PageParam) {
|
||||
return http.get<PageResult<ApiAccessLog>>('/infra/api-access-log/page', params)
|
||||
}
|
||||
|
||||
/** 获取 API 访问日志详情 */
|
||||
export function getApiAccessLog(id: number) {
|
||||
return http.get<ApiAccessLog>(`/infra/api-access-log/get?id=${id}`)
|
||||
}
|
||||
45
src/api/infra/api-error-log/index.ts
Normal file
45
src/api/infra/api-error-log/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** API 错误日志信息 */
|
||||
export interface ApiErrorLog {
|
||||
id: number
|
||||
traceId: string
|
||||
userId: number
|
||||
userType: number
|
||||
applicationName: string
|
||||
requestMethod: string
|
||||
requestParams: string
|
||||
requestUrl: string
|
||||
userIp: string
|
||||
userAgent: string
|
||||
exceptionTime: Date
|
||||
exceptionName: string
|
||||
exceptionMessage: string
|
||||
exceptionRootCauseMessage: string
|
||||
exceptionStackTrace: string
|
||||
exceptionClassName: string
|
||||
exceptionFileName: string
|
||||
exceptionMethodName: string
|
||||
exceptionLineNumber: number
|
||||
processUserId: number
|
||||
processStatus: number
|
||||
processTime: Date
|
||||
resultCode: number
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
/** 获取 API 错误日志分页列表 */
|
||||
export function getApiErrorLogPage(params: PageParam) {
|
||||
return http.get<PageResult<ApiErrorLog>>('/infra/api-error-log/page', params)
|
||||
}
|
||||
|
||||
/** 获取 API 错误日志详情 */
|
||||
export function getApiErrorLog(id: number) {
|
||||
return http.get<ApiErrorLog>(`/infra/api-error-log/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 更新 API 错误日志的处理状态 */
|
||||
export function updateApiErrorLogStatus(id: number, processStatus: number) {
|
||||
return http.put<boolean>(`/infra/api-error-log/update-status?id=${id}&processStatus=${processStatus}`)
|
||||
}
|
||||
45
src/api/infra/config/index.ts
Normal file
45
src/api/infra/config/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 参数配置信息 */
|
||||
export interface Config {
|
||||
id?: number
|
||||
category: string
|
||||
name: string
|
||||
key: string
|
||||
value: string
|
||||
type: number
|
||||
visible: boolean
|
||||
remark: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取参数配置分页列表 */
|
||||
export function getConfigPage(params: PageParam) {
|
||||
return http.get<PageResult<Config>>('/infra/config/page', params)
|
||||
}
|
||||
|
||||
/** 获取参数配置详情 */
|
||||
export function getConfig(id: number) {
|
||||
return http.get<Config>(`/infra/config/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 根据参数键名查询参数值 */
|
||||
export function getConfigKey(configKey: string) {
|
||||
return http.get<string>(`/infra/config/get-value-by-key?key=${configKey}`)
|
||||
}
|
||||
|
||||
/** 创建参数配置 */
|
||||
export function createConfig(data: Config) {
|
||||
return http.post<number>('/infra/config/create', data)
|
||||
}
|
||||
|
||||
/** 更新参数配置 */
|
||||
export function updateConfig(data: Config) {
|
||||
return http.put<boolean>('/infra/config/update', data)
|
||||
}
|
||||
|
||||
/** 删除参数配置 */
|
||||
export function deleteConfig(id: number) {
|
||||
return http.delete<boolean>(`/infra/config/delete?id=${id}`)
|
||||
}
|
||||
36
src/api/infra/data-source-config/index.ts
Normal file
36
src/api/infra/data-source-config/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 数据源配置信息 */
|
||||
export interface DataSourceConfig {
|
||||
id?: number
|
||||
name: string
|
||||
url: string
|
||||
username: string
|
||||
password: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取数据源配置列表(无分页) */
|
||||
export function getDataSourceConfigList() {
|
||||
return http.get<DataSourceConfig[]>('/infra/data-source-config/list')
|
||||
}
|
||||
|
||||
/** 获取数据源配置详情 */
|
||||
export function getDataSourceConfig(id: number) {
|
||||
return http.get<DataSourceConfig>(`/infra/data-source-config/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建数据源配置 */
|
||||
export function createDataSourceConfig(data: DataSourceConfig) {
|
||||
return http.post<number>('/infra/data-source-config/create', data)
|
||||
}
|
||||
|
||||
/** 更新数据源配置 */
|
||||
export function updateDataSourceConfig(data: DataSourceConfig) {
|
||||
return http.put<boolean>('/infra/data-source-config/update', data)
|
||||
}
|
||||
|
||||
/** 删除数据源配置 */
|
||||
export function deleteDataSourceConfig(id: number) {
|
||||
return http.delete<boolean>(`/infra/data-source-config/delete?id=${id}`)
|
||||
}
|
||||
67
src/api/infra/file/config/index.ts
Normal file
67
src/api/infra/file/config/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 文件客户端配置 */
|
||||
export interface FileClientConfig {
|
||||
basePath?: string
|
||||
host?: string
|
||||
port?: number
|
||||
username?: string
|
||||
password?: string
|
||||
mode?: string
|
||||
endpoint?: string
|
||||
bucket?: string
|
||||
accessKey?: string
|
||||
accessSecret?: string
|
||||
enablePathStyleAccess?: boolean
|
||||
enablePublicAccess?: boolean
|
||||
region?: string
|
||||
domain?: string
|
||||
}
|
||||
|
||||
/** 文件配置信息 */
|
||||
export interface FileConfig {
|
||||
id?: number
|
||||
name: string
|
||||
storage?: number
|
||||
master?: boolean
|
||||
visible?: boolean
|
||||
config?: FileClientConfig
|
||||
remark?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 查询文件配置分页列表 */
|
||||
export function getFileConfigPage(params: PageParam) {
|
||||
return http.get<PageResult<FileConfig>>('/infra/file-config/page', params)
|
||||
}
|
||||
|
||||
/** 查询文件配置详情 */
|
||||
export function getFileConfig(id: number) {
|
||||
return http.get<FileConfig>(`/infra/file-config/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 新增文件配置 */
|
||||
export function createFileConfig(data: FileConfig) {
|
||||
return http.post<number>('/infra/file-config/create', data)
|
||||
}
|
||||
|
||||
/** 修改文件配置 */
|
||||
export function updateFileConfig(data: FileConfig) {
|
||||
return http.put<boolean>('/infra/file-config/update', data)
|
||||
}
|
||||
|
||||
/** 删除文件配置 */
|
||||
export function deleteFileConfig(id: number) {
|
||||
return http.delete<boolean>(`/infra/file-config/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 更新文件配置为主配置 */
|
||||
export function updateFileConfigMaster(id: number) {
|
||||
return http.put<boolean>(`/infra/file-config/update-master?id=${id}`)
|
||||
}
|
||||
|
||||
/** 测试文件配置 */
|
||||
export function testFileConfig(id: number) {
|
||||
return http.get<string>(`/infra/file-config/test?id=${id}`)
|
||||
}
|
||||
97
src/api/infra/file/index.ts
Normal file
97
src/api/infra/file/index.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { http } from '@/http/http'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
/** 文件信息 */
|
||||
export interface FileVO {
|
||||
id?: number
|
||||
configId?: number
|
||||
path: string
|
||||
name?: string
|
||||
url?: string
|
||||
size?: number
|
||||
type?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 文件预签名信息 */
|
||||
export interface FilePresignedUrlRespVO {
|
||||
configId: number // 配置编号
|
||||
uploadUrl: string // 文件上传 URL
|
||||
url: string // 文件访问 URL
|
||||
path: string // 文件路径
|
||||
}
|
||||
|
||||
/** 创建文件请求 */
|
||||
export interface FileCreateReqVO {
|
||||
configId: number
|
||||
url: string
|
||||
path: string
|
||||
name: string
|
||||
type?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
/** 获取文件预签名地址 */
|
||||
export function getFilePresignedUrl(name: string, directory?: string) {
|
||||
return http.get<FilePresignedUrlRespVO>('/infra/file/presigned-url', { name, directory })
|
||||
}
|
||||
|
||||
/** 创建文件记录 */
|
||||
export function createFile(data: FileCreateReqVO) {
|
||||
return http.post<string>('/infra/file/create', data)
|
||||
}
|
||||
|
||||
/** 获取文件详情 */
|
||||
export function getFile(id: number) {
|
||||
return http.get<FileVO>(`/infra/file/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 删除文件 */
|
||||
export function deleteFile(id: number) {
|
||||
return http.delete(`/infra/file/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到后端
|
||||
*
|
||||
* @param filePath 本地文件路径
|
||||
* @param directory 目录(可选)
|
||||
* @returns 文件访问 URL
|
||||
*/
|
||||
export function uploadFile(filePath: string, directory?: string): Promise<string> {
|
||||
const tokenStore = useTokenStore()
|
||||
const userStore = useUserStore()
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${import.meta.env.VITE_SERVER_BASEURL}/infra/file/upload`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
'Accept': '*/*',
|
||||
'tenant-id': userStore.tenantId,
|
||||
'Authorization': `Bearer ${tokenStore.validToken}`,
|
||||
},
|
||||
formData: directory ? { directory } : undefined,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
const result = JSON.parse(res.data)
|
||||
if (result.code === 0) {
|
||||
resolve(result.data)
|
||||
} else {
|
||||
const toast = useToast()
|
||||
toast.show(result.msg || '上传失败')
|
||||
reject(new Error(result.msg || '上传失败'))
|
||||
}
|
||||
} else {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('上传失败:', err)
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
60
src/api/infra/job/index.ts
Normal file
60
src/api/infra/job/index.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
// TODO @AI:不用 baseUrl 方式
|
||||
const baseUrl = '/infra/job'
|
||||
|
||||
/** 定时任务信息 */
|
||||
export interface Job {
|
||||
id?: number
|
||||
name: string
|
||||
status: number
|
||||
handlerName: string
|
||||
handlerParam: string
|
||||
cronExpression: string
|
||||
retryCount: number
|
||||
retryInterval: number
|
||||
monitorTimeout: number
|
||||
createTime?: Date
|
||||
nextTimes?: Date[]
|
||||
}
|
||||
|
||||
/** 获取定时任务分页列表 */
|
||||
export function getJobPage(params: PageParam) {
|
||||
return http.get<PageResult<Job>>(`${baseUrl}/page`, params)
|
||||
}
|
||||
|
||||
/** 获取定时任务详情 */
|
||||
export function getJob(id: number) {
|
||||
return http.get<Job>(`${baseUrl}/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建定时任务 */
|
||||
export function createJob(data: Job) {
|
||||
return http.post<number>(`${baseUrl}/create`, data)
|
||||
}
|
||||
|
||||
/** 更新定时任务 */
|
||||
export function updateJob(data: Job) {
|
||||
return http.put<boolean>(`${baseUrl}/update`, data)
|
||||
}
|
||||
|
||||
/** 删除定时任务 */
|
||||
export function deleteJob(id: number) {
|
||||
return http.delete<boolean>(`${baseUrl}/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 更新定时任务状态 */
|
||||
export function updateJobStatus(id: number, status: number) {
|
||||
return http.put<boolean>(`${baseUrl}/update-status`, { id, status })
|
||||
}
|
||||
|
||||
/** 立即执行一次定时任务 */
|
||||
export function runJob(id: number) {
|
||||
return http.put<boolean>(`${baseUrl}/trigger?id=${id}`)
|
||||
}
|
||||
|
||||
/** 获取定时任务的下 n 次执行时间 */
|
||||
export function getJobNextTimes(id: number) {
|
||||
return http.get<Date[]>(`${baseUrl}/get_next_times?id=${id}`)
|
||||
}
|
||||
31
src/api/infra/job/log/index.ts
Normal file
31
src/api/infra/job/log/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
// TODO @AI:不用 baseUrl 方式
|
||||
const baseUrl = '/infra/job-log'
|
||||
|
||||
/** 定时任务日志信息 */
|
||||
export interface JobLog {
|
||||
id?: number
|
||||
jobId: number
|
||||
handlerName: string
|
||||
handlerParam: string
|
||||
cronExpression: string
|
||||
executeIndex: string
|
||||
beginTime: Date
|
||||
endTime: Date
|
||||
duration: string
|
||||
status: number
|
||||
createTime?: string
|
||||
result: string
|
||||
}
|
||||
|
||||
/** 获取定时任务日志分页列表 */
|
||||
export function getJobLogPage(params: PageParam) {
|
||||
return http.get<PageResult<JobLog>>(`${baseUrl}/page`, params)
|
||||
}
|
||||
|
||||
/** 获取定时任务日志详情 */
|
||||
export function getJobLog(id: number) {
|
||||
return http.get<JobLog>(`${baseUrl}/get?id=${id}`)
|
||||
}
|
||||
148
src/api/login.ts
Normal file
148
src/api/login.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type {
|
||||
AuthPermissionInfo,
|
||||
IAuthLoginRes,
|
||||
ICaptcha,
|
||||
IDoubleTokenRes,
|
||||
} from './types/login'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/**
|
||||
* 登录表单
|
||||
*/
|
||||
export interface ILoginForm {
|
||||
type: 'username' | 'register' | 'sms'
|
||||
username?: string
|
||||
password?: string
|
||||
nickname?: string
|
||||
captchaVerification?: string
|
||||
mobile?: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
/** 账号密码登录 Request VO */
|
||||
export interface AuthLoginReqVO {
|
||||
password?: string
|
||||
username?: string
|
||||
captchaVerification?: string
|
||||
// 绑定社交登录时,需要传递如下参数
|
||||
socialType?: number
|
||||
socialCode?: string
|
||||
socialState?: string
|
||||
}
|
||||
|
||||
/** 注册 Request VO */
|
||||
export interface AuthRegisterReqVO {
|
||||
username: string
|
||||
password: string
|
||||
captchaVerification: string
|
||||
}
|
||||
|
||||
/** 短信登录 Request VO */
|
||||
export interface AuthSmsLoginReqVO {
|
||||
mobile: string
|
||||
code: string
|
||||
}
|
||||
|
||||
/** 发送短信验证码 Request VO */
|
||||
export interface AuthSmsSendReqVO {
|
||||
mobile: string
|
||||
scene: number
|
||||
}
|
||||
|
||||
/** 租户信息 */
|
||||
export interface TenantVO {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
/** 重置密码 Request VO */
|
||||
export interface AuthResetPasswordReqVO {
|
||||
mobile: string
|
||||
code: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/** 获取验证码 */
|
||||
export function getCode(data: any) {
|
||||
return http.post<ICaptcha>('/system/captcha/get', data, null, null, { original: true })
|
||||
}
|
||||
|
||||
/** 校验验证码 */
|
||||
export function checkCaptcha(data: any) {
|
||||
return http.post<boolean>('/system/captcha/check', data, null, null, { original: true })
|
||||
}
|
||||
|
||||
/** 使用账号密码登录 */
|
||||
export function login(data: AuthLoginReqVO) {
|
||||
return http.post<IAuthLoginRes>('/system/auth/login', data)
|
||||
}
|
||||
|
||||
/** 注册用户 */
|
||||
export function register(data: AuthRegisterReqVO) {
|
||||
return http.post<IAuthLoginRes>('/system/auth/register', data)
|
||||
}
|
||||
|
||||
/** 短信登录 */
|
||||
export function smsLogin(data: AuthSmsLoginReqVO) {
|
||||
return http.post<IAuthLoginRes>('/system/auth/sms-login', data)
|
||||
}
|
||||
|
||||
/** 发送短信验证码 */
|
||||
export function sendSmsCode(data: AuthSmsSendReqVO) {
|
||||
return http.post<boolean>('/system/auth/send-sms-code', data)
|
||||
}
|
||||
|
||||
/** 获取租户简单列表 */
|
||||
export function getTenantSimpleList() {
|
||||
return http.get<TenantVO[]>('/system/tenant/simple-list')
|
||||
}
|
||||
|
||||
/** 根据租户域名获取租户信息 */
|
||||
export function getTenantByWebsite(website: string) {
|
||||
return http.get<TenantVO>(`/system/tenant/get-by-website?website=${website}`)
|
||||
}
|
||||
|
||||
/** 通过短信重置密码 */
|
||||
export function smsResetPassword(data: AuthResetPasswordReqVO) {
|
||||
return http.post<boolean>('/system/auth/reset-password', data)
|
||||
}
|
||||
|
||||
/** 刷新token */
|
||||
export function refreshToken(refreshToken: string) {
|
||||
return http.post<IDoubleTokenRes>(`/system/auth/refresh-token?refreshToken=${refreshToken}`)
|
||||
}
|
||||
|
||||
/** 获取权限信息 */
|
||||
export function getAuthPermissionInfo() {
|
||||
return http.get<AuthPermissionInfo>('/system/auth/get-permission-info')
|
||||
}
|
||||
|
||||
/** 退出登录 */
|
||||
export function logout() {
|
||||
return http.post<void>('/system/auth/logout')
|
||||
}
|
||||
|
||||
// TODO @芋艿:三方登录
|
||||
/**
|
||||
* 获取微信登录凭证
|
||||
* @returns Promise 包含微信登录凭证(code)
|
||||
*/
|
||||
export function getWxCode() {
|
||||
return new Promise<UniApp.LoginRes>((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: res => resolve(res),
|
||||
fail: err => reject(new Error(err)),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TODO @芋艿:三方登录
|
||||
/**
|
||||
* 微信登录
|
||||
* @param params 微信登录参数,包含code
|
||||
* @returns Promise 包含登录结果
|
||||
*/
|
||||
export function wxLogin(data: { code: string }) {
|
||||
return http.post<IAuthLoginRes>('/auth/wxLogin', data)
|
||||
}
|
||||
19
src/api/system/area/index.ts
Normal file
19
src/api/system/area/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 地区信息 */
|
||||
export interface Area {
|
||||
id: number
|
||||
name: string
|
||||
parentId?: number
|
||||
children?: Area[]
|
||||
}
|
||||
|
||||
/** 获得地区树 */
|
||||
export function getAreaTree() {
|
||||
return http.get<Area[]>('/system/area/tree')
|
||||
}
|
||||
|
||||
/** 获得 IP 对应的地区名 */
|
||||
export function getAreaByIp(ip: string) {
|
||||
return http.get<string>(`/system/area/get-by-ip?ip=${ip}`)
|
||||
}
|
||||
45
src/api/system/dept/index.ts
Normal file
45
src/api/system/dept/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 部门信息 */
|
||||
export interface Dept {
|
||||
id?: number
|
||||
name: string
|
||||
parentId: number
|
||||
status: number
|
||||
sort: number
|
||||
leaderUserId?: number
|
||||
phone?: string
|
||||
email?: string
|
||||
createTime?: Date
|
||||
children?: Dept[]
|
||||
}
|
||||
|
||||
/** 获取部门列表 */
|
||||
export function getDeptList(params?: { name?: string, status?: number }) {
|
||||
return http.get<Dept[]>('/system/dept/list', params)
|
||||
}
|
||||
|
||||
/** 获取部门精简列表 */
|
||||
export function getSimpleDeptList() {
|
||||
return http.get<Dept[]>('/system/dept/simple-list')
|
||||
}
|
||||
|
||||
/** 获取部门详情 */
|
||||
export function getDept(id: number) {
|
||||
return http.get<Dept>(`/system/dept/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建部门 */
|
||||
export function createDept(data: Dept) {
|
||||
return http.post<number>('/system/dept/create', data)
|
||||
}
|
||||
|
||||
/** 更新部门 */
|
||||
export function updateDept(data: Dept) {
|
||||
return http.put<boolean>('/system/dept/update', data)
|
||||
}
|
||||
|
||||
/** 删除部门 */
|
||||
export function deleteDept(id: number) {
|
||||
return http.delete<boolean>(`/system/dept/delete?id=${id}`)
|
||||
}
|
||||
46
src/api/system/dict/data/index.ts
Normal file
46
src/api/system/dict/data/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 字典数据 */
|
||||
export interface DictData {
|
||||
id?: number
|
||||
dictType: string
|
||||
label: string
|
||||
value: string
|
||||
colorType?: string
|
||||
cssClass?: string
|
||||
sort?: number
|
||||
status: number
|
||||
remark?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 查询字典数据(精简)列表 */
|
||||
export function getSimpleDictDataList() {
|
||||
return http.get<DictData[]>('/system/dict-data/simple-list')
|
||||
}
|
||||
|
||||
/** 查询字典数据分页列表 */
|
||||
export function getDictDataPage(params: PageParam) {
|
||||
return http.get<PageResult<DictData>>('/system/dict-data/page', params)
|
||||
}
|
||||
|
||||
/** 查询字典数据详情 */
|
||||
export function getDictData(id: number) {
|
||||
return http.get<DictData>(`/system/dict-data/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 新增字典数据 */
|
||||
export function createDictData(data: DictData) {
|
||||
return http.post<number>('/system/dict-data/create', data)
|
||||
}
|
||||
|
||||
/** 修改字典数据 */
|
||||
export function updateDictData(data: DictData) {
|
||||
return http.put<boolean>('/system/dict-data/update', data)
|
||||
}
|
||||
|
||||
/** 删除字典数据 */
|
||||
export function deleteDictData(id: number) {
|
||||
return http.delete<boolean>(`/system/dict-data/delete?id=${id}`)
|
||||
}
|
||||
42
src/api/system/dict/type/index.ts
Normal file
42
src/api/system/dict/type/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 字典类型 */
|
||||
export interface DictType {
|
||||
id?: number
|
||||
name: string
|
||||
type: string
|
||||
status: number
|
||||
remark?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 查询字典类型(精简)列表 */
|
||||
export function getSimpleDictTypeList() {
|
||||
return http.get<DictType[]>('/system/dict-type/list-all-simple')
|
||||
}
|
||||
|
||||
/** 查询字典类型分页列表 */
|
||||
export function getDictTypePage(params: PageParam) {
|
||||
return http.get<PageResult<DictType>>('/system/dict-type/page', params)
|
||||
}
|
||||
|
||||
/** 查询字典类型详情 */
|
||||
export function getDictType(id: number) {
|
||||
return http.get<DictType>(`/system/dict-type/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 新增字典类型 */
|
||||
export function createDictType(data: DictType) {
|
||||
return http.post<number>('/system/dict-type/create', data)
|
||||
}
|
||||
|
||||
/** 修改字典类型 */
|
||||
export function updateDictType(data: DictType) {
|
||||
return http.put<boolean>('/system/dict-type/update', data)
|
||||
}
|
||||
|
||||
/** 删除字典类型 */
|
||||
export function deleteDictType(id: number) {
|
||||
return http.delete<boolean>(`/system/dict-type/delete?id=${id}`)
|
||||
}
|
||||
26
src/api/system/login-log/index.ts
Normal file
26
src/api/system/login-log/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 登录日志信息 */
|
||||
export interface LoginLog {
|
||||
id?: number
|
||||
traceId?: string
|
||||
userId?: number
|
||||
userType?: number
|
||||
logType?: number
|
||||
username?: string
|
||||
userIp?: string
|
||||
userAgent?: string
|
||||
result?: number
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取登录日志分页列表 */
|
||||
export function getLoginLogPage(params: PageParam) {
|
||||
return http.get<PageResult<LoginLog>>('/system/login-log/page', params)
|
||||
}
|
||||
|
||||
/** 获取登录日志详情 */
|
||||
export function getLoginLog(id: number) {
|
||||
return http.get<LoginLog>(`/system/login-log/get?id=${id}`)
|
||||
}
|
||||
45
src/api/system/mail/account/index.ts
Normal file
45
src/api/system/mail/account/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 邮箱账号信息 */
|
||||
export interface MailAccount {
|
||||
id?: number
|
||||
mail: string
|
||||
username: string
|
||||
password?: string
|
||||
host: string
|
||||
port: number
|
||||
sslEnable: boolean
|
||||
starttlsEnable: boolean
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 获取邮箱账号分页列表 */
|
||||
export function getMailAccountPage(params: PageParam) {
|
||||
return http.get<PageResult<MailAccount>>('/system/mail-account/page', params)
|
||||
}
|
||||
|
||||
/** 获取邮箱账号(精简)列表 */
|
||||
export function getSimpleMailAccountList() {
|
||||
return http.get<MailAccount[]>('/system/mail-account/simple-list')
|
||||
}
|
||||
|
||||
/** 获取邮箱账号详情 */
|
||||
export function getMailAccount(id: number) {
|
||||
return http.get<MailAccount>(`/system/mail-account/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建邮箱账号 */
|
||||
export function createMailAccount(data: MailAccount) {
|
||||
return http.post<number>('/system/mail-account/create', data)
|
||||
}
|
||||
|
||||
/** 更新邮箱账号 */
|
||||
export function updateMailAccount(data: MailAccount) {
|
||||
return http.put<boolean>('/system/mail-account/update', data)
|
||||
}
|
||||
|
||||
/** 删除邮箱账号 */
|
||||
export function deleteMailAccount(id: number) {
|
||||
return http.delete<boolean>(`/system/mail-account/delete?id=${id}`)
|
||||
}
|
||||
34
src/api/system/mail/log/index.ts
Normal file
34
src/api/system/mail/log/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 邮件日志信息 */
|
||||
export interface MailLog {
|
||||
id?: number
|
||||
userId?: number
|
||||
userType?: number
|
||||
templateId?: number
|
||||
templateCode?: string
|
||||
templateTitle?: string
|
||||
templateContent?: string
|
||||
templateParams?: Record<string, any>
|
||||
accountId?: number
|
||||
fromMail?: string
|
||||
toMails?: string[]
|
||||
ccMails?: string[]
|
||||
bccMails?: string[]
|
||||
sendStatus?: number
|
||||
sendTime?: string
|
||||
sendMessageId?: string
|
||||
sendException?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 获取邮件日志分页列表 */
|
||||
export function getMailLogPage(params: PageParam) {
|
||||
return http.get<PageResult<MailLog>>('/system/mail-log/page', params)
|
||||
}
|
||||
|
||||
/** 获取邮件日志详情 */
|
||||
export function getMailLog(id: number) {
|
||||
return http.get<MailLog>(`/system/mail-log/get?id=${id}`)
|
||||
}
|
||||
56
src/api/system/mail/template/index.ts
Normal file
56
src/api/system/mail/template/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 邮件模板信息 */
|
||||
export interface MailTemplate {
|
||||
id?: number
|
||||
name: string
|
||||
code: string
|
||||
accountId?: number
|
||||
nickname?: string
|
||||
title: string
|
||||
content: string
|
||||
status: number
|
||||
remark?: string
|
||||
params?: string[]
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 发送邮件请求 */
|
||||
export interface MailSendReqVO {
|
||||
templateCode: string
|
||||
templateParams: Record<string, any>
|
||||
toMails: string[]
|
||||
ccMails?: string[]
|
||||
bccMails?: string[]
|
||||
}
|
||||
|
||||
/** 获取邮件模板分页列表 */
|
||||
export function getMailTemplatePage(params: PageParam) {
|
||||
return http.get<PageResult<MailTemplate>>('/system/mail-template/page', params)
|
||||
}
|
||||
|
||||
/** 获取邮件模板详情 */
|
||||
export function getMailTemplate(id: number) {
|
||||
return http.get<MailTemplate>(`/system/mail-template/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建邮件模板 */
|
||||
export function createMailTemplate(data: MailTemplate) {
|
||||
return http.post<number>('/system/mail-template/create', data)
|
||||
}
|
||||
|
||||
/** 更新邮件模板 */
|
||||
export function updateMailTemplate(data: MailTemplate) {
|
||||
return http.put<boolean>('/system/mail-template/update', data)
|
||||
}
|
||||
|
||||
/** 删除邮件模板 */
|
||||
export function deleteMailTemplate(id: number) {
|
||||
return http.delete<boolean>(`/system/mail-template/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 发送邮件 */
|
||||
export function sendMail(data: MailSendReqVO) {
|
||||
return http.post<number>('/system/mail-template/send-mail', data)
|
||||
}
|
||||
51
src/api/system/menu/index.ts
Normal file
51
src/api/system/menu/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 菜单信息 */
|
||||
export interface Menu {
|
||||
id?: number
|
||||
name: string
|
||||
permission: string
|
||||
type: number
|
||||
sort: number
|
||||
parentId: number
|
||||
path: string
|
||||
icon: string
|
||||
component: string
|
||||
componentName?: string
|
||||
status: number
|
||||
visible: boolean
|
||||
keepAlive: boolean
|
||||
alwaysShow?: boolean
|
||||
createTime?: Date
|
||||
children?: Menu[]
|
||||
}
|
||||
|
||||
/** 获取菜单列表 */
|
||||
export function getMenuList(params?: { name?: string, status?: number }) {
|
||||
return http.get<Menu[]>('/system/menu/list', params)
|
||||
}
|
||||
|
||||
/** 获取菜单精简列表 */
|
||||
export function getSimpleMenuList() {
|
||||
return http.get<Menu[]>('/system/menu/simple-list')
|
||||
}
|
||||
|
||||
/** 获取菜单详情 */
|
||||
export function getMenu(id: number) {
|
||||
return http.get<Menu>(`/system/menu/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建菜单 */
|
||||
export function createMenu(data: Menu) {
|
||||
return http.post<number>('/system/menu/create', data)
|
||||
}
|
||||
|
||||
/** 更新菜单 */
|
||||
export function updateMenu(data: Menu) {
|
||||
return http.put<boolean>('/system/menu/update', data)
|
||||
}
|
||||
|
||||
/** 删除菜单 */
|
||||
export function deleteMenu(id: number) {
|
||||
return http.delete<boolean>(`/system/menu/delete?id=${id}`)
|
||||
}
|
||||
39
src/api/system/notice/index.ts
Normal file
39
src/api/system/notice/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
const baseUrl = '/system/notice'
|
||||
|
||||
/** 通知公告信息 */
|
||||
export interface Notice {
|
||||
id?: number
|
||||
title: string
|
||||
content: string
|
||||
type: number
|
||||
status: number
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取通知公告分页列表 */
|
||||
export function getNoticePage(params: PageParam) {
|
||||
return http.get<PageResult<Notice>>(`${baseUrl}/page`, params)
|
||||
}
|
||||
|
||||
/** 获取通知公告详情 */
|
||||
export function getNotice(id: number) {
|
||||
return http.get<Notice>(`${baseUrl}/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建通知公告 */
|
||||
export function createNotice(data: Notice) {
|
||||
return http.post<number>(`${baseUrl}/create`, data)
|
||||
}
|
||||
|
||||
/** 更新通知公告 */
|
||||
export function updateNotice(data: Notice) {
|
||||
return http.put<boolean>(`${baseUrl}/update`, data)
|
||||
}
|
||||
|
||||
/** 删除通知公告 */
|
||||
export function deleteNotice(id: number) {
|
||||
return http.delete<boolean>(`${baseUrl}/delete?id=${id}`)
|
||||
}
|
||||
54
src/api/system/notify/message/index.ts
Normal file
54
src/api/system/notify/message/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 站内信消息信息 */
|
||||
export interface NotifyMessage {
|
||||
id: number
|
||||
userId: number
|
||||
userType: number
|
||||
templateId: number
|
||||
templateCode: string
|
||||
templateNickname: string
|
||||
templateContent: string
|
||||
templateType: number
|
||||
templateParams: string
|
||||
readStatus: boolean
|
||||
readTime: Date
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 查询站内信消息列表 */
|
||||
export function getNotifyMessagePage(params: PageParam) {
|
||||
return http.get<PageResult<NotifyMessage>>('/system/notify-message/page', params)
|
||||
}
|
||||
|
||||
/** 查询站内信消息详情 */
|
||||
export function getNotifyMessage(id: number) {
|
||||
return http.get<NotifyMessage>(`/system/notify-message/get`, { id })
|
||||
}
|
||||
|
||||
/** 获取我的站内信分页 */
|
||||
export function getMyNotifyMessagePage(params: PageParam) {
|
||||
return http.get<PageResult<NotifyMessage>>('/system/notify-message/my-page', params)
|
||||
}
|
||||
|
||||
/** 获取我的站内信详情 */
|
||||
export function getMyNotifyMessage(id: number) {
|
||||
return http.get<NotifyMessage>(`/system/notify-message/get`, { id })
|
||||
}
|
||||
|
||||
/** 批量标记站内信已读 */
|
||||
export function updateNotifyMessageRead(ids: number | number[]) {
|
||||
const idsArray = Array.isArray(ids) ? ids : [ids]
|
||||
return http.put<boolean>('/system/notify-message/update-read', undefined, { ids: idsArray })
|
||||
}
|
||||
|
||||
/** 标记所有站内信为已读 */
|
||||
export function updateAllNotifyMessageRead() {
|
||||
return http.put<boolean>('/system/notify-message/update-all-read')
|
||||
}
|
||||
|
||||
/** 获取当前用户的未读站内信数量 */
|
||||
export function getUnreadNotifyMessageCount() {
|
||||
return http.get<number>('/system/notify-message/get-unread-count')
|
||||
}
|
||||
54
src/api/system/notify/template/index.ts
Normal file
54
src/api/system/notify/template/index.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 站内信模板信息 */
|
||||
export interface NotifyTemplate {
|
||||
id?: number
|
||||
name: string
|
||||
nickname: string
|
||||
code: string
|
||||
content: string
|
||||
type?: number
|
||||
params?: string[]
|
||||
status: number
|
||||
remark?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 发送站内信请求 */
|
||||
export interface NotifySendReqVO {
|
||||
userId: number
|
||||
userType: number
|
||||
templateCode: string
|
||||
templateParams: Record<string, any>
|
||||
}
|
||||
|
||||
/** 查询站内信模板列表 */
|
||||
export function getNotifyTemplatePage(params: PageParam) {
|
||||
return http.get<PageResult<NotifyTemplate>>('/system/notify-template/page', params)
|
||||
}
|
||||
|
||||
/** 查询站内信模板详情 */
|
||||
export function getNotifyTemplate(id: number) {
|
||||
return http.get<NotifyTemplate>(`/system/notify-template/get`, { id })
|
||||
}
|
||||
|
||||
/** 新增站内信模板 */
|
||||
export function createNotifyTemplate(data: NotifyTemplate) {
|
||||
return http.post<number>('/system/notify-template/create', data)
|
||||
}
|
||||
|
||||
/** 修改站内信模板 */
|
||||
export function updateNotifyTemplate(data: NotifyTemplate) {
|
||||
return http.put<boolean>('/system/notify-template/update', data)
|
||||
}
|
||||
|
||||
/** 删除站内信模板 */
|
||||
export function deleteNotifyTemplate(id: number) {
|
||||
return http.delete<boolean>(`/system/notify-template/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 发送站内信 */
|
||||
export function sendNotify(data: NotifySendReqVO) {
|
||||
return http.post<number>('/system/notify-template/send-notify', data)
|
||||
}
|
||||
48
src/api/system/oauth2/client/index.ts
Normal file
48
src/api/system/oauth2/client/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** OAuth2.0 客户端信息 */
|
||||
export interface OAuth2Client {
|
||||
id?: number
|
||||
clientId: string
|
||||
secret: string
|
||||
name: string
|
||||
logo: string
|
||||
description: string
|
||||
status: number
|
||||
accessTokenValiditySeconds: number
|
||||
refreshTokenValiditySeconds: number
|
||||
redirectUris: string[]
|
||||
autoApprove: boolean
|
||||
authorizedGrantTypes: string[]
|
||||
scopes: string[]
|
||||
authorities: string[]
|
||||
resourceIds: string[]
|
||||
additionalInformation: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取 OAuth2.0 客户端分页列表 */
|
||||
export function getOAuth2ClientPage(params: PageParam) {
|
||||
return http.get<PageResult<OAuth2Client>>('/system/oauth2-client/page', params)
|
||||
}
|
||||
|
||||
/** 获取 OAuth2.0 客户端详情 */
|
||||
export function getOAuth2Client(id: number) {
|
||||
return http.get<OAuth2Client>(`/system/oauth2-client/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建 OAuth2.0 客户端 */
|
||||
export function createOAuth2Client(data: OAuth2Client) {
|
||||
return http.post<number>('/system/oauth2-client/create', data)
|
||||
}
|
||||
|
||||
/** 更新 OAuth2.0 客户端 */
|
||||
export function updateOAuth2Client(data: OAuth2Client) {
|
||||
return http.put<boolean>('/system/oauth2-client/update', data)
|
||||
}
|
||||
|
||||
/** 删除 OAuth2.0 客户端 */
|
||||
export function deleteOAuth2Client(id: number) {
|
||||
return http.delete<boolean>(`/system/oauth2-client/delete?id=${id}`)
|
||||
}
|
||||
24
src/api/system/oauth2/token/index.ts
Normal file
24
src/api/system/oauth2/token/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** OAuth2.0 令牌信息 */
|
||||
export interface OAuth2Token {
|
||||
id?: number
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
userId: number
|
||||
userType: number
|
||||
clientId: string
|
||||
createTime?: Date
|
||||
expiresTime?: Date
|
||||
}
|
||||
|
||||
/** 获取 OAuth2.0 令牌分页列表 */
|
||||
export function getOAuth2TokenPage(params: PageParam) {
|
||||
return http.get<PageResult<OAuth2Token>>('/system/oauth2-token/page', params)
|
||||
}
|
||||
|
||||
/** 删除 OAuth2.0 令牌 */
|
||||
export function deleteOAuth2Token(accessToken: string) {
|
||||
return http.delete<boolean>(`/system/oauth2-token/delete?accessToken=${accessToken}`)
|
||||
}
|
||||
31
src/api/system/operate-log/index.ts
Normal file
31
src/api/system/operate-log/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 操作日志信息 */
|
||||
export interface OperateLog {
|
||||
id?: number
|
||||
traceId?: string
|
||||
userId?: number
|
||||
userType?: number
|
||||
userName?: string
|
||||
type?: string
|
||||
subType?: string
|
||||
bizId?: number
|
||||
action?: string
|
||||
extra?: string
|
||||
requestMethod?: string
|
||||
requestUrl?: string
|
||||
userIp?: string
|
||||
userAgent?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取操作日志分页列表 */
|
||||
export function getOperateLogPage(params: PageParam) {
|
||||
return http.get<PageResult<OperateLog>>('/system/operate-log/page', params)
|
||||
}
|
||||
|
||||
/** 获取操作日志详情 */
|
||||
export function getOperateLog(id: number) {
|
||||
return http.get<OperateLog>(`/system/operate-log/get?id=${id}`)
|
||||
}
|
||||
43
src/api/system/post/index.ts
Normal file
43
src/api/system/post/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 岗位信息 */
|
||||
export interface Post {
|
||||
id?: number
|
||||
name: string
|
||||
code: string
|
||||
sort: number
|
||||
status: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 获取岗位分页列表 */
|
||||
export function getPostPage(params: PageParam) {
|
||||
return http.get<PageResult<Post>>('/system/post/page', params)
|
||||
}
|
||||
|
||||
/** 获取岗位精简列表 */
|
||||
export function getSimplePostList() {
|
||||
return http.get<Post[]>('/system/post/simple-list')
|
||||
}
|
||||
|
||||
/** 获取岗位详情 */
|
||||
export function getPost(id: number) {
|
||||
return http.get<Post>(`/system/post/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建岗位 */
|
||||
export function createPost(data: Post) {
|
||||
return http.post<number>('/system/post/create', data)
|
||||
}
|
||||
|
||||
/** 更新岗位 */
|
||||
export function updatePost(data: Post) {
|
||||
return http.put<boolean>('/system/post/update', data)
|
||||
}
|
||||
|
||||
/** 删除岗位 */
|
||||
export function deletePost(id: number) {
|
||||
return http.delete<boolean>(`/system/post/delete?id=${id}`)
|
||||
}
|
||||
46
src/api/system/role/index.ts
Normal file
46
src/api/system/role/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 角色信息 */
|
||||
export interface Role {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
sort: number
|
||||
status: number
|
||||
type?: number
|
||||
remark?: string
|
||||
dataScope?: number
|
||||
dataScopeDeptIds?: number[]
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取角色分页列表 */
|
||||
export function getRolePage(params: PageParam) {
|
||||
return http.get<PageResult<Role>>('/system/role/page', params)
|
||||
}
|
||||
|
||||
/** 获取角色详情 */
|
||||
export function getRole(id: number) {
|
||||
return http.get<Role>(`/system/role/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建角色 */
|
||||
export function createRole(data: Role) {
|
||||
return http.post<number>('/system/role/create', data)
|
||||
}
|
||||
|
||||
/** 更新角色 */
|
||||
export function updateRole(data: Role) {
|
||||
return http.put<boolean>('/system/role/update', data)
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
export function deleteRole(id: number) {
|
||||
return http.delete<boolean>(`/system/role/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 获取角色精简列表 */
|
||||
export function getSimpleRoleList() {
|
||||
return http.get<Role[]>('/system/role/simple-list')
|
||||
}
|
||||
45
src/api/system/sms/channel/index.ts
Normal file
45
src/api/system/sms/channel/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 短信渠道信息 */
|
||||
export interface SmsChannel {
|
||||
id?: number
|
||||
code: string
|
||||
status: number
|
||||
signature: string
|
||||
remark?: string
|
||||
apiKey: string
|
||||
apiSecret?: string
|
||||
callbackUrl?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取短信渠道分页列表 */
|
||||
export function getSmsChannelPage(params: PageParam) {
|
||||
return http.get<PageResult<SmsChannel>>('/system/sms-channel/page', params)
|
||||
}
|
||||
|
||||
/** 获取短信渠道精简列表 */
|
||||
export function getSimpleSmsChannelList() {
|
||||
return http.get<SmsChannel[]>('/system/sms-channel/simple-list')
|
||||
}
|
||||
|
||||
/** 获取短信渠道详情 */
|
||||
export function getSmsChannel(id: number) {
|
||||
return http.get<SmsChannel>(`/system/sms-channel/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建短信渠道 */
|
||||
export function createSmsChannel(data: SmsChannel) {
|
||||
return http.post<number>('/system/sms-channel/create', data)
|
||||
}
|
||||
|
||||
/** 更新短信渠道 */
|
||||
export function updateSmsChannel(data: SmsChannel) {
|
||||
return http.put<boolean>('/system/sms-channel/update', data)
|
||||
}
|
||||
|
||||
/** 删除短信渠道 */
|
||||
export function deleteSmsChannel(id: number) {
|
||||
return http.delete<boolean>(`/system/sms-channel/delete?id=${id}`)
|
||||
}
|
||||
39
src/api/system/sms/log/index.ts
Normal file
39
src/api/system/sms/log/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 短信日志信息 */
|
||||
export interface SmsLog {
|
||||
id?: number
|
||||
channelId?: number
|
||||
channelCode: string
|
||||
templateId?: number
|
||||
templateCode: string
|
||||
templateType?: number
|
||||
templateContent: string
|
||||
templateParams?: Record<string, any>
|
||||
apiTemplateId: string
|
||||
mobile: string
|
||||
userId?: number
|
||||
userType?: number
|
||||
sendStatus?: number
|
||||
sendTime?: string
|
||||
apiSendCode?: string
|
||||
apiSendMsg?: string
|
||||
apiRequestId?: string
|
||||
apiSerialNo?: string
|
||||
receiveStatus?: number
|
||||
receiveTime?: string
|
||||
apiReceiveCode?: string
|
||||
apiReceiveMsg?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 获取短信日志分页列表 */
|
||||
export function getSmsLogPage(params: PageParam) {
|
||||
return http.get<PageResult<SmsLog>>('/system/sms-log/page', params)
|
||||
}
|
||||
|
||||
/** 获取短信日志详情 */
|
||||
export function getSmsLog(id: number) {
|
||||
return http.get<SmsLog>(`/system/sms-log/get?id=${id}`)
|
||||
}
|
||||
55
src/api/system/sms/template/index.ts
Normal file
55
src/api/system/sms/template/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 短信模板信息 */
|
||||
export interface SmsTemplate {
|
||||
id?: number
|
||||
type?: number
|
||||
status: number
|
||||
code: string
|
||||
name: string
|
||||
content: string
|
||||
remark?: string
|
||||
apiTemplateId: string
|
||||
channelId?: number
|
||||
channelCode?: string
|
||||
params?: string[]
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 发送短信请求 */
|
||||
export interface SmsSendReqVO {
|
||||
mobile: string
|
||||
templateCode: string
|
||||
templateParams: Record<string, any>
|
||||
}
|
||||
|
||||
/** 获取短信模板分页列表 */
|
||||
export function getSmsTemplatePage(params: PageParam) {
|
||||
return http.get<PageResult<SmsTemplate>>('/system/sms-template/page', params)
|
||||
}
|
||||
|
||||
/** 获取短信模板详情 */
|
||||
export function getSmsTemplate(id: number) {
|
||||
return http.get<SmsTemplate>(`/system/sms-template/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建短信模板 */
|
||||
export function createSmsTemplate(data: SmsTemplate) {
|
||||
return http.post<number>('/system/sms-template/create', data)
|
||||
}
|
||||
|
||||
/** 更新短信模板 */
|
||||
export function updateSmsTemplate(data: SmsTemplate) {
|
||||
return http.put<boolean>('/system/sms-template/update', data)
|
||||
}
|
||||
|
||||
/** 删除短信模板 */
|
||||
export function deleteSmsTemplate(id: number) {
|
||||
return http.delete<boolean>(`/system/sms-template/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 发送短信 */
|
||||
export function sendSms(data: SmsSendReqVO) {
|
||||
return http.post<number>('/system/sms-template/send-sms', data)
|
||||
}
|
||||
41
src/api/system/social/client/index.ts
Normal file
41
src/api/system/social/client/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 社交客户端信息 */
|
||||
export interface SocialClient {
|
||||
id?: number
|
||||
name: string
|
||||
socialType: number
|
||||
userType: number
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
agentId?: string
|
||||
publicKey?: string
|
||||
status: number
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取社交客户端分页列表 */
|
||||
export function getSocialClientPage(params: PageParam) {
|
||||
return http.get<PageResult<SocialClient>>('/system/social-client/page', params)
|
||||
}
|
||||
|
||||
/** 获取社交客户端详情 */
|
||||
export function getSocialClient(id: number) {
|
||||
return http.get<SocialClient>(`/system/social-client/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建社交客户端 */
|
||||
export function createSocialClient(data: SocialClient) {
|
||||
return http.post<number>('/system/social-client/create', data)
|
||||
}
|
||||
|
||||
/** 更新社交客户端 */
|
||||
export function updateSocialClient(data: SocialClient) {
|
||||
return http.put<boolean>('/system/social-client/update', data)
|
||||
}
|
||||
|
||||
/** 删除社交客户端 */
|
||||
export function deleteSocialClient(id: number) {
|
||||
return http.delete<boolean>(`/system/social-client/delete?id=${id}`)
|
||||
}
|
||||
28
src/api/system/social/user/index.ts
Normal file
28
src/api/system/social/user/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 社交用户信息 */
|
||||
export interface SocialUser {
|
||||
id?: number
|
||||
type: number
|
||||
openid: string
|
||||
token: string
|
||||
rawTokenInfo: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
rawUserInfo: string
|
||||
code: string
|
||||
state: string
|
||||
createTime?: Date
|
||||
updateTime?: Date
|
||||
}
|
||||
|
||||
/** 获取社交用户分页列表 */
|
||||
export function getSocialUserPage(params: PageParam) {
|
||||
return http.get<PageResult<SocialUser>>('/system/social-user/page', params)
|
||||
}
|
||||
|
||||
/** 获取社交用户详情 */
|
||||
export function getSocialUser(id: number) {
|
||||
return http.get<SocialUser>(`/system/social-user/get?id=${id}`)
|
||||
}
|
||||
46
src/api/system/tenant/index.ts
Normal file
46
src/api/system/tenant/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 租户信息 */
|
||||
export interface Tenant {
|
||||
id?: number
|
||||
name: string
|
||||
packageId: number
|
||||
contactName: string
|
||||
contactMobile: string
|
||||
accountCount: number
|
||||
expireTime: Date | any
|
||||
websites: string[]
|
||||
status: number
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取租户分页列表 */
|
||||
export function getTenantPage(params: PageParam) {
|
||||
return http.get<PageResult<Tenant>>('/system/tenant/page', params)
|
||||
}
|
||||
|
||||
/** 获取租户精简信息列表 */
|
||||
export function getSimpleTenantList() {
|
||||
return http.get<Tenant[]>('/system/tenant/simple-list')
|
||||
}
|
||||
|
||||
/** 获取租户详情 */
|
||||
export function getTenant(id: number) {
|
||||
return http.get<Tenant>(`/system/tenant/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建租户 */
|
||||
export function createTenant(data: Tenant) {
|
||||
return http.post<number>('/system/tenant/create', data)
|
||||
}
|
||||
|
||||
/** 更新租户 */
|
||||
export function updateTenant(data: Tenant) {
|
||||
return http.put<boolean>('/system/tenant/update', data)
|
||||
}
|
||||
|
||||
/** 删除租户 */
|
||||
export function deleteTenant(id: number) {
|
||||
return http.delete<boolean>(`/system/tenant/delete?id=${id}`)
|
||||
}
|
||||
42
src/api/system/tenant/package/index.ts
Normal file
42
src/api/system/tenant/package/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 租户套餐信息 */
|
||||
export interface TenantPackage {
|
||||
id?: number
|
||||
name: string
|
||||
status: number
|
||||
remark: string
|
||||
menuIds: number[]
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 获取租户套餐分页列表 */
|
||||
export function getTenantPackagePage(params: PageParam) {
|
||||
return http.get<PageResult<TenantPackage>>('/system/tenant-package/page', params)
|
||||
}
|
||||
|
||||
/** 获取租户套餐精简信息列表 */
|
||||
export function getTenantPackageList() {
|
||||
return http.get<TenantPackage[]>('/system/tenant-package/get-simple-list')
|
||||
}
|
||||
|
||||
/** 获取租户套餐详情 */
|
||||
export function getTenantPackage(id: number) {
|
||||
return http.get<TenantPackage>(`/system/tenant-package/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建租户套餐 */
|
||||
export function createTenantPackage(data: TenantPackage) {
|
||||
return http.post<number>('/system/tenant-package/create', data)
|
||||
}
|
||||
|
||||
/** 更新租户套餐 */
|
||||
export function updateTenantPackage(data: TenantPackage) {
|
||||
return http.put<boolean>('/system/tenant-package/update', data)
|
||||
}
|
||||
|
||||
/** 删除租户套餐 */
|
||||
export function deleteTenantPackage(id: number) {
|
||||
return http.delete<boolean>(`/system/tenant-package/delete?id=${id}`)
|
||||
}
|
||||
72
src/api/system/user/index.ts
Normal file
72
src/api/system/user/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 用户信息 */
|
||||
export interface User {
|
||||
id?: number
|
||||
username: string
|
||||
nickname: string
|
||||
password?: string
|
||||
deptId?: number
|
||||
deptName?: string
|
||||
postIds?: number[]
|
||||
email?: string
|
||||
mobile?: string
|
||||
sex?: number
|
||||
avatar?: string
|
||||
status: number
|
||||
remark?: string
|
||||
loginIp?: string
|
||||
loginDate?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
/** 获取用户分页列表 */
|
||||
export function getUserPage(params: PageParam) {
|
||||
return http.get<PageResult<User>>('/system/user/page', params)
|
||||
}
|
||||
|
||||
/** 获取用户详情 */
|
||||
export function getUser(id: number) {
|
||||
return http.get<User>(`/system/user/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建用户 */
|
||||
export function createUser(data: User) {
|
||||
return http.post<number>('/system/user/create', data)
|
||||
}
|
||||
|
||||
/** 更新用户 */
|
||||
export function updateUser(data: User) {
|
||||
return http.put<boolean>('/system/user/update', data)
|
||||
}
|
||||
|
||||
/** 删除用户 */
|
||||
export function deleteUser(id: number) {
|
||||
return http.delete<boolean>(`/system/user/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 重置用户密码 */
|
||||
export function resetUserPassword(id: number, password: string) {
|
||||
return http.put<boolean>('/system/user/update-password', { id, password })
|
||||
}
|
||||
|
||||
/** 修改用户状态 */
|
||||
export function updateUserStatus(id: number, status: number) {
|
||||
return http.put<boolean>('/system/user/update-status', { id, status })
|
||||
}
|
||||
|
||||
/** 获取用户拥有的角色列表 */
|
||||
export function getUserRoleIds(userId: number) {
|
||||
return http.get<number[]>(`/system/permission/list-user-roles?userId=${userId}`)
|
||||
}
|
||||
|
||||
/** 分配用户角色 */
|
||||
export function assignUserRole(userId: number, roleIds: number[]) {
|
||||
return http.post<boolean>('/system/permission/assign-user-role', { userId, roleIds })
|
||||
}
|
||||
|
||||
/** 获取用户精简列表 */
|
||||
export function getSimpleUserList() {
|
||||
return http.get<User[]>('/system/user/simple-list')
|
||||
}
|
||||
48
src/api/system/user/profile/index.ts
Normal file
48
src/api/system/user/profile/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 用户个人中心信息 */
|
||||
export interface UserProfileVO {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
email?: string
|
||||
mobile?: string
|
||||
sex?: number
|
||||
avatar?: string
|
||||
loginIp: string
|
||||
loginDate: Date
|
||||
createTime: Date
|
||||
roles: { id: number, name: string }[]
|
||||
dept: { id: number, name: string }
|
||||
posts: { id: number, name: string }[]
|
||||
}
|
||||
|
||||
/** 更新个人信息请求 */
|
||||
export interface UpdateProfileReqVO {
|
||||
nickname?: string
|
||||
email?: string
|
||||
mobile?: string
|
||||
sex?: number
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
/** 更新密码请求 */
|
||||
export interface UpdatePasswordReqVO {
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
/** 获取登录用户个人信息 */
|
||||
export function getUserProfile() {
|
||||
return http.get<UserProfileVO>('/system/user/profile/get')
|
||||
}
|
||||
|
||||
/** 修改用户个人信息 */
|
||||
export function updateUserProfile(data: UpdateProfileReqVO) {
|
||||
return http.put<boolean>('/system/user/profile/update', data)
|
||||
}
|
||||
|
||||
/** 修改用户个人密码 */
|
||||
export function updateUserPassword(data: UpdatePasswordReqVO) {
|
||||
return http.put<boolean>('/system/user/profile/update-password', data)
|
||||
}
|
||||
109
src/api/types/login.ts
Normal file
109
src/api/types/login.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
// 认证模式类型
|
||||
export type AuthMode = 'single' | 'double'
|
||||
|
||||
// 单Token响应类型
|
||||
export interface ISingleTokenRes {
|
||||
token: string
|
||||
expiresIn: number // 有效期(秒)
|
||||
}
|
||||
|
||||
// 双Token响应类型
|
||||
export interface IDoubleTokenRes {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
// accessExpiresIn: number // 访问令牌有效期(秒)
|
||||
// refreshExpiresIn: number // 刷新令牌有效期(秒)
|
||||
expiresTime: number // 访问令牌过期时间,单位:毫秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录返回的信息,其实就是 token 信息
|
||||
*/
|
||||
export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface IUserInfoRes {
|
||||
userId: number
|
||||
username: string
|
||||
nickname: string
|
||||
avatar?: string
|
||||
[key: string]: any // 允许其他扩展字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限信息
|
||||
*/
|
||||
export interface AuthPermissionInfo {
|
||||
user: IUserInfoRes
|
||||
roles: string[]
|
||||
permissions: string[]
|
||||
// menus: AppRouteRecordRaw[]; // add by 芋艿:暂时用不到
|
||||
}
|
||||
|
||||
// 认证存储数据结构
|
||||
// TODO @芋艿:可以考虑删除
|
||||
export interface AuthStorage {
|
||||
mode: AuthMode
|
||||
tokens: ISingleTokenRes | IDoubleTokenRes
|
||||
userInfo?: IUserInfoRes
|
||||
loginTime: number // 登录时间戳
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
export interface ICaptcha {
|
||||
captchaEnabled: boolean
|
||||
uuid: string
|
||||
image: string
|
||||
}
|
||||
/**
|
||||
* 上传成功的信息
|
||||
*/
|
||||
export interface IUploadSuccessInfo {
|
||||
fileId: number
|
||||
originalName: string
|
||||
fileName: string
|
||||
storagePath: string
|
||||
fileHash: string
|
||||
fileType: string
|
||||
fileBusinessType: string
|
||||
fileSize: number
|
||||
}
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export interface IUpdateInfo {
|
||||
id: number
|
||||
name: string
|
||||
sex: string
|
||||
}
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
export interface IUpdatePassword {
|
||||
id: number
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为单Token响应
|
||||
* @param tokenRes 登录响应数据
|
||||
* @returns 是否为单Token响应
|
||||
*/
|
||||
export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
|
||||
return 'token' in tokenRes && !('refreshToken' in tokenRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为双Token响应
|
||||
* @param tokenRes 登录响应数据
|
||||
* @returns 是否为双Token响应
|
||||
*/
|
||||
export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
|
||||
return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
|
||||
}
|
||||
0
src/components/.gitkeep
Normal file
0
src/components/.gitkeep
Normal file
62
src/components/dict-tag/dict-tag.vue
Normal file
62
src/components/dict-tag/dict-tag.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import type { TagType } from 'wot-design-uni/components/wd-tag/types'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { getDictObj } from '@/hooks/useDict'
|
||||
|
||||
interface DictTagProps {
|
||||
type: string // 字典类型
|
||||
value: any // 字典值
|
||||
plain?: boolean // 是否镂空,默认为 true
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DictTagProps>(), {
|
||||
plain: true,
|
||||
})
|
||||
|
||||
/**
|
||||
* 后端颜色类型 => wd-tag 的 type 映射
|
||||
*
|
||||
* 后端可配置:default、primary、success、info、warning、danger
|
||||
* wd-tag 支持:default、primary、success、warning、danger
|
||||
* 匹配不上时默认用 default
|
||||
*/
|
||||
const COLOR_TYPE_MAP: Record<string, TagType> = {
|
||||
default: 'default',
|
||||
primary: 'primary',
|
||||
success: 'success',
|
||||
info: 'default', // wd-tag 无 info,映射为 default
|
||||
warning: 'warning',
|
||||
danger: 'danger',
|
||||
}
|
||||
|
||||
/** 获取字典标签 */
|
||||
const dictTag = computed(() => {
|
||||
// 校验参数有效性
|
||||
if (!props.type || props.value === undefined || props.value === null) {
|
||||
return null
|
||||
}
|
||||
// 获取字典对象
|
||||
const dict = getDictObj(props.type, String(props.value))
|
||||
if (!dict) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
label: dict.label || '',
|
||||
tagType: COLOR_TYPE_MAP[dict.colorType || ''] || 'default',
|
||||
cssClass: dict.cssClass,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-tag
|
||||
v-if="dictTag"
|
||||
:type="dictTag.tagType"
|
||||
:plain="plain"
|
||||
:custom-class="dictTag.cssClass"
|
||||
>
|
||||
{{ dictTag.label }}
|
||||
</wd-tag>
|
||||
</template>
|
||||
1
src/components/dict-tag/index.ts
Normal file
1
src/components/dict-tag/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as DictTag } from './dict-tag.vue'
|
||||
1
src/components/system-select/index.ts
Normal file
1
src/components/system-select/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as UserPicker } from './user-picker.vue'
|
||||
116
src/components/system-select/user-picker.vue
Normal file
116
src/components/system-select/user-picker.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<wd-select-picker
|
||||
v-if="useDefaultSlot"
|
||||
v-model="selectedId"
|
||||
:label="label"
|
||||
:label-width="label ? '180rpx' : '0'"
|
||||
:columns="userList"
|
||||
value-key="id"
|
||||
label-key="nickname"
|
||||
:type="type"
|
||||
:prop="prop"
|
||||
use-default-slot
|
||||
filterable
|
||||
:placeholder="placeholder"
|
||||
@confirm="handleConfirm"
|
||||
>
|
||||
<slot />
|
||||
</wd-select-picker>
|
||||
|
||||
<wd-select-picker
|
||||
v-else
|
||||
v-model="selectedId"
|
||||
:label="label"
|
||||
:label-width="label ? '180rpx' : '0'"
|
||||
:columns="userList"
|
||||
value-key="id"
|
||||
label-key="nickname"
|
||||
:type="type"
|
||||
:prop="prop"
|
||||
filterable
|
||||
:placeholder="placeholder"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { User } from '@/api/system/user'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: number | number[]
|
||||
type?: 'radio' | 'checkbox'
|
||||
label?: string
|
||||
placeholder?: string
|
||||
prop?: string
|
||||
useDefaultSlot?: boolean
|
||||
}>(), {
|
||||
type: 'checkbox',
|
||||
label: '',
|
||||
placeholder: '请选择',
|
||||
prop: '',
|
||||
useDefaultSlot: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number | number[] | undefined): void
|
||||
(e: 'confirm', users: User[]): void
|
||||
}>()
|
||||
|
||||
const userList = ref<User[]>([])
|
||||
const selectedId = ref<number | string | number[]>([])
|
||||
|
||||
/** 根据用户 ID 获取昵称 */
|
||||
function getUserNickname(userId: number | undefined): string {
|
||||
if (!userId) {
|
||||
return ''
|
||||
}
|
||||
const user = userList.value.find(u => u.id === userId)
|
||||
return user?.nickname || ''
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getUserNickname,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (props.type === 'radio') {
|
||||
// 单选时,如果值为 undefined,使用空字符串避免警告
|
||||
selectedId.value = val !== undefined ? val : ''
|
||||
} else {
|
||||
// 多选时,确保是数组
|
||||
selectedId.value = Array.isArray(val) ? val : []
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 加载用户列表 */
|
||||
async function loadUserList() {
|
||||
userList.value = await getSimpleUserList()
|
||||
}
|
||||
|
||||
/** 选择确认 */
|
||||
function handleConfirm({ value }: { value: any }) {
|
||||
emit('update:modelValue', value)
|
||||
|
||||
// 发出包含完整用户对象的 confirm 事件
|
||||
if (Array.isArray(value)) {
|
||||
const selectedUsers = userList.value.filter(user => value.includes(user.id))
|
||||
emit('confirm', selectedUsers)
|
||||
} else if (value) {
|
||||
const selectedUser = userList.value.find(user => user.id === value)
|
||||
emit('confirm', selectedUser ? [selectedUser] : [])
|
||||
} else {
|
||||
emit('confirm', [])
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
})
|
||||
</script>
|
||||
35
src/env.d.ts
vendored
Normal file
35
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-svg-loader" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** 网站标题,应用名称 */
|
||||
readonly VITE_APP_TITLE: string
|
||||
/** 服务端口号 */
|
||||
readonly VITE_SERVER_PORT: string
|
||||
/** 后台接口地址 */
|
||||
readonly VITE_SERVER_BASEURL: string
|
||||
/** H5是否需要代理 */
|
||||
readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
|
||||
/** H5是否需要代理,需要的话有个前缀 */
|
||||
readonly VITE_APP_PROXY_PREFIX: string
|
||||
/** 后端是否有统一前缀 /api */
|
||||
readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false'
|
||||
/** 认证模式,'single' | 'double' ==> 单token | 双token */
|
||||
readonly VITE_AUTH_MODE: 'single' | 'double'
|
||||
/** 是否清除console */
|
||||
readonly VITE_DELETE_CONSOLE: string
|
||||
// 更多环境变量...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare const __VITE_APP_PROXY__: 'true' | 'false'
|
||||
41
src/hooks/useAccess.ts
Normal file
41
src/hooks/useAccess.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
/**
|
||||
* 权限控制 Hook
|
||||
* @description 提供基于角色和权限码的权限判断方法
|
||||
*/
|
||||
function useAccess() {
|
||||
const userStore = useUserStore()
|
||||
|
||||
/**
|
||||
* 基于角色判断是否有权限
|
||||
* @description 通过用户的角色列表判断是否具有指定角色
|
||||
* @param roles 需要判断的角色列表
|
||||
* @returns 是否具有指定角色中的任意一个
|
||||
*/
|
||||
function hasAccessByRoles(roles: string[]): boolean {
|
||||
const userRoleSet = new Set(userStore.roles)
|
||||
const intersection = roles.filter(item => userRoleSet.has(item))
|
||||
return intersection.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于权限码判断是否有权限
|
||||
* @description 通过用户的权限码列表判断是否具有指定权限
|
||||
* @param codes 需要判断的权限码列表
|
||||
* @returns 是否具有指定权限码中的任意一个
|
||||
*/
|
||||
function hasAccessByCodes(codes: string[]): boolean {
|
||||
const userCodesSet = new Set(userStore.permissions)
|
||||
const intersection = codes.filter(item => userCodesSet.has(item))
|
||||
return intersection.length > 0
|
||||
}
|
||||
|
||||
return {
|
||||
hasAccessByCodes,
|
||||
hasAccessByRoles,
|
||||
}
|
||||
}
|
||||
|
||||
export { useAccess }
|
||||
export default useAccess
|
||||
132
src/hooks/useDict.ts
Normal file
132
src/hooks/useDict.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { DictItem } from '@/store/dict'
|
||||
import { useDictStore } from '@/store/dict'
|
||||
|
||||
type ColorType = 'error' | 'info' | 'primary' | 'success' | 'warning'
|
||||
|
||||
export interface DictDataType {
|
||||
dictType?: string
|
||||
label: string
|
||||
value: boolean | number | string
|
||||
colorType?: string
|
||||
cssClass?: string
|
||||
}
|
||||
|
||||
export interface NumberDictDataType extends DictDataType {
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface StringDictDataType extends DictDataType {
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典标签
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @param value 字典值
|
||||
* @returns 字典标签
|
||||
*/
|
||||
export function getDictLabel(dictType: string, value: any): string {
|
||||
const dictStore = useDictStore()
|
||||
const dictObj = dictStore.getDictData(dictType, value)
|
||||
return dictObj ? dictObj.label : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典对象
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @param value 字典值
|
||||
* @returns 字典对象
|
||||
*/
|
||||
export function getDictObj(dictType: string, value: any): DictItem | null {
|
||||
const dictStore = useDictStore()
|
||||
const dictObj = dictStore.getDictData(dictType, value)
|
||||
return dictObj || null
|
||||
}
|
||||
|
||||
export function getIntDictOptions(dictType: string): NumberDictDataType[] {
|
||||
// 获得通用的 DictDataType 列表
|
||||
const dictOptions: DictDataType[] = getDictOptions(dictType)
|
||||
// 转换成 number 类型的 NumberDictDataType 类型
|
||||
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警
|
||||
const dictOption: NumberDictDataType[] = []
|
||||
dictOptions.forEach((dict: DictDataType) => {
|
||||
dictOption.push({
|
||||
...dict,
|
||||
value: Number.parseInt(`${dict.value}`),
|
||||
})
|
||||
})
|
||||
return dictOption
|
||||
}
|
||||
|
||||
export function getStrDictOptions(dictType: string) {
|
||||
// 获得通用的 DictDataType 列表
|
||||
const dictOptions: DictDataType[] = getDictOptions(dictType)
|
||||
// 转换成 string 类型的 StringDictDataType 类型
|
||||
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警
|
||||
const dictOption: StringDictDataType[] = []
|
||||
dictOptions.forEach((dict: DictDataType) => {
|
||||
dictOption.push({
|
||||
...dict,
|
||||
value: `${dict.value}`,
|
||||
})
|
||||
})
|
||||
return dictOption
|
||||
}
|
||||
|
||||
export function getBoolDictOptions(dictType: string) {
|
||||
const dictOption: DictDataType[] = []
|
||||
const dictOptions: DictDataType[] = getDictOptions(dictType)
|
||||
dictOptions.forEach((dict: DictDataType) => {
|
||||
dictOption.push({
|
||||
...dict,
|
||||
value: `${dict.value}` === 'true',
|
||||
})
|
||||
})
|
||||
return dictOption
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数组,用于 picker、radio 等
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @param valueType 字典值类型,默认 string 类型
|
||||
* @returns 字典数组
|
||||
*/
|
||||
export function getDictOptions(
|
||||
dictType: string,
|
||||
valueType: 'boolean' | 'number' | 'string' = 'string',
|
||||
): DictDataType[] {
|
||||
const dictStore = useDictStore()
|
||||
const dictOpts = dictStore.getDictOptions(dictType)
|
||||
const dictOptions: DictDataType[] = []
|
||||
|
||||
if (dictOpts.length > 0) {
|
||||
let dictValue: boolean | number | string = ''
|
||||
dictOpts.forEach((dict) => {
|
||||
switch (valueType) {
|
||||
case 'boolean': {
|
||||
dictValue = `${dict.value}` === 'true'
|
||||
break
|
||||
}
|
||||
case 'number': {
|
||||
dictValue = Number.parseInt(`${dict.value}`)
|
||||
break
|
||||
}
|
||||
case 'string': {
|
||||
dictValue = `${dict.value}`
|
||||
break
|
||||
}
|
||||
}
|
||||
dictOptions.push({
|
||||
value: dictValue,
|
||||
label: dict.label,
|
||||
colorType: dict.colorType as ColorType,
|
||||
cssClass: dict.cssClass,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return dictOptions
|
||||
}
|
||||
54
src/hooks/useRequest.ts
Normal file
54
src/hooks/useRequest.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface IUseRequestOptions<T> {
|
||||
/** 是否立即执行 */
|
||||
immediate?: boolean
|
||||
/** 初始化数据 */
|
||||
initialData?: T
|
||||
}
|
||||
|
||||
interface IUseRequestReturn<T, P = undefined> {
|
||||
loading: Ref<boolean>
|
||||
error: Ref<boolean | Error>
|
||||
data: Ref<T | undefined>
|
||||
run: (args?: P) => Promise<T | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
|
||||
* @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
|
||||
* @param options 包含请求选项的对象 {immediate, initialData}。
|
||||
* @param options.immediate 是否立即执行请求,默认为false。
|
||||
* @param options.initialData 初始化数据,默认为undefined。
|
||||
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
|
||||
*/
|
||||
export default function useRequest<T, P = undefined>(
|
||||
func: (args?: P) => Promise<T>,
|
||||
options: IUseRequestOptions<T> = { immediate: false },
|
||||
): IUseRequestReturn<T, P> {
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
|
||||
const run = async (args?: P) => {
|
||||
loading.value = true
|
||||
return func(args)
|
||||
.then((res) => {
|
||||
data.value = res
|
||||
error.value = false
|
||||
return data.value
|
||||
})
|
||||
.catch((err) => {
|
||||
error.value = err
|
||||
throw err
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
if (options.immediate) {
|
||||
(run as (args: P) => Promise<T | undefined>)({} as P)
|
||||
}
|
||||
return { loading, error, data, run }
|
||||
}
|
||||
116
src/hooks/useScroll.md
Normal file
116
src/hooks/useScroll.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# 上拉刷新和下拉加载更多
|
||||
|
||||
在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。
|
||||
场景一 页面滚动
|
||||
|
||||
```
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '上拉刷新和下拉加载更多',
|
||||
enablePullDownRefresh: true,
|
||||
onReachBottomDistance: 100,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
场景二 局部滚动 结合 `scroll-view`
|
||||
|
||||
## 关键文件
|
||||
|
||||
- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。
|
||||
- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。
|
||||
|
||||
## `useScroll` Hook
|
||||
|
||||
`useScroll` 是一个 Vue Composition API Hook,它封装了处理下拉刷新和上拉加载的通用逻辑。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。
|
||||
- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。
|
||||
- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。
|
||||
- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。
|
||||
|
||||
### 使用方法
|
||||
|
||||
```typescript
|
||||
import { useScroll } from '@/hooks/useScroll'
|
||||
import { getList } from '@/service/list' // 你的数据请求API
|
||||
|
||||
const {
|
||||
list, // 响应式的数据列表
|
||||
loading, // 是否加载中
|
||||
finished, // 是否已全部加载
|
||||
error, // 是否加载失败
|
||||
onScrollToLower, // 滚动到底部时触发的事件
|
||||
onRefresherRefresh, // 下拉刷新时触发的事件
|
||||
} = useScroll(getList) // 将获取数据的API函数传入
|
||||
```
|
||||
|
||||
## `scroll-view` 组件
|
||||
|
||||
`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。
|
||||
|
||||
### 关键属性
|
||||
|
||||
- `scroll-y`: 允许纵向滚动。
|
||||
- `refresher-enabled`: 启用下拉刷新。
|
||||
- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。
|
||||
- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。
|
||||
- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。
|
||||
|
||||
## 示例代码
|
||||
|
||||
以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll` 和 `scroll-view` 结合使用。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="scroll-page">
|
||||
<scroll-view
|
||||
class="scroll-view"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="loading"
|
||||
@scrolltolower="onScrollToLower"
|
||||
@refresherrefresh="onRefresherRefresh"
|
||||
>
|
||||
<view v-for="item in list" :key="item.id" class="scroll-item">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
|
||||
<!-- 加载状态提示 -->
|
||||
<view v-if="loading" class="loading-tip">加载中...</view>
|
||||
<view v-if="finished" class="finished-tip">没有更多了</view>
|
||||
<view v-if="error" class="error-tip">加载失败,请重试</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useScroll } from '@/hooks/useScroll'
|
||||
import { getList } from '@/service/list'
|
||||
|
||||
const { list, loading, finished, error, onScrollToLower, onRefresherRefresh } = useScroll(getList)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式省略 */
|
||||
.scroll-page, .scroll-view {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 实现步骤总结
|
||||
|
||||
1. **创建API**: 确保你有一个返回分页数据的API请求函数(例如 `getList`),它应该接受页码和页面大小作为参数。
|
||||
2. **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook,将你的API函数作为参数传入。
|
||||
3. **模板绑定**:
|
||||
- 使用 `scroll-view` 组件作为滚动容器。
|
||||
- 将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。
|
||||
- 将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。
|
||||
- 将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。
|
||||
4. **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。
|
||||
5. **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。
|
||||
|
||||
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。
|
||||
75
src/hooks/useScroll.ts
Normal file
75
src/hooks/useScroll.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable brace-style */ // 原因:unibest 官方维护的代码,尽量不要大概,避免难以合并
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
interface UseScrollOptions<T> {
|
||||
fetchData: (page: number, pageSize: number) => Promise<T[]>
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
interface UseScrollReturn<T> {
|
||||
list: Ref<T[]>
|
||||
loading: Ref<boolean>
|
||||
finished: Ref<boolean>
|
||||
error: Ref<any>
|
||||
refresh: () => Promise<void>
|
||||
loadMore: () => Promise<void>
|
||||
}
|
||||
|
||||
export function useScroll<T>({
|
||||
fetchData,
|
||||
pageSize = 10,
|
||||
}: UseScrollOptions<T>): UseScrollReturn<T> {
|
||||
const list = ref<T[]>([]) as Ref<T[]>
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref<any>(null)
|
||||
const page = ref(1)
|
||||
|
||||
const loadData = async () => {
|
||||
if (loading.value || finished.value)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const data = await fetchData(page.value, pageSize)
|
||||
if (data.length < pageSize) {
|
||||
finished.value = true
|
||||
}
|
||||
list.value.push(...data)
|
||||
page.value++
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
page.value = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
return {
|
||||
list,
|
||||
loading,
|
||||
finished,
|
||||
error,
|
||||
refresh,
|
||||
loadMore,
|
||||
}
|
||||
}
|
||||
172
src/hooks/useUpload.ts
Normal file
172
src/hooks/useUpload.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getEnvBaseUrl } from '@/utils/index'
|
||||
|
||||
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/upload`
|
||||
|
||||
type TfileType = 'image' | 'file'
|
||||
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
|
||||
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
|
||||
|
||||
interface TOptions<T extends TfileType> {
|
||||
formData?: Record<string, any>
|
||||
maxSize?: number
|
||||
accept?: T extends 'image' ? TImage[] : TFile[]
|
||||
fileType?: T
|
||||
success?: (params: any) => void
|
||||
error?: (err: any) => void
|
||||
}
|
||||
|
||||
export default function useUpload<T extends TfileType>(options: TOptions<T> = {} as TOptions<T>) {
|
||||
const {
|
||||
formData = {},
|
||||
maxSize = 5 * 1024 * 1024,
|
||||
accept = ['*'],
|
||||
fileType = 'image',
|
||||
success,
|
||||
error: onError,
|
||||
} = options
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
const data = ref<any>(null)
|
||||
const toast = useToast()
|
||||
|
||||
const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
|
||||
if (size > maxSize) {
|
||||
// 注释 by 芋艿:使用 wd-toast 替代
|
||||
// uni.showToast({
|
||||
// title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
|
||||
// icon: 'none',
|
||||
// })
|
||||
toast.show(`文件大小不能超过 ${maxSize / 1024 / 1024}MB`)
|
||||
return
|
||||
}
|
||||
|
||||
// const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
|
||||
// const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
|
||||
|
||||
// if (!isTypeValid) {
|
||||
// uni.showToast({
|
||||
// title: `仅支持 ${accept.join(', ')} 格式的文件`,
|
||||
// icon: 'none',
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
|
||||
loading.value = true
|
||||
uploadFile({
|
||||
tempFilePath,
|
||||
formData,
|
||||
onSuccess: (res) => {
|
||||
// 修改这里的解析逻辑,适应不同平台的返回格式
|
||||
let parsedData = res
|
||||
try {
|
||||
// 尝试解析为JSON
|
||||
const jsonData = JSON.parse(res)
|
||||
// 检查是否包含data字段
|
||||
parsedData = jsonData.data || jsonData
|
||||
} catch (e) {
|
||||
// 如果解析失败,使用原始数据
|
||||
console.log('Response is not JSON, using raw data:', res)
|
||||
}
|
||||
data.value = parsedData
|
||||
// console.log('上传成功', res)
|
||||
success?.(parsedData)
|
||||
},
|
||||
onError: (err) => {
|
||||
error.value = err
|
||||
onError?.(err)
|
||||
},
|
||||
onComplete: () => {
|
||||
loading.value = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const run = () => {
|
||||
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
|
||||
// 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
|
||||
const chooseFileOptions = {
|
||||
count: 1,
|
||||
success: (res: any) => {
|
||||
console.log('File selected successfully:', res)
|
||||
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
|
||||
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
|
||||
// h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
|
||||
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
|
||||
// App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
|
||||
let tempFilePath = ''
|
||||
let size = 0
|
||||
// #ifdef MP-WEIXIN
|
||||
tempFilePath = res.tempFiles[0].tempFilePath
|
||||
size = res.tempFiles[0].size
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
tempFilePath = res.tempFilePaths[0]
|
||||
size = res.tempFiles[0].size
|
||||
// #endif
|
||||
handleFileChoose({ tempFilePath, size })
|
||||
},
|
||||
fail: (err: any) => {
|
||||
console.error('File selection failed:', err)
|
||||
error.value = err
|
||||
onError?.(err)
|
||||
},
|
||||
}
|
||||
|
||||
if (fileType === 'image') {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
...chooseFileOptions,
|
||||
mediaType: ['image'],
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage(chooseFileOptions)
|
||||
// #endif
|
||||
} else {
|
||||
uni.chooseFile({
|
||||
...chooseFileOptions,
|
||||
type: 'all',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { loading, error, data, run }
|
||||
}
|
||||
|
||||
async function uploadFile({
|
||||
tempFilePath,
|
||||
formData,
|
||||
onSuccess,
|
||||
onError,
|
||||
onComplete,
|
||||
}: {
|
||||
tempFilePath: string
|
||||
formData: Record<string, any>
|
||||
onSuccess: (data: any) => void
|
||||
onError: (err: any) => void
|
||||
onComplete: () => void
|
||||
}) {
|
||||
uni.uploadFile({
|
||||
url: VITE_UPLOAD_BASEURL,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
formData,
|
||||
success: (uploadFileRes) => {
|
||||
try {
|
||||
const data = uploadFileRes.data
|
||||
onSuccess(data)
|
||||
} catch (err) {
|
||||
onError(err)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('Upload failed:', err)
|
||||
onError(err)
|
||||
},
|
||||
complete: onComplete,
|
||||
})
|
||||
}
|
||||
223
src/http/http.ts
Normal file
223
src/http/http.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import type { IDoubleTokenRes } from '@/api/types/login'
|
||||
import type { CustomRequestOptions, IResponse } from '@/http/types'
|
||||
import { nextTick } from 'vue'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { getLastPage, isDoubleTokenMode } from '@/utils'
|
||||
import { ApiEncrypt } from '@/utils/encrypt'
|
||||
import { toLoginPage } from '@/utils/toLoginPage'
|
||||
import { ResultEnum } from './tools/enum'
|
||||
|
||||
// 刷新 token 状态管理
|
||||
let refreshing = false // 防止重复刷新 token 标识
|
||||
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
|
||||
|
||||
export function http<T>(options: CustomRequestOptions) {
|
||||
// 1. 返回 Promise 对象
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
uni.request({
|
||||
...options,
|
||||
dataType: 'json',
|
||||
// #ifndef MP-WEIXIN
|
||||
responseType: 'json',
|
||||
// #endif
|
||||
// 响应成功
|
||||
success: async (res) => {
|
||||
let responseData = res.data as IResponse<T>
|
||||
// add by panda:检查是否需要解密响应数据
|
||||
const encryptHeader = ApiEncrypt.getEncryptHeader()
|
||||
const isEncryptResponse = res.header[encryptHeader] === 'true' || res.header[encryptHeader.toLowerCase()] === 'true'
|
||||
if (isEncryptResponse && typeof responseData === 'string') {
|
||||
try {
|
||||
// 解密响应数据
|
||||
responseData = ApiEncrypt.decryptResponse(responseData)
|
||||
} catch (error) {
|
||||
console.error('响应数据解密失败:', error)
|
||||
throw new Error(`响应数据解密失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const { code } = responseData
|
||||
// 检查是否是401错误(包括HTTP状态码401或业务码401)
|
||||
const isTokenExpired = res.statusCode === 401 || code === 401
|
||||
|
||||
if (isTokenExpired) {
|
||||
const tokenStore = useTokenStore()
|
||||
if (!isDoubleTokenMode) {
|
||||
// 未启用双token策略,清理用户信息,跳转到登录页
|
||||
tokenStore.logout()
|
||||
toLoginPage()
|
||||
return reject(res)
|
||||
}
|
||||
|
||||
/* -------- 无感刷新 token ----------- */
|
||||
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
|
||||
// token 失效的,且有刷新 token 的,才放到请求队列里
|
||||
if (refreshToken) {
|
||||
taskQueue.push(() => {
|
||||
resolve(http<T>(options))
|
||||
})
|
||||
}
|
||||
|
||||
// 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
|
||||
if (refreshToken && !refreshing) {
|
||||
refreshing = true
|
||||
try {
|
||||
// 发起刷新 token 请求(使用 store 的 refreshToken 方法)
|
||||
await tokenStore.refreshToken()
|
||||
// 刷新 token 成功
|
||||
refreshing = false
|
||||
nextTick(() => {
|
||||
// 关闭其他弹窗
|
||||
// 注释 by 芋艿:刷新 token 成功,是后台静默操作,没必要提示用户
|
||||
// uni.hideToast()
|
||||
// uni.showToast({
|
||||
// title: 'token 刷新成功',
|
||||
// icon: 'none',
|
||||
// })
|
||||
})
|
||||
// 将任务队列的所有任务重新请求
|
||||
taskQueue.forEach(task => task())
|
||||
} catch (refreshErr) {
|
||||
console.error('刷新 token 失败:', refreshErr)
|
||||
refreshing = false
|
||||
// 刷新 token 失败,跳转到登录页
|
||||
nextTick(() => {
|
||||
// 关闭其他弹窗
|
||||
uni.hideToast()
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none',
|
||||
})
|
||||
})
|
||||
// 清除用户信息
|
||||
await tokenStore.logout()
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
// 优化 by 芋艿:跳转登录页时,携带上次浏览的页面地址,登录成功后可以跳回去
|
||||
const lastPage = getLastPage()
|
||||
let queryString = ''
|
||||
if (lastPage) {
|
||||
const fullPath = lastPage.$page?.fullPath || `/${lastPage.route}`
|
||||
queryString = `?redirect=${encodeURIComponent(fullPath)}`
|
||||
}
|
||||
toLoginPage({ queryString })
|
||||
}, 2000)
|
||||
} finally {
|
||||
// 不管刷新 token 成功与否,都清空任务队列
|
||||
taskQueue = []
|
||||
}
|
||||
}
|
||||
|
||||
return reject(res)
|
||||
}
|
||||
|
||||
// 处理其他成功状态(HTTP状态码200-299)
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// add by panda 25.12.10:如果设置了 original 为 true,则返回原始数据。例如说:滑块验证码,有自己的返回格式
|
||||
if (options.original) {
|
||||
return resolve(responseData as unknown as T)
|
||||
}
|
||||
// 处理业务逻辑错误
|
||||
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
|
||||
// add by 芋艿:后端返回的 msg 提示
|
||||
!options.hideErrorToast
|
||||
&& uni.showToast({
|
||||
icon: 'none',
|
||||
title: responseData.msg || responseData.message || '请求错误',
|
||||
})
|
||||
// add by 芋艿:reject 替代原本的 resolve,避免调用的地方以为请求成功
|
||||
return reject(responseData)
|
||||
}
|
||||
return resolve(responseData.data)
|
||||
}
|
||||
|
||||
// 处理其他错误
|
||||
!options.hideErrorToast
|
||||
&& uni.showToast({
|
||||
icon: 'none',
|
||||
title: (res.data as any).msg || '请求错误',
|
||||
})
|
||||
reject(res)
|
||||
},
|
||||
// 响应失败
|
||||
fail(err) {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: '网络错误,换个网络试试',
|
||||
})
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
* @param url 后台地址
|
||||
* @param query 请求query参数
|
||||
* @param header 请求头,默认为json格式
|
||||
* @param options 其他配置项
|
||||
* @returns
|
||||
*/
|
||||
export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
query,
|
||||
method: 'GET',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
* @param url 后台地址
|
||||
* @param data 请求body参数
|
||||
* @param query 请求query参数,post请求也支持query,很多微信接口都需要
|
||||
* @param header 请求头,默认为json格式
|
||||
* @param options 其他配置项
|
||||
* @returns
|
||||
*/
|
||||
export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
query,
|
||||
data,
|
||||
method: 'POST',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
/**
|
||||
* PUT 请求
|
||||
*/
|
||||
export function httpPut<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
data,
|
||||
query,
|
||||
method: 'PUT',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求(无请求体,仅 query)
|
||||
*/
|
||||
export function httpDelete<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||
return http<T>({
|
||||
url,
|
||||
data,
|
||||
query,
|
||||
method: 'DELETE',
|
||||
header,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
// 支持与 axios 类似的API调用
|
||||
http.get = httpGet
|
||||
http.post = httpPost
|
||||
http.put = httpPut
|
||||
http.delete = httpDelete
|
||||
104
src/http/interceptor.ts
Normal file
104
src/http/interceptor.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/* eslint-disable brace-style */ // 原因:unibest 官方维护的代码,尽量不要大概,避免难以合并
|
||||
import type { CustomRequestOptions } from '@/http/types'
|
||||
import { useTokenStore, useUserStore } from '@/store'
|
||||
import { getEnvBaseUrl } from '@/utils'
|
||||
import { ApiEncrypt } from '@/utils/encrypt'
|
||||
import { stringifyQuery } from './tools/queryString'
|
||||
|
||||
// 请求基准地址
|
||||
const baseUrl = getEnvBaseUrl()
|
||||
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
|
||||
|
||||
const whiteList: string[] = [
|
||||
'/login',
|
||||
'/refresh-token',
|
||||
'/system/tenant/get-id-by-name',
|
||||
] // 白名单列表,不需要传递 token 字段
|
||||
|
||||
// 拦截器配置
|
||||
const httpInterceptor = {
|
||||
// 拦截前触发
|
||||
invoke(options: CustomRequestOptions) {
|
||||
// 接口请求支持通过 query 参数配置 queryString
|
||||
if (options.query) {
|
||||
const queryStr = stringifyQuery(options.query)
|
||||
if (options.url.includes('?')) {
|
||||
options.url += `&${queryStr}`
|
||||
}
|
||||
else {
|
||||
options.url += `?${queryStr}`
|
||||
}
|
||||
}
|
||||
// 非 http 开头需拼接地址
|
||||
if (!options.url.startsWith('http')) {
|
||||
// #ifdef H5
|
||||
if (JSON.parse(import.meta.env.VITE_APP_PROXY_ENABLE)) {
|
||||
// 自动拼接代理前缀
|
||||
options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
|
||||
}
|
||||
else {
|
||||
options.url = baseUrl + options.url
|
||||
}
|
||||
// #endif
|
||||
// 非H5正常拼接
|
||||
// #ifndef H5
|
||||
options.url = baseUrl + options.url
|
||||
// #endif
|
||||
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
|
||||
}
|
||||
// 1. 请求超时
|
||||
options.timeout = 60000 // 60s
|
||||
// 2. (可选)添加小程序端请求头标识
|
||||
options.header = {
|
||||
...options.header,
|
||||
}
|
||||
// 3. 添加 token 请求头标识
|
||||
const tokenStore = useTokenStore()
|
||||
const token = tokenStore.validToken
|
||||
let isToken = (options!.header || {}).isToken === false
|
||||
|
||||
for (const v of whiteList) {
|
||||
if (options.url && options.url.includes(v)) {
|
||||
isToken = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isToken && token) {
|
||||
options.header.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 4. 添加租户标识
|
||||
if (tenantEnable && tenantEnable === 'true') {
|
||||
const tenantId = useUserStore().tenantId
|
||||
if (tenantId) {
|
||||
options.header['tenant-id'] = tenantId
|
||||
}
|
||||
}
|
||||
|
||||
// 5. add by panda:是否 API 加密
|
||||
if (options.isEncrypt) {
|
||||
try {
|
||||
// 加密请求数据
|
||||
if (options.data) {
|
||||
options.data = ApiEncrypt.encryptRequest(options.data)
|
||||
// 设置加密标识头
|
||||
options.header[ApiEncrypt.getEncryptHeader()] = 'true'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求数据加密失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
},
|
||||
}
|
||||
|
||||
export const requestInterceptor = {
|
||||
install() {
|
||||
// 拦截 request 请求
|
||||
uni.addInterceptor('request', httpInterceptor)
|
||||
// 拦截 uploadFile 文件上传
|
||||
uni.addInterceptor('uploadFile', httpInterceptor)
|
||||
},
|
||||
}
|
||||
68
src/http/tools/enum.ts
Normal file
68
src/http/tools/enum.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export enum ResultEnum {
|
||||
// 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200)
|
||||
Success0 = 0, // 成功
|
||||
Success200 = 200, // 成功
|
||||
Error = 400, // 错误
|
||||
Unauthorized = 401, // 未授权
|
||||
Forbidden = 403, // 禁止访问(原为forbidden)
|
||||
NotFound = 404, // 未找到(原为notFound)
|
||||
MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
|
||||
RequestTimeout = 408, // 请求超时(原为requestTimeout)
|
||||
InternalServerError = 500, // 服务器错误(原为internalServerError)
|
||||
NotImplemented = 501, // 未实现(原为notImplemented)
|
||||
BadGateway = 502, // 网关错误(原为badGateway)
|
||||
ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
|
||||
GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
|
||||
HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
|
||||
}
|
||||
export enum ContentTypeEnum {
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||
}
|
||||
/**
|
||||
* 根据状态码,生成对应的错误信息
|
||||
* @param {number|string} status 状态码
|
||||
* @returns {string} 错误信息
|
||||
*/
|
||||
export function ShowMessage(status: number | string): string {
|
||||
let message: string
|
||||
switch (status) {
|
||||
case 400:
|
||||
message = '请求错误(400)'
|
||||
break
|
||||
case 401:
|
||||
message = '未授权,请重新登录(401)'
|
||||
break
|
||||
case 403:
|
||||
message = '拒绝访问(403)'
|
||||
break
|
||||
case 404:
|
||||
message = '请求出错(404)'
|
||||
break
|
||||
case 408:
|
||||
message = '请求超时(408)'
|
||||
break
|
||||
case 500:
|
||||
message = '服务器错误(500)'
|
||||
break
|
||||
case 501:
|
||||
message = '服务未实现(501)'
|
||||
break
|
||||
case 502:
|
||||
message = '网络错误(502)'
|
||||
break
|
||||
case 503:
|
||||
message = '服务不可用(503)'
|
||||
break
|
||||
case 504:
|
||||
message = '网络超时(504)'
|
||||
break
|
||||
case 505:
|
||||
message = 'HTTP版本不受支持(505)'
|
||||
break
|
||||
default:
|
||||
message = `连接出错(${status})!`
|
||||
}
|
||||
return `${message},请检查网络或联系管理员!`
|
||||
}
|
||||
29
src/http/tools/queryString.ts
Normal file
29
src/http/tools/queryString.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
|
||||
* 支持基本类型值和数组,不支持嵌套对象
|
||||
* @param obj 要序列化的对象
|
||||
* @returns 序列化后的查询字符串
|
||||
*/
|
||||
export function stringifyQuery(obj: Record<string, any>): string {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
||||
return ''
|
||||
|
||||
return Object.entries(obj)
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => {
|
||||
// 对键进行编码
|
||||
const encodedKey = encodeURIComponent(key)
|
||||
|
||||
// 处理数组类型
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.filter(item => item !== undefined && item !== null)
|
||||
.map(item => `${encodedKey}=${encodeURIComponent(item)}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
// 处理基本类型
|
||||
return `${encodedKey}=${encodeURIComponent(value)}`
|
||||
})
|
||||
.join('&')
|
||||
}
|
||||
41
src/http/types.ts
Normal file
41
src/http/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
|
||||
*/
|
||||
export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||
query?: Record<string, any>
|
||||
/** 出错时是否隐藏错误提示 */
|
||||
hideErrorToast?: boolean
|
||||
/** 是否返回原始数据 add by panda 25.12.10 */
|
||||
original?: boolean
|
||||
/** 是否API加密 add by panda 25.12.24 */
|
||||
isEncrypt?: boolean
|
||||
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
||||
|
||||
// 通用响应格式(兼容 msg + message 字段)
|
||||
export type IResponse<T = any> = {
|
||||
code: number
|
||||
data: T
|
||||
message: string
|
||||
[key: string]: any // 允许额外属性
|
||||
} | {
|
||||
code: number
|
||||
data: T
|
||||
msg: string
|
||||
[key: string]: any // 允许额外属性
|
||||
}
|
||||
|
||||
/** 分页参数 */
|
||||
export interface PageParam {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
[key: string]: any // 允许额外属性
|
||||
}
|
||||
|
||||
/** 分页结果 */
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/** 加载状态枚举 - 从 wot-design-uni 重新导出 */
|
||||
export type { LoadMoreState } from 'wot-design-uni/components/wd-loadmore/types'
|
||||
3
src/layouts/default.vue
Normal file
3
src/layouts/default.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
19
src/main.ts
Normal file
19
src/main.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { requestInterceptor } from './http/interceptor'
|
||||
import { routeInterceptor } from './router/interceptor'
|
||||
|
||||
import store from './store'
|
||||
import '@/style/index.scss'
|
||||
import 'virtual:uno.css'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.use(store)
|
||||
app.use(routeInterceptor)
|
||||
app.use(requestInterceptor)
|
||||
|
||||
return {
|
||||
app,
|
||||
}
|
||||
}
|
||||
168
src/pages-bpm/category/components/search-form.vue
Normal file
168
src/pages-bpm/category/components/search-form.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
分类名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入分类名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
分类标志
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入分类标志"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
分类状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`分类名:${formData.name}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`分类标志:${formData.code}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程分类'
|
||||
})
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
129
src/pages-bpm/category/detail/index.vue
Normal file
129
src/pages-bpm/category/detail/index.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程分类详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="分类编号" :value="formData?.id" />
|
||||
<wd-cell title="分类名" :value="formData?.name" />
|
||||
<wd-cell title="分类标志" :value="formData?.code" />
|
||||
<wd-cell title="分类描述" :value="formData?.description || '-'" />
|
||||
<wd-cell title="分类状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="分类排序" :value="formData?.sort" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:category:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:category:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteCategory, getCategory } from '@/api/bpm/category'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<Category>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/category/index')
|
||||
}
|
||||
|
||||
/** 加载流程分类详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getCategory(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑流程分类 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/category/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除流程分类 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该流程分类吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteCategory(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
159
src/pages-bpm/category/form/index.vue
Normal file
159
src/pages-bpm/category/form/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="分类名"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入分类名"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="分类标志"
|
||||
label-width="180rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入分类标志"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
label="分类描述"
|
||||
label-width="180rpx"
|
||||
prop="description"
|
||||
clearable
|
||||
placeholder="请输入分类描述"
|
||||
/>
|
||||
<wd-cell title="分类状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model.number="formData.sort"
|
||||
label="分类排序"
|
||||
label-width="180rpx"
|
||||
prop="sort"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入分类排序"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createCategory, getCategory, updateCategory } from '@/api/bpm/category'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑流程分类' : '新增流程分类')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<Category>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
description: '',
|
||||
sort: 0,
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '分类名不能为空' }],
|
||||
code: [{ required: true, message: '分类标志不能为空' }],
|
||||
status: [{ required: true, message: '分类状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/category/index')
|
||||
}
|
||||
|
||||
/** 加载流程分类详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getCategory(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateCategory(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createCategory(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
167
src/pages-bpm/category/index.vue
Normal file
167
src/pages-bpm/category/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程分类管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程分类列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">分类标志:</text>
|
||||
<text>{{ item.code }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">分类描述:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.description || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">分类排序:</text>
|
||||
<text>{{ item.sort }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程分类数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:category:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getCategoryPage } from '@/api/bpm/category'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Category[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程分类列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getCategoryPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增流程分类 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/category/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Category) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/category/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
168
src/pages-bpm/oa/leave/components/search-form.vue
Normal file
168
src/pages-bpm/oa/leave/components/search-form.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
请假类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
审批结果
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
请假原因
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.reason"
|
||||
placeholder="请输入请假原因"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
type: -1, // -1 表示全部
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
reason: undefined as string | undefined,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.type !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.BPM_OA_LEAVE_TYPE, formData.type)}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
if (formData.reason) {
|
||||
conditions.push(`原因:${formData.reason}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索请假记录'
|
||||
})
|
||||
|
||||
// 时间选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
type: formData.type === -1 ? undefined : formData.type,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
reason: formData.reason || undefined,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.type = -1
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
formData.reason = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
285
src/pages-bpm/oa/leave/create/index.vue
Normal file
285
src/pages-bpm/oa/leave/create/index.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<view class="yd-page-container pb-[76rpx]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="发起请假"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
<view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
|
||||
<!-- 表单内容 -->
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border title="请假信息">
|
||||
<wd-picker
|
||||
v-model="formData.type"
|
||||
:columns="getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
|
||||
label="请假类型"
|
||||
label-width="200rpx"
|
||||
prop="type"
|
||||
:rules="[{ required: true, message: '请选择请假类型' }]"
|
||||
placeholder="请选择请假类型"
|
||||
/>
|
||||
<wd-datetime-picker
|
||||
v-model="formData.startTime"
|
||||
label="开始时间"
|
||||
label-width="200rpx"
|
||||
prop="startTime"
|
||||
:rules="[{ required: true, message: '请选择开始时间' }]"
|
||||
placeholder="请选择开始时间"
|
||||
/>
|
||||
<wd-datetime-picker
|
||||
v-model="formData.endTime"
|
||||
label="结束时间"
|
||||
label-width="200rpx"
|
||||
prop="endTime"
|
||||
:rules="[{ required: true, message: '请选择结束时间' }]"
|
||||
placeholder="请选择结束时间"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
label="请假原因"
|
||||
label-width="200rpx"
|
||||
prop="reason"
|
||||
:rules="[{ required: true, message: '请输入请假原因' }]"
|
||||
placeholder="请输入请假原因"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
<!-- 流程预览卡片 -->
|
||||
<view class="mx-24rpx mb-120rpx mt-24rpx rounded-16rpx bg-white">
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<text class="text-28rpx text-[#333] font-bold">流程预览</text>
|
||||
<wd-loading v-if="processTimeLineLoading" size="32rpx" />
|
||||
</view>
|
||||
|
||||
<!-- 流程时间线 -->
|
||||
<ProcessInstanceTimeline
|
||||
v-if="activityNodes.length > 0"
|
||||
:activity-nodes="activityNodes"
|
||||
:show-status-icon="false"
|
||||
@select-user-confirm="selectUserConfirm"
|
||||
/>
|
||||
|
||||
<!-- 无流程数据提示 -->
|
||||
<view v-else-if="!processTimeLineLoading" class="py-40rpx text-center">
|
||||
<text class="text-24rpx text-[#999]">暂无流程预览数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部提交按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button type="primary" class="flex-1" :loading="formLoading" @click="handleSubmit">
|
||||
提交
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Leave } from '@/api/bpm/oa/leave'
|
||||
import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useMessage, useToast } from 'wot-design-uni'
|
||||
import { getProcessDefinition } from '@/api/bpm/definition'
|
||||
import { createLeave } from '@/api/bpm/oa/leave'
|
||||
import { getApprovalDetail } from '@/api/bpm/processInstance'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import ProcessInstanceTimeline from '@/pages-bpm/processInstance/detail/components/time-line.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmCandidateStrategyEnum, BpmNodeIdEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const message = useMessage()
|
||||
const formLoading = ref(false)
|
||||
const processTimeLineLoading = ref(false) // 流程预览加载状态
|
||||
|
||||
// 流程相关数据
|
||||
const processDefineKey = 'oa_leave' // 流程定义 Key
|
||||
const processDefinitionId = ref('')
|
||||
const activityNodes = ref<ApprovalNodeInfo[]>([]) // 审批节点信息
|
||||
const startUserSelectTasks = ref<any[]>([]) // 发起人需要选择审批人的用户任务列表
|
||||
const startUserSelectAssignees = ref<any>({}) // 发起人选择审批人的数据
|
||||
const tempStartUserSelectAssignees = ref<any>({}) // 临时保存的审批人数据
|
||||
|
||||
const formData = ref<Partial<Leave>>({
|
||||
type: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
reason: undefined,
|
||||
})
|
||||
const formRules = {
|
||||
type: [{ required: true, message: '请选择请假类型' }],
|
||||
startTime: [{ required: true, message: '请选择开始时间' }],
|
||||
endTime: [{ required: true, message: '请选择结束时间' }],
|
||||
reason: [{ required: true, message: '请输入请假原因' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 计算请假天数
|
||||
const leaveDays = computed(() => {
|
||||
if (!formData.value.startTime || !formData.value.endTime) {
|
||||
return 0
|
||||
}
|
||||
const start = new Date(formData.value.startTime)
|
||||
const end = new Date(formData.value.endTime)
|
||||
return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
message.confirm({
|
||||
title: '提示',
|
||||
msg: '确定要返回吗?请先保存您填写的信息!',
|
||||
}).then(({ action }) => {
|
||||
if (action === 'confirm') {
|
||||
navigateBackPlus('/pages-bpm/oa/leave/index')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取流程审批详情 */
|
||||
async function getProcessApprovalDetail() {
|
||||
if (!processDefinitionId.value) {
|
||||
return
|
||||
}
|
||||
|
||||
processTimeLineLoading.value = true
|
||||
try {
|
||||
const data = await getApprovalDetail({
|
||||
processDefinitionId: processDefinitionId.value,
|
||||
activityId: BpmNodeIdEnum.START_USER_NODE_ID,
|
||||
processVariablesStr: JSON.stringify({
|
||||
day: leaveDays.value,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
toast.show('查询不到审批详情信息!')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取审批节点,显示 Timeline 的数据
|
||||
activityNodes.value = data.activityNodes || []
|
||||
|
||||
// 获取发起人自选的任务
|
||||
startUserSelectTasks.value = data.activityNodes?.filter(
|
||||
(node: ApprovalNodeInfo) =>
|
||||
BpmCandidateStrategyEnum.START_USER_SELECT === node.candidateStrategy,
|
||||
) || []
|
||||
|
||||
// 恢复之前的选择审批人
|
||||
if (startUserSelectTasks.value.length > 0) {
|
||||
for (const node of startUserSelectTasks.value) {
|
||||
startUserSelectAssignees.value[node.id]
|
||||
= tempStartUserSelectAssignees.value[node.id]
|
||||
&& tempStartUserSelectAssignees.value[node.id].length > 0
|
||||
? tempStartUserSelectAssignees.value[node.id]
|
||||
: []
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取流程审批详情失败:', error)
|
||||
} finally {
|
||||
processTimeLineLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择审批人确认 */
|
||||
function selectUserConfirm(id: string, userList: any[]) {
|
||||
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id) || []
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
if (formData.value.startTime! >= formData.value.endTime!) {
|
||||
toast.show('结束时间必须大于开始时间')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验指定审批人
|
||||
if (startUserSelectTasks.value.length > 0) {
|
||||
for (const userTask of startUserSelectTasks.value) {
|
||||
if (
|
||||
Array.isArray(startUserSelectAssignees.value[userTask.id])
|
||||
&& startUserSelectAssignees.value[userTask.id].length === 0
|
||||
) {
|
||||
toast.show(`请选择${userTask.name}的审批人`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
const submitData = { ...formData.value }
|
||||
// 设置指定审批人
|
||||
if (startUserSelectTasks.value.length > 0) {
|
||||
submitData.startUserSelectAssignees = startUserSelectAssignees.value
|
||||
}
|
||||
|
||||
await createLeave(submitData)
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
navigateBackPlus('/pages-bpm/oa/leave/index')
|
||||
}, 1500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听表单数据变化,重新预测流程节点
|
||||
watch(
|
||||
() => [formData.value.startTime, formData.value.endTime, formData.value.type],
|
||||
(newValue, oldValue) => {
|
||||
if (!oldValue || !oldValue.some(v => v !== undefined)) {
|
||||
return
|
||||
}
|
||||
if (newValue && newValue.some(v => v !== undefined)) {
|
||||
// 记录之前的节点审批人
|
||||
tempStartUserSelectAssignees.value = { ...startUserSelectAssignees.value }
|
||||
startUserSelectAssignees.value = {}
|
||||
// 加载最新的审批详情,主要用于节点预测
|
||||
getProcessApprovalDetail()
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 组件初始化
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取流程定义详情
|
||||
const processDefinitionDetail = await getProcessDefinition(undefined, processDefineKey)
|
||||
if (!processDefinitionDetail) {
|
||||
toast.show('OA 请假的流程模型未配置,请检查!')
|
||||
return
|
||||
}
|
||||
processDefinitionId.value = processDefinitionDetail.id
|
||||
|
||||
// 获取流程审批详情
|
||||
await getProcessApprovalDetail()
|
||||
} catch (error) {
|
||||
console.error('初始化流程失败:', error)
|
||||
toast.show('初始化流程失败,请稍后重试')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
76
src/pages-bpm/oa/leave/detail/index.vue
Normal file
76
src/pages-bpm/oa/leave/detail/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view :class="embedded ? '' : 'yd-page-container'">
|
||||
<!-- 顶部导航栏(仅路由访问时显示) -->
|
||||
<wd-navbar
|
||||
v-if="!embedded"
|
||||
title="请假详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group :border="!embedded">
|
||||
<wd-cell title="请假类型">
|
||||
<dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="formData.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="开始时间" :value="formatDateTime(formData.startTime) || '-'" />
|
||||
<wd-cell title="结束时间" :value="formatDateTime(formData.endTime) || '-'" />
|
||||
<wd-cell title="请假原因" :value="formData.reason || '-'" />
|
||||
<wd-cell title="审批状态">
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="formData.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Leave } from '@/api/bpm/oa/leave'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getLeave } from '@/api/bpm/oa/leave'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | string
|
||||
embedded?: boolean // 是否作为嵌入组件使用(非路由访问)
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Partial<Leave>>({})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/oa/leave/index')
|
||||
}
|
||||
|
||||
/** 获取详情数据 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getLeave(Number(props.id))
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
218
src/pages-bpm/oa/leave/index.vue
Normal file
218
src/pages-bpm/oa/leave/index.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="请假列表"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<LeaveSearchForm @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<view class="bpm-list">
|
||||
<!-- 请假列表 -->
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="bpm-card"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="bpm-card-content">
|
||||
<view class="bpm-card-header">
|
||||
<view class="bpm-card-title">
|
||||
<dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="bpm-summary">
|
||||
<view class="bpm-summary-item">
|
||||
<text class="text-[#999]">开始时间:</text>
|
||||
<text>{{ formatDateTime(item.startTime) }}</text>
|
||||
</view>
|
||||
<view class="bpm-summary-item">
|
||||
<text class="text-[#999]">结束时间:</text>
|
||||
<text>{{ formatDateTime(item.endTime) }}</text>
|
||||
</view>
|
||||
<view class="bpm-summary-item">
|
||||
<text class="text-[#999]">请假原因:</text>
|
||||
<text>{{ item.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bpm-card-info">
|
||||
<view class="bpm-user">
|
||||
<view class="bpm-avatar">
|
||||
{{ userNickname?.[0] }}
|
||||
</view>
|
||||
<text class="bpm-nickname">{{ userNickname }}</text>
|
||||
</view>
|
||||
<text class="bpm-time">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bpm-actions">
|
||||
<view class="bpm-action-btn" @click.stop="handleDetail(item)">
|
||||
<wd-icon name="view" size="32rpx" />
|
||||
<text class="ml-8rpx">详情</text>
|
||||
</view>
|
||||
<view class="bpm-action-btn" @click.stop="handleProgress(item)">
|
||||
<wd-icon name="queue" size="32rpx" />
|
||||
<text class="ml-8rpx">审批进度</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="item.status === BpmProcessInstanceStatus.RUNNING"
|
||||
class="bpm-action-btn text-[#ff4d4f]!"
|
||||
@click.stop="handleCancel(item)"
|
||||
>
|
||||
<wd-icon name="close" size="32rpx" color="#ff4d4f" />
|
||||
<text class="ml-8rpx">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="bpm-empty">
|
||||
<wd-status-tip image="content" tip="暂无请假记录" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleCreate"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Leave } from '@/api/bpm/oa/leave'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getLeavePage } from '@/api/bpm/oa/leave'
|
||||
import { cancelProcessInstanceByStartUser } from '@/api/bpm/processInstance'
|
||||
import { useUserStore } from '@/store'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmProcessInstanceStatus, DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import LeaveSearchForm from './components/search-form.vue'
|
||||
import '@/pages/bpm/styles/index.scss'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userNickname = computed(() => userStore.userInfo?.nickname || '')
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<Leave[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getLeavePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Leave) {
|
||||
uni.navigateTo({ url: `/pages-bpm/oa/leave/detail/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
/** 审批进度 */
|
||||
function handleProgress(item: Leave) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${item.processInstanceId}` })
|
||||
}
|
||||
|
||||
/** 取消请假 */
|
||||
function handleCancel(item: Leave) {
|
||||
uni.showModal({
|
||||
title: '取消流程',
|
||||
editable: true,
|
||||
placeholderText: '请输入取消原因',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
const reason = res.content?.trim()
|
||||
if (!reason) {
|
||||
uni.showToast({ title: '请输入取消原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await cancelProcessInstanceByStartUser(String(item.processInstanceId), reason)
|
||||
// 更新状态
|
||||
uni.showToast({ title: '取消成功', icon: 'success' })
|
||||
item.status = BpmProcessInstanceStatus.CANCEL
|
||||
item.endTime = new Date()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 发起请假 */
|
||||
function handleCreate() {
|
||||
uni.navigateTo({ url: '/pages-bpm/oa/leave/create/index' })
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
153
src/pages-bpm/process-expression/components/search-form.vue
Normal file
153
src/pages-bpm/process-expression/components/search-form.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
表达式名字
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入表达式名字"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
表达式状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名字:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程表达式'
|
||||
})
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
131
src/pages-bpm/process-expression/detail/index.vue
Normal file
131
src/pages-bpm/process-expression/detail/index.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程表达式详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="表达式名字" :value="formData?.name" />
|
||||
<wd-cell title="表达式状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="表达式">
|
||||
<view class="break-all">
|
||||
{{ formData?.expression }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-expression:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-expression:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteProcessExpression, getProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<ProcessExpression>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-expression/index')
|
||||
}
|
||||
|
||||
/** 加载流程表达式详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getProcessExpression(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑流程表达式 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-expression/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除流程表达式 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该流程表达式吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteProcessExpression(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
140
src/pages-bpm/process-expression/form/index.vue
Normal file
140
src/pages-bpm/process-expression/form/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="表达式名字"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入表达式名字"
|
||||
/>
|
||||
<wd-cell title="表达式状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.expression"
|
||||
label="表达式"
|
||||
label-width="180rpx"
|
||||
prop="expression"
|
||||
clearable
|
||||
placeholder="请输入表达式"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { ProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createProcessExpression, getProcessExpression, updateProcessExpression } from '@/api/bpm/process-expression'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑流程表达式' : '新增流程表达式')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<ProcessExpression>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
expression: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '表达式名字不能为空' }],
|
||||
status: [{ required: true, message: '表达式状态不能为空' }],
|
||||
expression: [{ required: true, message: '表达式不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-expression/index')
|
||||
}
|
||||
|
||||
/** 加载流程表达式详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getProcessExpression(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateProcessExpression(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createProcessExpression(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
161
src/pages-bpm/process-expression/index.vue
Normal file
161
src/pages-bpm/process-expression/index.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程表达式管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程表达式列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between gap-16rpx">
|
||||
<view class="min-w-0 flex-1 truncate text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">表达式:</text>
|
||||
<text class="break-all">{{ item.expression }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程表达式数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:process-expression:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessExpression } from '@/api/bpm/process-expression'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getProcessExpressionPage } from '@/api/bpm/process-expression'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<ProcessExpression[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程表达式列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getProcessExpressionPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增流程表达式 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/process-expression/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: ProcessExpression) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-expression/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
94
src/pages-bpm/process-listener/components/search-form.vue
Normal file
94
src/pages-bpm/process-listener/components/search-form.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
监听器名字
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入监听器名字"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
监听器类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio value="">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getStrDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
type: '', // 空字符串表示全部
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名字:${formData.name}`)
|
||||
}
|
||||
if (formData.type) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE, formData.type)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程监听器'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
type: formData.type || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.type = ''
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
138
src/pages-bpm/process-listener/detail/index.vue
Normal file
138
src/pages-bpm/process-listener/detail/index.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程监听器详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="监听器名字" :value="formData?.name" />
|
||||
<wd-cell title="监听器类型">
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="监听器状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="监听事件" :value="formData?.event" />
|
||||
<wd-cell title="值类型">
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE" :value="formData?.valueType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="值">
|
||||
<view class="break-all">
|
||||
{{ formData?.value }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-listener:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:process-listener:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessListener } from '@/api/bpm/process-listener'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteProcessListener, getProcessListener } from '@/api/bpm/process-listener'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<ProcessListener>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-listener/index')
|
||||
}
|
||||
|
||||
/** 加载流程监听器详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getProcessListener(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑流程监听器 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-listener/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除流程监听器 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该流程监听器吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteProcessListener(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
220
src/pages-bpm/process-listener/form/index.vue
Normal file
220
src/pages-bpm/process-listener/form/index.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="监听器名字"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入监听器名字"
|
||||
/>
|
||||
<wd-cell title="监听器状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="监听器类型" title-width="180rpx" prop="type" center>
|
||||
<wd-radio-group v-model="formData.type" shape="button" @change="handleTypeChange">
|
||||
<wd-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="监听事件" title-width="180rpx" prop="event" center>
|
||||
<wd-radio-group v-model="formData.event" shape="button">
|
||||
<wd-radio
|
||||
v-for="option in eventOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="值类型" title-width="180rpx" prop="valueType" center>
|
||||
<wd-radio-group v-model="formData.valueType" shape="button" @change="handleValueTypeChange">
|
||||
<wd-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.value"
|
||||
:label="valueLabel"
|
||||
label-width="180rpx"
|
||||
prop="value"
|
||||
clearable
|
||||
:placeholder="valuePlaceholder"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { ProcessListener } from '@/api/bpm/process-listener'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createProcessListener, getProcessListener, updateProcessListener } from '@/api/bpm/process-listener'
|
||||
import { getIntDictOptions, getStrDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
/** 执行监听器事件选项 */
|
||||
const EVENT_EXECUTION_OPTIONS = [
|
||||
{ label: '开始', value: 'start' },
|
||||
{ label: '结束', value: 'end' },
|
||||
]
|
||||
|
||||
/** 任务监听器事件选项 */
|
||||
const EVENT_OPTIONS = [
|
||||
{ label: '创建', value: 'create' },
|
||||
{ label: '指派', value: 'assignment' },
|
||||
{ label: '完成', value: 'complete' },
|
||||
{ label: '删除', value: 'delete' },
|
||||
{ label: '更新', value: 'update' },
|
||||
{ label: '超时', value: 'timeout' },
|
||||
]
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑流程监听器' : '新增流程监听器')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<ProcessListener>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
type: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
event: '',
|
||||
valueType: '',
|
||||
value: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '监听器名字不能为空' }],
|
||||
type: [{ required: true, message: '监听器类型不能为空' }],
|
||||
status: [{ required: true, message: '监听器状态不能为空' }],
|
||||
event: [{ required: true, message: '监听事件不能为空' }],
|
||||
valueType: [{ required: true, message: '值类型不能为空' }],
|
||||
value: [{ required: true, message: '值不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 根据类型获取事件选项 */
|
||||
const eventOptions = computed(() => {
|
||||
return formData.value.type === 'execution' ? EVENT_EXECUTION_OPTIONS : EVENT_OPTIONS
|
||||
})
|
||||
|
||||
/** 值标签 */
|
||||
const valueLabel = computed(() => {
|
||||
return formData.value.valueType === 'class' ? '类路径' : '表达式'
|
||||
})
|
||||
|
||||
/** 值占位符 */
|
||||
const valuePlaceholder = computed(() => {
|
||||
return formData.value.valueType === 'class' ? '请输入类路径' : '请输入表达式'
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/process-listener/index')
|
||||
}
|
||||
|
||||
/** 类型变更时清空事件 */
|
||||
function handleTypeChange() {
|
||||
formData.value.event = ''
|
||||
}
|
||||
|
||||
/** 值类型变更时清空值 */
|
||||
function handleValueTypeChange() {
|
||||
formData.value.value = ''
|
||||
}
|
||||
|
||||
/** 加载流程监听器详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getProcessListener(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateProcessListener(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createProcessListener(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
168
src/pages-bpm/process-listener/index.vue
Normal file
168
src/pages-bpm/process-listener/index.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程监听器管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程监听器列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between gap-16rpx">
|
||||
<view class="min-w-0 flex-1 truncate text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">监听器类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">监听事件:</text>
|
||||
<text>{{ item.event }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">值类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE" :value="item.valueType" />
|
||||
</view>
|
||||
<view class="mb-12rpx text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">值:</text>
|
||||
<text class="break-all">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程监听器数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:process-listener:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessListener } from '@/api/bpm/process-listener'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getProcessListenerPage } from '@/api/bpm/process-listener'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<ProcessListener[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程监听器列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getProcessListenerPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增流程监听器 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/process-listener/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: ProcessListener) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/process-listener/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
284
src/pages-bpm/processInstance/create/index.vue
Normal file
284
src/pages-bpm/processInstance/create/index.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<!-- TODO vben 对应的地址:/Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/create/index.vue -->
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="发起申请"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<wd-search
|
||||
v-model="searchName"
|
||||
placeholder="请输入流程名称"
|
||||
placeholder-left
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
|
||||
<!-- 分类标签 -->
|
||||
<wd-tabs
|
||||
v-model="activeCategory"
|
||||
slidable="always"
|
||||
sticky
|
||||
@click="handleTabClick"
|
||||
>
|
||||
<wd-tab v-for="item in categoryList" :key="item.code" :title="item.name" :name="item.code" />
|
||||
</wd-tabs>
|
||||
|
||||
<!-- 流程定义列表 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="h-[calc(100vh-320rpx)]"
|
||||
:scroll-into-view="scrollIntoView"
|
||||
scroll-with-animation
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<view
|
||||
v-for="item in categoryList"
|
||||
:id="`category-${item.code}`"
|
||||
:key="item.code"
|
||||
class="category-section mx-24rpx mt-24rpx"
|
||||
:data-category="item.code"
|
||||
>
|
||||
<!-- 分类标题 -->
|
||||
<view class="mb-16rpx flex items-center">
|
||||
<text class="text-28rpx text-[#333] font-bold">{{ item.name }}</text>
|
||||
</view>
|
||||
<!-- 流程列表 -->
|
||||
<view v-if="groupedDefinitions[item.code]?.length" class="overflow-hidden rounded-16rpx bg-white">
|
||||
<view
|
||||
v-for="definition in groupedDefinitions[item.code]"
|
||||
:key="definition.id"
|
||||
class="flex items-center border-b border-[#f5f5f5] p-24rpx last:border-b-0"
|
||||
@click="handleSelect(definition)"
|
||||
>
|
||||
<wd-img
|
||||
v-if="definition.icon"
|
||||
:src="definition.icon"
|
||||
width="64rpx"
|
||||
height="64rpx"
|
||||
mode="aspectFit"
|
||||
radius="24rpx"
|
||||
class="mr-16rpx"
|
||||
/>
|
||||
<view
|
||||
v-else
|
||||
class="mr-16rpx h-64rpx w-64rpx flex items-center justify-center rounded-12rpx"
|
||||
:style="{ backgroundColor: getIconColor(definition.name) }"
|
||||
>
|
||||
<text class="text-24rpx text-white font-bold">{{ getIconText(definition.name) }}</text>
|
||||
</view>
|
||||
<text class="text-28rpx text-[#333]">{{ definition.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="overflow-hidden rounded-16rpx bg-white p-24rpx text-center">
|
||||
<text class="text-26rpx text-[#999]">该分类下暂无流程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="categoryList.length === 0" class="py-100rpx">
|
||||
<wd-status-tip image="content" tip="暂无可发起的流程" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import type { ProcessDefinition } from '@/api/bpm/definition'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getCategorySimpleList } from '@/api/bpm/category'
|
||||
import { getProcessDefinitionList } from '@/api/bpm/definition'
|
||||
import { getMobileFormCustomPath } from '@/pages-bpm/utils'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmModelFormType } from '@/utils/constants'
|
||||
|
||||
// TODO @芋艿:【重新发起流程】支持通过 processInstanceId 参数重新发起已有流程
|
||||
// 对应 vben 第 44-60 行:从路由获取 processInstanceId,查询流程实例后自动选中对应流程定义并填充表单数据
|
||||
|
||||
// TODO @芋艿:【流程表单填写】选择流程后跳转到表单填写页面
|
||||
// 对应 vben form.vue 全部:包含以下子功能:
|
||||
// - 表单渲染 (form-create):vben form.vue 第 145-152 行
|
||||
// - 审批流程预览时间线:vben form.vue 第 153-162 行
|
||||
// - 流程图预览 (BPMN/简易):vben form.vue 第 163-178 行
|
||||
// - 发起人自选审批人:vben form.vue 第 30-32, 85-95 行
|
||||
// - 表单字段权限控制 (读/写/隐藏):vben form.vue 第 119-131 行
|
||||
// - 业务表单跳转 (formCustomCreatePath):vben form.vue 第 79-85 行
|
||||
// - 表单值变化重新预测审批节点:vben form.vue 第 87-102 行
|
||||
// - 提交流程实例:vben form.vue 第 56-76 行
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const searchName = ref('')
|
||||
const activeCategory = ref('')
|
||||
const categoryList = ref<Category[]>([])
|
||||
const categoryPositions = ref<{ code: string, top: number }[]>([]) // 分类区域位置信息(用于滚动时自动切换 tab)
|
||||
const scrollIntoView = ref('')
|
||||
const isTabClicking = ref(false) // 是否正在通过点击 tab 触发滚动(避免滚动事件反向更新 tab)
|
||||
|
||||
const definitionList = ref<ProcessDefinition[]>([])
|
||||
|
||||
/** 根据流程名称获取图标背景色 */
|
||||
function getIconColor(name: string): string {
|
||||
const iconColors = ['#D98469', '#7BC67C', '#4A7FEB', '#9B7FEB', '#4A9DEB']
|
||||
// 根据名称 hashcode 取模选择颜色
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = (hash * 31 + name.charCodeAt(i)) | 0
|
||||
}
|
||||
return iconColors[Math.abs(hash) % iconColors.length]
|
||||
}
|
||||
|
||||
/** 获取流程名称的前两个字符作为图标文字 */
|
||||
function getIconText(name: string): string {
|
||||
return name?.slice(0, 2) || ''
|
||||
}
|
||||
|
||||
/** 过滤后的流程定义 */
|
||||
const filteredDefinitions = computed(() => {
|
||||
if (!searchName.value.trim()) {
|
||||
return definitionList.value
|
||||
}
|
||||
return definitionList.value.filter(item =>
|
||||
item.name.toLowerCase().includes(searchName.value.toLowerCase()),
|
||||
)
|
||||
})
|
||||
|
||||
/** 按分类分组的流程定义 */
|
||||
const groupedDefinitions = computed<Record<string, ProcessDefinition[]>>(() => {
|
||||
const grouped: Record<string, ProcessDefinition[]> = {}
|
||||
filteredDefinitions.value.forEach((item) => {
|
||||
if (!item.category)
|
||||
return
|
||||
if (!grouped[item.category])
|
||||
grouped[item.category] = []
|
||||
grouped[item.category].push(item)
|
||||
})
|
||||
return grouped
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages/bpm/index')
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
async function handleSearch() {
|
||||
// 搜索后重新计算分类位置
|
||||
await nextTick()
|
||||
updateCategoryPositions()
|
||||
}
|
||||
|
||||
/** Tab 点击 */
|
||||
function handleTabClick({ name }: { index: number, name: string }) {
|
||||
isTabClicking.value = true
|
||||
// 滚动到对应分类
|
||||
scrollIntoView.value = ''
|
||||
nextTick(() => {
|
||||
scrollIntoView.value = `category-${name}`
|
||||
// 300ms 后恢复滚动监听
|
||||
setTimeout(() => {
|
||||
isTabClicking.value = false
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/** 滚动事件 - 自动切换 tab */
|
||||
function handleScroll(e: { detail: { scrollTop: number } }) {
|
||||
if (isTabClicking.value || categoryPositions.value.length === 0) {
|
||||
return
|
||||
}
|
||||
// 找到当前滚动位置对应的分类
|
||||
const scrollTop = e.detail.scrollTop
|
||||
for (let i = categoryPositions.value.length - 1; i >= 0; i--) {
|
||||
if (scrollTop >= categoryPositions.value[i].top - 20) {
|
||||
if (activeCategory.value !== categoryPositions.value[i].code) {
|
||||
activeCategory.value = categoryPositions.value[i].code
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新分类区域位置信息 */
|
||||
function updateCategoryPositions() {
|
||||
const query = uni.createSelectorQuery()
|
||||
query.selectAll('.category-section').boundingClientRect()
|
||||
query.exec((res) => {
|
||||
if (res && res[0]) {
|
||||
const positions: { code: string, top: number }[] = []
|
||||
const firstTop = res[0][0]?.top || 0
|
||||
res[0].forEach((item: { top: number, dataset?: { category?: string } }, index: number) => {
|
||||
const cat = categoryList.value[index]
|
||||
if (cat) {
|
||||
positions.push({
|
||||
code: cat.code,
|
||||
top: item.top - firstTop,
|
||||
})
|
||||
}
|
||||
})
|
||||
categoryPositions.value = positions
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 选择流程定义 */
|
||||
function handleSelect(item: ProcessDefinition) {
|
||||
// 情况一:流程表单,提示仅允许 PC 端发起
|
||||
if (item.formType === BpmModelFormType.NORMAL) {
|
||||
// TODO @jason:业务表单:/Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue
|
||||
toast.show('流程表单仅支持 PC 端发起')
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:业务表单,跳转到对应的移动端页面
|
||||
if (item.formType === BpmModelFormType.CUSTOM) {
|
||||
const mobilePath = getMobileFormCustomPath(item.formCustomCreatePath)
|
||||
if (mobilePath) {
|
||||
uni.navigateTo({ url: mobilePath })
|
||||
} else {
|
||||
toast.show('该业务表单暂不支持移动端发起')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载分类列表 */
|
||||
async function loadCategoryList() {
|
||||
categoryList.value = await getCategorySimpleList()
|
||||
}
|
||||
|
||||
/** 加载流程定义列表 */
|
||||
async function loadDefinitionList() {
|
||||
definitionList.value = await getProcessDefinitionList({ suspensionState: 1 })
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onLoad(async () => {
|
||||
await Promise.all([loadCategoryList(), loadDefinitionList()])
|
||||
// 默认选中第一个分类
|
||||
if (categoryList.value.length > 0) {
|
||||
activeCategory.value = categoryList.value[0].code
|
||||
}
|
||||
// 等待 DOM 渲染后计算分类位置
|
||||
await nextTick()
|
||||
setTimeout(() => {
|
||||
updateCategoryPositions()
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
143
src/pages-bpm/processInstance/detail/add-sign/index.vue
Normal file
143
src/pages-bpm/processInstance/detail/add-sign/index.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="加签任务"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 加签处理人 -->
|
||||
<UserPicker
|
||||
v-model="formData.userIds"
|
||||
prop="userIds"
|
||||
type="checkbox"
|
||||
label="加签处理人:"
|
||||
label-width="200rpx"
|
||||
placeholder="请选择加签处理人"
|
||||
:rules="formRules.userIds"
|
||||
/>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="200rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx flex gap-16rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
class="flex-1"
|
||||
plain
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit('before')"
|
||||
>
|
||||
向前加签
|
||||
</wd-button>
|
||||
<wd-button
|
||||
type="primary"
|
||||
class="flex-1"
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit('after')"
|
||||
>
|
||||
向后加签
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { signCreateTask } from '@/api/bpm/task'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const formData = reactive({
|
||||
userIds: [] as number[],
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
userIds: [
|
||||
{ required: true, message: '加签处理人不能为空', validator: (value: number[]) => value.length > 0 },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit(type: 'before' | 'after') {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
await signCreateTask({
|
||||
id: taskId.value as string,
|
||||
type,
|
||||
userIds: formData.userIds,
|
||||
reason: formData.reason,
|
||||
})
|
||||
const actionText = type === 'before' ? '向前加签' : '向后加签'
|
||||
toast.success(`${actionText}成功`)
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
327
src/pages-bpm/processInstance/detail/audit/index.vue
Normal file
327
src/pages-bpm/processInstance/detail/audit/index.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="isApprove ? '审批同意' : '审批拒绝'"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 审批表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 下一个节点的审批人 -->
|
||||
<view v-if="isApprove && nextAssigneesActivityNode.length > 0" class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center">
|
||||
<text class="mr-8rpx text-[#f56c6c]">*</text>
|
||||
<text class="text-28rpx text-[#333]">下一个节点的审批人</text>
|
||||
</view>
|
||||
<ProcessInstanceTimeline
|
||||
:activity-nodes="nextAssigneesActivityNode"
|
||||
:show-status-icon="false"
|
||||
:enable-approve-user-select="true"
|
||||
@select-user-confirm="selectNextAssigneesConfirm"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 签名 -->
|
||||
<view v-if="isApprove && taskInfo?.signEnable" class="border-b border-[#eee] p-24rpx">
|
||||
<view class="mb-16rpx flex items-center">
|
||||
<text class="mr-8rpx text-[#f56c6c]">*</text>
|
||||
<text class="text-28rpx text-[#333]">签名</text>
|
||||
</view>
|
||||
<view class="flex items-center gap-16rpx">
|
||||
<wd-button type="primary" size="small" @click="openSignatureModal">
|
||||
{{ formData.signPicUrl ? '重新签名' : '点击签名' }}
|
||||
</wd-button>
|
||||
<image
|
||||
v-if="formData.signPicUrl"
|
||||
:src="formData.signPicUrl"
|
||||
class="h-80rpx w-192rpx"
|
||||
mode="aspectFit"
|
||||
@click="previewSignature"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
:type="isApprove ? 'primary' : 'error'"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isApprove ? '同意' : '拒绝' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 签名弹窗 -->
|
||||
<wd-popup v-model="showSignatureModal" position="bottom" custom-style="height: 60vh;">
|
||||
<view class="h-full flex flex-col">
|
||||
<view class="flex items-center justify-between border-b border-[#eee] p-24rpx">
|
||||
<text class="text-32rpx text-[#333] font-bold">手写签名</text>
|
||||
<wd-icon name="close" size="40rpx" @click="showSignatureModal = false" />
|
||||
</view>
|
||||
<view class="flex-1 p-24rpx">
|
||||
<wd-signature
|
||||
:height="300"
|
||||
:export-scale="2"
|
||||
background-color="#ffffff"
|
||||
@confirm="handleSignatureConfirm"
|
||||
@clear="handleSignatureClear"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getApprovalDetail, getNextApproveNodes } from '@/api/bpm/processInstance'
|
||||
import { approveTask, rejectTask } from '@/api/bpm/task'
|
||||
import ProcessInstanceTimeline from '@/pages-bpm/processInstance/detail/components/time-line.vue'
|
||||
import { getEnvBaseUrl, navigateBackPlus } from '@/utils'
|
||||
import { BpmCandidateStrategyEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId?: string
|
||||
taskId?: string
|
||||
pass?: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
const taskId = computed(() => props.taskId || '')
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const isApprove = computed(() => props.pass !== 'false') // true: 同意, false: 拒绝
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const taskInfo = ref<Task | null>(null) // 任务信息
|
||||
|
||||
const nextAssigneesActivityNode = ref<ApprovalNodeInfo[]>([]) // 下一个节点审批人列表
|
||||
const approveUserSelectTasks = ref<ApprovalNodeInfo[]>([]) // 需要选择审批人的节点列表
|
||||
const approveUserSelectAssignees = ref<Record<string, number[]>>({}) // 审批人选择的审批人数据
|
||||
|
||||
// 签名相关
|
||||
const showSignatureModal = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
reason: '',
|
||||
signPicUrl: '', // 签名图片 URL
|
||||
})
|
||||
|
||||
const formRules = computed(() => {
|
||||
let rules = {}
|
||||
if (taskInfo.value?.reasonRequire) {
|
||||
rules = {
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 加载任务信息 */
|
||||
async function loadTaskInfo() {
|
||||
const data = await getApprovalDetail({
|
||||
processInstanceId: processInstanceId.value,
|
||||
taskId: taskId.value,
|
||||
})
|
||||
taskInfo.value = data?.todoTask || null
|
||||
}
|
||||
|
||||
/** 加载下一个节点审批人 */
|
||||
async function loadNextApproveNodes() {
|
||||
if (!isApprove.value) {
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
processInstanceId: processInstanceId.value,
|
||||
taskId: taskId.value,
|
||||
}
|
||||
const data = await getNextApproveNodes(params)
|
||||
if (data && data.length > 0) {
|
||||
nextAssigneesActivityNode.value = data
|
||||
// 获取审批人自选的任务
|
||||
approveUserSelectTasks.value = data.filter(
|
||||
(node: ApprovalNodeInfo) =>
|
||||
BpmCandidateStrategyEnum.APPROVE_USER_SELECT === node.candidateStrategy,
|
||||
) || []
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择下一个节点审批人确认 */
|
||||
function selectNextAssigneesConfirm(activityId: string, userList: any[]) {
|
||||
approveUserSelectAssignees.value[activityId] = userList.map(user => user.id)
|
||||
}
|
||||
|
||||
/** 打开签名弹窗 */
|
||||
function openSignatureModal() {
|
||||
showSignatureModal.value = true
|
||||
}
|
||||
|
||||
/** 签名确认 */
|
||||
async function handleSignatureConfirm(result: { tempFilePath: string, base64: string }) {
|
||||
toast.loading('上传中...')
|
||||
try {
|
||||
// 上传签名图片
|
||||
const url = await uploadSignatureFile(result.tempFilePath)
|
||||
formData.signPicUrl = url
|
||||
showSignatureModal.value = false
|
||||
toast.success('签名成功')
|
||||
} catch (err) {
|
||||
console.error('上传失败:', err)
|
||||
toast.show('上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传签名文件 */
|
||||
function uploadSignatureFile(tempFilePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${getEnvBaseUrl()}/infra/file/upload`,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
success: (uploadFileRes) => {
|
||||
try {
|
||||
const data = JSON.parse(uploadFileRes.data)
|
||||
if (data.code === 0 && data.data) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
reject(new Error(data.msg || '上传失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('上传失败:', err)
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 签名清除 */
|
||||
function handleSignatureClear() {
|
||||
formData.signPicUrl = ''
|
||||
}
|
||||
|
||||
/** 预览签名 */
|
||||
function previewSignature() {
|
||||
if (formData.signPicUrl) {
|
||||
uni.previewImage({
|
||||
urls: [formData.signPicUrl],
|
||||
current: formData.signPicUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交审批 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
// 验证签名
|
||||
if (isApprove.value && taskInfo.value?.signEnable && !formData.signPicUrl) {
|
||||
toast.show('请先进行签名')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证审批人选择
|
||||
if (isApprove.value && approveUserSelectTasks.value.length > 0) {
|
||||
for (const task of approveUserSelectTasks.value) {
|
||||
if (!approveUserSelectAssignees.value[task.id] || approveUserSelectAssignees.value[task.id].length === 0) {
|
||||
toast.show(`请选择「${task.name}」的审批人`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (isApprove.value) {
|
||||
// 审批通过
|
||||
await approveTask({
|
||||
id: taskId.value as string,
|
||||
reason: formData.reason,
|
||||
signPicUrl: formData.signPicUrl || undefined,
|
||||
nextAssignees: Object.keys(approveUserSelectAssignees.value).length > 0
|
||||
? approveUserSelectAssignees.value
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
// 审批拒绝
|
||||
await rejectTask({
|
||||
id: taskId.value as string,
|
||||
reason: formData.reason,
|
||||
})
|
||||
}
|
||||
toast.success('审批成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 1000)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(async () => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
// 加载任务信息和下一个节点审批人
|
||||
await loadTaskInfo()
|
||||
await loadNextApproveNodes()
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- 表单详情:流程表单/业务表单 -->
|
||||
<template>
|
||||
<view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
|
||||
<!-- 标题 -->
|
||||
<view class="px-24rpx pt-24rpx text-28rpx text-[#333] font-bold">
|
||||
审批详情
|
||||
</view>
|
||||
<!-- 表单内容:业务表单 -->
|
||||
<template v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
|
||||
<!-- OA 请假详情 -->
|
||||
<LeaveDetail
|
||||
v-if="processDefinition?.formCustomViewPath === '/bpm/oa/leave/detail'"
|
||||
:id="processInstance?.businessKey"
|
||||
embedded
|
||||
/>
|
||||
<!-- 未配置的业务表单 -->
|
||||
<view v-else class="px-24rpx py-32rpx text-26rpx text-[#999]">
|
||||
暂不支持该业务表单,请参考 LeaveDetail 配置
|
||||
</view>
|
||||
</template>
|
||||
<!-- TODO @jason:表单内容:流程表单 -->
|
||||
<template v-else-if="processDefinition?.formType === BpmModelFormType.NORMAL">
|
||||
<view class="px-24rpx py-32rpx text-26rpx text-[#999]">
|
||||
流程表单仅 PC 端支持预览
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessDefinition, ProcessInstance } from '@/api/bpm/processInstance'
|
||||
// 特殊:业务表单组件(uniapp 小程序不支持动态组件,需要静态导入)
|
||||
import LeaveDetail from '@/pages-bpm/oa/leave/detail/index.vue'
|
||||
import { BpmModelFormType } from '@/utils/constants'
|
||||
|
||||
defineProps<{
|
||||
/** 流程定义 */
|
||||
processDefinition?: ProcessDefinition
|
||||
/** 流程实例 */
|
||||
processInstance?: ProcessInstance
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,311 @@
|
||||
<!-- 操作按钮 -->
|
||||
<template>
|
||||
<!-- 有待审批的任务 -->
|
||||
<view v-if="runningTask" class="yd-detail-footer">
|
||||
<view class="w-full flex items-center">
|
||||
<!-- 左侧操作按钮 -->
|
||||
<view v-for="(action, idx) in leftOperations" :key="idx" class="mr-32rpx w-60rpx flex flex-col items-center" @click="handleOperation(action.operationType)">
|
||||
<wd-icon :name="action.iconName" size="40rpx" color="#1890ff" />
|
||||
<text class="mt-4rpx text-22rpx text-[#333]">{{ action.displayName }}</text>
|
||||
</view>
|
||||
<!-- 更多操作按钮 -->
|
||||
<view v-if="moreOperations.length > 0" class="mr-32rpx w-60rpx flex flex-col items-center" @click="handleShowMore">
|
||||
<wd-icon name="ellipsis" size="40rpx" color="#1890ff" />
|
||||
<text class="mt-4rpx text-22rpx text-[#333]">更多</text>
|
||||
</view>
|
||||
<!-- 更多操作 ActionSheet -->
|
||||
<wd-action-sheet v-if="moreOperations.length > 0" v-model="showMoreActions" :actions="moreOperations" title="请选择操作" @select="handleMoreAction" />
|
||||
|
||||
<!-- 右侧按钮,TODO @jason:是否一定要保留两个按钮(需要的哈) -->
|
||||
<view class="flex flex-1 gap-16rpx">
|
||||
<wd-button
|
||||
v-for="(action, idx) in rightOperations"
|
||||
:key="idx"
|
||||
:plain="action.plain"
|
||||
:type="action.btnType"
|
||||
:round="false"
|
||||
class="flex-1"
|
||||
custom-style="min-width: 200rpx; width: 200rpx;"
|
||||
@click="handleOperation(action.operationType)"
|
||||
>
|
||||
{{ action.displayName }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 无待审批的任务 仅显示取消按钮。TODO @jason:看看还需要显示(这个微信交流下) -->
|
||||
<view v-if="!runningTask && isShowProcessStartCancel()" class="yd-detail-footer">
|
||||
<wd-button
|
||||
plain
|
||||
type="primary"
|
||||
:round="false"
|
||||
block
|
||||
@click="handleOperation(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL)"
|
||||
>
|
||||
取消
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Action } from 'wot-design-uni/components/wd-action-sheet/types'
|
||||
import type { ButtonType } from 'wot-design-uni/components/wd-button/types'
|
||||
import type { ProcessInstance } from '@/api/bpm/processInstance'
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import { useUserStore } from '@/store'
|
||||
import {
|
||||
BpmProcessInstanceStatus,
|
||||
BpmTaskOperationButtonTypeEnum,
|
||||
BpmTaskStatusEnum,
|
||||
OPERATION_BUTTON_NAME,
|
||||
} from '@/utils/constants'
|
||||
|
||||
const showMoreActions = ref(false)
|
||||
|
||||
type MoreOperationType = Action & {
|
||||
operationType: number
|
||||
}
|
||||
|
||||
interface LeftOperationType {
|
||||
operationType: number
|
||||
iconName: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface RightOperationType {
|
||||
operationType: number
|
||||
btnType: ButtonType
|
||||
displayName: string
|
||||
plain: boolean
|
||||
}
|
||||
const operationIconsMap: Record<number, string> = {
|
||||
[BpmTaskOperationButtonTypeEnum.TRANSFER]: 'transfer',
|
||||
[BpmTaskOperationButtonTypeEnum.ADD_SIGN]: 'add',
|
||||
[BpmTaskOperationButtonTypeEnum.DELEGATE]: 'share',
|
||||
[BpmTaskOperationButtonTypeEnum.RETURN]: 'arrow-left',
|
||||
[BpmTaskOperationButtonTypeEnum.COPY]: 'copy',
|
||||
[BpmTaskOperationButtonTypeEnum.DELETE_SIGN]: 'remove',
|
||||
[BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL]: 'stop-circle',
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const leftOperations = ref<LeftOperationType[]>([]) // 左侧操作按钮 【最多两个】{转办, 委派, 退回, 加签, 抄送等}
|
||||
const rightOperationTypes = [] // 右侧操作按钮【最多两个】{通过,拒绝, 取消}
|
||||
const rightOperations = ref<RightOperationType[]>([])
|
||||
const moreOperations = ref<MoreOperationType[]>([]) // 更多操作
|
||||
const runningTask = ref<Task>()
|
||||
const processInstance = ref<ProcessInstance>()
|
||||
const reasonRequire = ref<boolean>(false)
|
||||
|
||||
/** 初始化 */
|
||||
function init(theProcessInstance: ProcessInstance, task: Task) {
|
||||
processInstance.value = theProcessInstance
|
||||
runningTask.value = task
|
||||
if (task) {
|
||||
reasonRequire.value = task.reasonRequire ?? false
|
||||
// TODO @jason:这里的判断,是否可以简化哈?就是默认计算出按钮,然后根据数量,去渲染具体的按钮。
|
||||
// 右侧按钮
|
||||
if (isHandleTaskStatus() && isShowButton(BpmTaskOperationButtonTypeEnum.REJECT)) {
|
||||
rightOperationTypes.push(BpmTaskOperationButtonTypeEnum.REJECT)
|
||||
rightOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.REJECT,
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.REJECT),
|
||||
btnType: 'error',
|
||||
plain: true,
|
||||
})
|
||||
}
|
||||
if (isHandleTaskStatus() && isShowButton(BpmTaskOperationButtonTypeEnum.APPROVE)) {
|
||||
rightOperationTypes.push(BpmTaskOperationButtonTypeEnum.APPROVE)
|
||||
rightOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.APPROVE,
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.APPROVE),
|
||||
btnType: 'primary',
|
||||
plain: false,
|
||||
})
|
||||
}
|
||||
|
||||
// 左侧操作,和更多操作
|
||||
Object.keys(task.buttonsSetting || {}).forEach((key) => {
|
||||
const operationType = Number(key)
|
||||
if (task.buttonsSetting[key].enable && isHandleTaskStatus()
|
||||
&& !rightOperationTypes.includes(operationType)) {
|
||||
if (leftOperations.value.length >= 2) {
|
||||
moreOperations.value.push({
|
||||
name: getButtonDisplayName(operationType),
|
||||
operationType,
|
||||
})
|
||||
} else {
|
||||
leftOperations.value.push({
|
||||
operationType,
|
||||
iconName: operationIconsMap[operationType],
|
||||
displayName: getButtonDisplayName(operationType),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 减签操作的显示
|
||||
if (isShowDeleteSign()) {
|
||||
if (leftOperations.value.length >= 2) {
|
||||
moreOperations.value.push({
|
||||
name: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.DELETE_SIGN),
|
||||
operationType: BpmTaskOperationButtonTypeEnum.DELETE_SIGN,
|
||||
})
|
||||
} else {
|
||||
leftOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.DELETE_SIGN,
|
||||
iconName: operationIconsMap[BpmTaskOperationButtonTypeEnum.DELETE_SIGN],
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.DELETE_SIGN),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 是否显示流程取消
|
||||
if (isShowProcessStartCancel()) {
|
||||
if (rightOperationTypes.length < 2) {
|
||||
rightOperationTypes.push(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL)
|
||||
rightOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
|
||||
btnType: 'primary',
|
||||
plain: true,
|
||||
})
|
||||
} else {
|
||||
if (leftOperations.value.length >= 2) {
|
||||
moreOperations.value.push({
|
||||
name: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
|
||||
operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
|
||||
})
|
||||
} else {
|
||||
leftOperations.value.push({
|
||||
operationType: BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL,
|
||||
iconName: operationIconsMap[BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL],
|
||||
displayName: getButtonDisplayName(BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到相应的操作页面 */
|
||||
function handleOperation(operationType: number) {
|
||||
switch (operationType) {
|
||||
case BpmTaskOperationButtonTypeEnum.APPROVE:
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/audit/index?processInstanceId=${processInstance.value.id}&taskId=${runningTask.value?.id}&pass=true` })
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.REJECT:
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/audit/index?processInstanceId=${processInstance.value.id}&taskId=${runningTask.value?.id}&pass=false` })
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.DELEGATE:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/reassign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}&type=delegate`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.TRANSFER:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/reassign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}&type=transfer`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.ADD_SIGN:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/add-sign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.RETURN:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/return/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.DELETE_SIGN:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/delete-sign/index?processInstanceId=${runningTask.value.processInstanceId}&taskId=${runningTask.value.id}&children=${encodeURIComponent(JSON.stringify(runningTask.value.children || []))}`,
|
||||
})
|
||||
break
|
||||
case BpmTaskOperationButtonTypeEnum.PROCESS_START_CANCEL:
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/process-cancel/index?processInstanceId=${processInstance.value.id}&taskId=${runningTask.value?.id}`,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示更多操作 */
|
||||
function handleShowMore() {
|
||||
showMoreActions.value = true
|
||||
}
|
||||
|
||||
/** 处理更多操作选择 */
|
||||
function handleMoreAction(action: { item: MoreOperationType }) {
|
||||
handleOperation(action.item.operationType)
|
||||
showMoreActions.value = false
|
||||
}
|
||||
|
||||
/** 获取按钮的显示名称 */
|
||||
function getButtonDisplayName(btnType: BpmTaskOperationButtonTypeEnum) {
|
||||
let displayName = OPERATION_BUTTON_NAME.get(btnType)
|
||||
if (
|
||||
runningTask.value?.buttonsSetting
|
||||
&& runningTask.value?.buttonsSetting[btnType]
|
||||
) {
|
||||
displayName = runningTask.value.buttonsSetting[btnType].displayName
|
||||
}
|
||||
return displayName
|
||||
}
|
||||
|
||||
/** 是否显示按钮 */
|
||||
function isShowButton(btnType: BpmTaskOperationButtonTypeEnum): boolean {
|
||||
let isShow = true
|
||||
if (
|
||||
runningTask.value?.buttonsSetting
|
||||
&& runningTask.value?.buttonsSetting[btnType]
|
||||
) {
|
||||
isShow = runningTask.value.buttonsSetting[btnType].enable
|
||||
}
|
||||
return isShow
|
||||
}
|
||||
|
||||
/** 任务是否为处理中状态 */
|
||||
function isHandleTaskStatus() {
|
||||
let canHandle = false
|
||||
if (BpmTaskStatusEnum.RUNNING === runningTask.value?.status) {
|
||||
canHandle = true
|
||||
}
|
||||
return canHandle
|
||||
}
|
||||
|
||||
/** 流程状态是否为结束状态 */
|
||||
function isEndProcessStatus(status: number) {
|
||||
let isEndStatus = false
|
||||
if (
|
||||
BpmProcessInstanceStatus.APPROVE === status
|
||||
|| BpmProcessInstanceStatus.REJECT === status
|
||||
|| BpmProcessInstanceStatus.CANCEL === status
|
||||
) {
|
||||
isEndStatus = true
|
||||
}
|
||||
return isEndStatus
|
||||
}
|
||||
|
||||
/** 流程发起人是否为当前用户 */
|
||||
function isProcessStartUser() {
|
||||
let isStartUser = false
|
||||
if (userStore.userInfo?.userId === processInstance.value?.startUser?.id) {
|
||||
isStartUser = true
|
||||
}
|
||||
return isStartUser
|
||||
}
|
||||
|
||||
/** 是否显示减签 */
|
||||
function isShowDeleteSign() {
|
||||
return runningTask.value?.children?.length > 0
|
||||
}
|
||||
|
||||
/** 是否显示流程发起人取消 */
|
||||
function isShowProcessStartCancel() {
|
||||
return isProcessStartUser() && !isEndProcessStatus(processInstance.value?.status)
|
||||
}
|
||||
|
||||
/** 暴露方法 */
|
||||
defineExpose({ init })
|
||||
</script>
|
||||
394
src/pages-bpm/processInstance/detail/components/time-line.vue
Normal file
394
src/pages-bpm/processInstance/detail/components/time-line.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<!-- 遍历每个审批节点 -->
|
||||
<view
|
||||
v-for="(activity, index) in activityNodes"
|
||||
:key="activity.id || index"
|
||||
class="relative pb-24rpx pl-80rpx"
|
||||
>
|
||||
<!-- 时间线圆点 -->
|
||||
<view
|
||||
class="absolute left-12rpx top-8rpx h-52rpx w-52rpx flex items-center justify-center rounded-full bg-blue-500"
|
||||
>
|
||||
<!-- 节点类型图标 -->
|
||||
<wd-icon
|
||||
:name="getApprovalNodeTypeIcon(activity.nodeType)"
|
||||
size="32rpx"
|
||||
color="white"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 状态小图标 -->
|
||||
<view
|
||||
v-if="showStatusIcon"
|
||||
class="absolute left-48rpx top-44rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
|
||||
:style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
|
||||
>
|
||||
<wd-icon
|
||||
:name="getApprovalNodeIcon(activity.status, activity.nodeType)"
|
||||
size="12rpx"
|
||||
color="white"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 连接线 -->
|
||||
<view
|
||||
v-if="index < activityNodes.length - 1"
|
||||
class="absolute bottom-0 left-38rpx top-64rpx w-2rpx bg-[#e5e5e5]"
|
||||
/>
|
||||
|
||||
<!-- 节点内容 -->
|
||||
<view class="ml-8rpx">
|
||||
<!-- 第一行:节点名称、时间 -->
|
||||
<view class="mb-8rpx flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<text class="text-28rpx text-[#333] font-bold">{{ activity.name }}</text>
|
||||
<text v-if="activity.status === BpmTaskStatusEnum.SKIP" class="ml-8rpx text-24rpx text-[#999]">
|
||||
【跳过】
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
v-if="activity.status !== BpmTaskStatusEnum.NOT_START && getApprovalNodeTime(activity)"
|
||||
class="text-22rpx text-[#999]"
|
||||
>
|
||||
{{ getApprovalNodeTime(activity) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 子流程节点 -->
|
||||
<view v-if="activity.nodeType === BpmNodeTypeEnum.CHILD_PROCESS_NODE" class="mb-16rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
:disabled="!activity.processInstanceId"
|
||||
@click="handleChildProcess(activity)"
|
||||
>
|
||||
查看子流程
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 需要自定义选择审批人 -->
|
||||
<view v-if="shouldShowCustomUserSelect(activity)" class="mb-16rpx">
|
||||
<view class="flex flex-wrap items-center">
|
||||
<!-- 添加用户按钮 -->
|
||||
<UserPicker
|
||||
:model-value="getSelectedUserIds(activity.id)"
|
||||
type="checkbox"
|
||||
use-default-slot
|
||||
@confirm="(users) => handleCustomUserSelectConfirm(activity.id, users)"
|
||||
>
|
||||
<view
|
||||
class="mb-8rpx mr-16rpx h-48rpx w-48rpx flex items-center justify-center border-indigo-500 rounded-lg border-solid"
|
||||
>
|
||||
<wd-icon name="user-add" size="32rpx" color="blue" />
|
||||
</view>
|
||||
</UserPicker>
|
||||
<!-- 已选择的用户 -->
|
||||
<view
|
||||
v-for="(user, userIndex) in customApproveUsers[activity.id]"
|
||||
:key="user.id || userIndex"
|
||||
class="mb-8rpx mr-16rpx flex items-center rounded-32rpx bg-[#f5f5f5] pr-16rpx"
|
||||
>
|
||||
<view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
|
||||
{{ user.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<text class="text-24rpx text-[#333]">{{ user.nickname }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批人员列表 -->
|
||||
<view v-else class="mb-16rpx">
|
||||
<!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
|
||||
<view v-if="activity.tasks && activity.tasks.length > 0">
|
||||
<view
|
||||
v-for="(task, taskIndex) in activity.tasks"
|
||||
:key="taskIndex"
|
||||
class="mb-16rpx"
|
||||
>
|
||||
<!-- 审批人信息 -->
|
||||
<view v-if="task.assigneeUser || task.ownerUser" class="mb-8rpx flex items-center">
|
||||
<!-- TODO @jason 用户头像显示 -->
|
||||
<view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
|
||||
{{ (task.assigneeUser?.nickname || task.ownerUser?.nickname)?.[0] || '?' }}
|
||||
|
||||
<!-- 任务状态小图标 -->
|
||||
<view
|
||||
v-if="showStatusIcon && shouldShowTaskStatusIcon(task.status)"
|
||||
class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
|
||||
:style="{ backgroundColor: getApprovalNodeColor(task.status) }"
|
||||
>
|
||||
<wd-icon
|
||||
:name="getApprovalNodeIcon(task.status, activity.nodeType)"
|
||||
size="12rpx"
|
||||
color="white"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex-1">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<text class="text-26rpx text-[#333]">
|
||||
{{ task.assigneeUser?.nickname || task.ownerUser?.nickname }}
|
||||
</text>
|
||||
<text
|
||||
v-if="task.assigneeUser?.deptName || task.ownerUser?.deptName"
|
||||
class="ml-8rpx text-22rpx text-[#999]"
|
||||
>
|
||||
{{ task.assigneeUser?.deptName || task.ownerUser?.deptName }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-4rpx flex items-center">
|
||||
<text :class="getStatusTextClass(task.status)" class="text-24rpx">
|
||||
{{ getStatusText(task.status) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<view
|
||||
v-if="shouldShowApprovalReason(task, activity.nodeType)"
|
||||
class="mt-8rpx rounded-8rpx bg-[#f5f5f5] p-16rpx"
|
||||
>
|
||||
<text class="text-24rpx text-[#666]">审批意见:{{ task.reason }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 签名 -->
|
||||
<view
|
||||
v-if="task.signPicUrl && activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE"
|
||||
class="mt-8rpx flex items-center rounded-8rpx bg-[#f5f5f5] p-16rpx"
|
||||
>
|
||||
<text class="text-24rpx text-[#666]">签名:</text>
|
||||
<image
|
||||
:src="task.signPicUrl"
|
||||
class="ml-8rpx h-96rpx w-288rpx"
|
||||
mode="aspectFit"
|
||||
@click="previewImage(task.signPicUrl)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||
<view v-if="activity.candidateUsers && activity.candidateUsers.length > 0">
|
||||
<view
|
||||
v-for="(user, userIndex) in activity.candidateUsers"
|
||||
:key="userIndex"
|
||||
class="mb-8rpx flex items-center"
|
||||
>
|
||||
<view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
|
||||
{{ user.nickname?.[0] || '?' }}
|
||||
|
||||
<!-- 候选状态图标 -->
|
||||
<view
|
||||
v-if="showStatusIcon"
|
||||
class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
|
||||
:style="{ backgroundColor: getApprovalNodeColor(BpmTaskStatusEnum.NOT_START) }"
|
||||
>
|
||||
<wd-icon name="time" size="12rpx" color="white" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex-1">
|
||||
<text class="text-26rpx text-[#333]">{{ user.nickname }}</text>
|
||||
<text v-if="user.deptName" class="ml-8rpx text-22rpx text-[#999]">
|
||||
{{ user.deptName }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
|
||||
import { ref } from 'vue'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { BpmCandidateStrategyEnum, BpmNodeTypeEnum, BpmTaskStatusEnum } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activityNodes: ApprovalNodeInfo[]
|
||||
enableApproveUserSelect?: boolean
|
||||
showStatusIcon?: boolean
|
||||
}>(),
|
||||
{
|
||||
showStatusIcon: true,
|
||||
enableApproveUserSelect: false,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectUserConfirm: [activityId: string, userList: any[]]
|
||||
}>()
|
||||
|
||||
// 状态图标映射
|
||||
const statusIconMap: Record<string, { color: string, icon: string }> = {
|
||||
'-2': { color: '#909398', icon: 'skip-forward' }, // 跳过
|
||||
'-1': { color: '#909398', icon: 'time' }, // 审批未开始
|
||||
'0': { color: '#f59e0b', icon: 'refresh1' }, // 待审批
|
||||
'1': { color: '#f59e0b', icon: 'refresh1' }, // 审批中
|
||||
'2': { color: '#00b32a', icon: 'check' }, // 审批通过
|
||||
'3': { color: '#f46b6c', icon: 'close' }, // 审批不通过
|
||||
'4': { color: '#cccccc', icon: 'delete' }, // 已取消
|
||||
'5': { color: '#f46b6c', icon: 'arrow-left' }, // 退回
|
||||
'6': { color: '#448ef7', icon: 'time' }, // 委派中
|
||||
'7': { color: '#00b32a', icon: 'check' }, // 审批通过中
|
||||
}
|
||||
|
||||
// 节点类型图标映射 TODO 图标重新选一下
|
||||
const nodeTypeSvgMap: Record<number, { color: string, icon: string }> = {
|
||||
[BpmNodeTypeEnum.END_EVENT_NODE]: { color: '#909398', icon: 'poweroff' },
|
||||
[BpmNodeTypeEnum.START_USER_NODE]: { color: '#909398', icon: 'user' },
|
||||
[BpmNodeTypeEnum.USER_TASK_NODE]: { color: '#ff943e', icon: 'user-talk' },
|
||||
[BpmNodeTypeEnum.TRANSACTOR_NODE]: { color: '#ff943e', icon: 'edit' },
|
||||
[BpmNodeTypeEnum.COPY_TASK_NODE]: { color: '#3296fb', icon: 'copy' },
|
||||
[BpmNodeTypeEnum.CONDITION_NODE]: { color: '#14bb83', icon: 'branch' },
|
||||
[BpmNodeTypeEnum.PARALLEL_BRANCH_NODE]: { color: '#14bb83', icon: 'branch' },
|
||||
[BpmNodeTypeEnum.CHILD_PROCESS_NODE]: { color: '#14bb83', icon: 'cluster' },
|
||||
}
|
||||
|
||||
const onlyStatusIconShow = [BpmTaskStatusEnum.NOT_START, BpmTaskStatusEnum.RUNNING, BpmTaskStatusEnum.WAIT] // 只有状态是 -1、0、1 才展示头像右小角状态小 icon
|
||||
|
||||
// 响应式数据
|
||||
const customApproveUsers = ref<Record<string, any[]>>({})
|
||||
const showUserPicker = ref(false)
|
||||
const selectedUserIds = ref<number[]>([])
|
||||
const selectedActivityNodeId = ref<string>()
|
||||
|
||||
/** 获取审批节点类型图标 */
|
||||
function getApprovalNodeTypeIcon(nodeType: number) {
|
||||
return nodeTypeSvgMap[nodeType]?.icon || 'time'
|
||||
}
|
||||
|
||||
/** 获取审批节点图标 */
|
||||
function getApprovalNodeIcon(taskStatus: number, nodeType: number) {
|
||||
if (taskStatus === BpmTaskStatusEnum.NOT_START) {
|
||||
return statusIconMap[taskStatus]?.icon || 'time'
|
||||
}
|
||||
return statusIconMap[taskStatus]?.icon || 'time'
|
||||
}
|
||||
|
||||
/** 获取审批节点颜色 */
|
||||
function getApprovalNodeColor(taskStatus: number) {
|
||||
return statusIconMap[taskStatus]?.color || '#909398'
|
||||
}
|
||||
|
||||
/** 获取审批节点时间 */
|
||||
function getApprovalNodeTime(node: ApprovalNodeInfo) {
|
||||
if (node.nodeType === BpmNodeTypeEnum.START_USER_NODE && node.startTime) {
|
||||
return formatDateTime(node.startTime)
|
||||
}
|
||||
if (node.endTime) {
|
||||
return formatDateTime(node.endTime)
|
||||
}
|
||||
if (node.startTime) {
|
||||
return formatDateTime(node.startTime)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 是否显示任务状态图标 */
|
||||
function shouldShowTaskStatusIcon(status: number) {
|
||||
return onlyStatusIconShow.includes(status)
|
||||
}
|
||||
|
||||
/** 判断是否需要显示自定义选择审批人 */
|
||||
function shouldShowCustomUserSelect(activity: ApprovalNodeInfo) {
|
||||
return (
|
||||
(!activity.tasks || activity.tasks.length === 0)
|
||||
&& ((BpmCandidateStrategyEnum.START_USER_SELECT === activity.candidateStrategy
|
||||
&& (!activity.candidateUsers || activity.candidateUsers.length === 0))
|
||||
|| (props.enableApproveUserSelect
|
||||
&& BpmCandidateStrategyEnum.APPROVE_USER_SELECT === activity.candidateStrategy))
|
||||
)
|
||||
}
|
||||
|
||||
/** 判断是否需要显示审批意见 */
|
||||
function shouldShowApprovalReason(task: any, nodeType: number) {
|
||||
return (
|
||||
task.reason
|
||||
&& [BpmNodeTypeEnum.END_EVENT_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(nodeType)
|
||||
)
|
||||
}
|
||||
|
||||
/** 获取状态文本样式类 */
|
||||
function getStatusTextClass(status: number) {
|
||||
const colorMap: Record<number, string> = {
|
||||
[BpmTaskStatusEnum.RUNNING]: 'text-[#ff943e]',
|
||||
[BpmTaskStatusEnum.APPROVE]: 'text-[#00b32a]',
|
||||
[BpmTaskStatusEnum.REJECT]: 'text-[#f46b6c]',
|
||||
[BpmTaskStatusEnum.CANCEL]: 'text-[#cccccc]',
|
||||
[BpmTaskStatusEnum.RETURN]: 'text-[#f46b6c]',
|
||||
}
|
||||
return colorMap[status] || 'text-[#666]'
|
||||
}
|
||||
|
||||
/** 获取状态文本 */
|
||||
function getStatusText(status: number) {
|
||||
const textMap: Record<number, string> = {
|
||||
[BpmTaskStatusEnum.NOT_START]: '未开始',
|
||||
[BpmTaskStatusEnum.RUNNING]: '待审批',
|
||||
[BpmTaskStatusEnum.APPROVE]: '已通过',
|
||||
[BpmTaskStatusEnum.REJECT]: '已拒绝',
|
||||
[BpmTaskStatusEnum.CANCEL]: '已取消',
|
||||
[BpmTaskStatusEnum.RETURN]: '已退回',
|
||||
[BpmTaskStatusEnum.SKIP]: '已跳过',
|
||||
}
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
/** 用户选择确认 */
|
||||
function handleCustomUserSelectConfirm(activityId: string, users: any[]) {
|
||||
customApproveUsers.value[activityId] = users || []
|
||||
emit('selectUserConfirm', activityId, users)
|
||||
}
|
||||
|
||||
/** 获取选中的用户ID数组 */
|
||||
function getSelectedUserIds(activityId: string): number[] {
|
||||
const users = customApproveUsers.value[activityId] || []
|
||||
return users.map(user => user.id).filter(id => id !== undefined)
|
||||
}
|
||||
|
||||
/** 跳转子流程 */
|
||||
function handleChildProcess(activity: ApprovalNodeInfo) {
|
||||
if (!activity.processInstanceId) {
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${activity.processInstanceId}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 预览图片 */
|
||||
function previewImage(url: string) {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: url,
|
||||
})
|
||||
}
|
||||
|
||||
/** 设置自定义审批人 */
|
||||
function setCustomApproveUsers(activityId: string, users: any[]) {
|
||||
customApproveUsers.value[activityId] = users || []
|
||||
}
|
||||
|
||||
/** 批量设置多个节点的自定义审批人 */
|
||||
function batchSetCustomApproveUsers(data: Record<string, any[]>) {
|
||||
Object.keys(data).forEach((activityId) => {
|
||||
customApproveUsers.value[activityId] = data[activityId] || []
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
setCustomApproveUsers,
|
||||
batchSetCustomApproveUsers,
|
||||
})
|
||||
</script>
|
||||
165
src/pages-bpm/processInstance/detail/delete-sign/index.vue
Normal file
165
src/pages-bpm/processInstance/detail/delete-sign/index.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="减签任务"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 减签人员选择 -->
|
||||
<wd-picker
|
||||
v-model="formData.deleteSignTaskId"
|
||||
:columns="taskOptions"
|
||||
value-key="id"
|
||||
label-key="label"
|
||||
label="减签人员:"
|
||||
label-width="180rpx"
|
||||
placeholder="请选择减签人员"
|
||||
prop="deleteSignTaskId"
|
||||
/>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
减签
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { signDeleteTask } from '@/api/bpm/task'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
children?: string // JSON 字符串格式的子任务数据
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const taskOptions = ref<any[]>([])
|
||||
const formData = reactive({
|
||||
deleteSignTaskId: '',
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
deleteSignTaskId: [
|
||||
{ required: true, message: '减签人员不能为空' },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 获取减签人员标签 */
|
||||
function getDeleteSignUserLabel(task: any): string {
|
||||
const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName
|
||||
const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname
|
||||
return `${nickname} ( 所属部门:${deptName} )`
|
||||
}
|
||||
|
||||
/** 获取可减签的任务列表 */
|
||||
async function loadDeleteSignTaskList() {
|
||||
let childTasks = []
|
||||
// 从 props 中获取子任务数据
|
||||
if (props.children) {
|
||||
try {
|
||||
childTasks = JSON.parse(decodeURIComponent(props.children))
|
||||
} catch (parseError) {
|
||||
console.error('[delete-sign] 解析子任务数据失败:', parseError)
|
||||
}
|
||||
}
|
||||
// 提示没有子任务数据
|
||||
if (childTasks.length === 0) {
|
||||
toast.show('没有可减签的任务')
|
||||
return
|
||||
}
|
||||
|
||||
taskOptions.value = childTasks.map(task => ({
|
||||
id: task.id,
|
||||
label: getDeleteSignUserLabel(task),
|
||||
}))
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
await signDeleteTask({
|
||||
id: formData.deleteSignTaskId,
|
||||
reason: formData.reason,
|
||||
})
|
||||
toast.success('减签成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时,获取可减签任务列表 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
loadDeleteSignTaskList()
|
||||
})
|
||||
</script>
|
||||
147
src/pages-bpm/processInstance/detail/index.vue
Normal file
147
src/pages-bpm/processInstance/detail/index.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<view class="yd-page-container pb-[80rpx]">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="审批详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 区域:流程信息(基本信息) -->
|
||||
<view class="relative mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
|
||||
<!-- 审批状态图标(盖章效果) -->
|
||||
<image
|
||||
v-if="processInstance?.status !== undefined"
|
||||
:src="getStatusIcon(processInstance?.status)"
|
||||
class="absolute right-20rpx top-20rpx z-10 h-144rpx w-144rpx"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="p-24rpx">
|
||||
<!-- 标题 -->
|
||||
<view class="mb-16rpx pr-160rpx">
|
||||
<text class="text-32rpx text-[#333] font-bold">{{ processInstance?.name }}</text>
|
||||
</view>
|
||||
<!-- 发起人信息 -->
|
||||
<view class="flex items-center">
|
||||
<view class="mr-12rpx h-64rpx w-64rpx flex items-center justify-center rounded-full bg-[#1890ff] text-white">
|
||||
{{ processInstance?.startUser?.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<view>
|
||||
<text class="text-28rpx text-[#333]">{{ processInstance?.startUser?.nickname }}</text>
|
||||
<text v-if="processInstance?.startUser?.deptName" class="ml-8rpx text-24rpx text-[#999]">
|
||||
{{ processInstance?.startUser?.deptName }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 提交时间 -->
|
||||
<view class="mt-16rpx text-24rpx text-[#999]">
|
||||
提交于 {{ formatDateTime(processInstance?.startTime) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 区域:审批详情(表单) -->
|
||||
<FormDetail :process-definition="processDefinition" :process-instance="processInstance" />
|
||||
|
||||
<!-- 区域:审批进度 -->
|
||||
<view class="mx-24rpx mt-24rpx rounded-16rpx bg-white">
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex">
|
||||
<text class="text-28rpx text-[#333] font-bold">审批进度</text>
|
||||
</view>
|
||||
<!-- 流程时间线 -->
|
||||
<ProcessInstanceTimeline :activity-nodes="activityNodes" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- TODO 待开发:区域:流程评论 -->
|
||||
|
||||
<!-- 区域:底部操作栏 -->
|
||||
<ProcessInstanceOperationButton ref="operationButtonRef" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApprovalNodeInfo, ProcessDefinition, ProcessInstance } from '@/api/bpm/processInstance'
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getApprovalDetail } from '@/api/bpm/processInstance'
|
||||
import { getTaskListByProcessInstanceId } from '@/api/bpm/task'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { BpmProcessInstanceStatus } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import FormDetail from './components/form-detail.vue'
|
||||
import ProcessInstanceOperationButton from './components/operation-button.vue'
|
||||
import ProcessInstanceTimeline from './components/time-line.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id: string // 流程实例的编号
|
||||
taskId?: string // 任务编号
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const processInstance = ref<ProcessInstance>()
|
||||
const processDefinition = ref<ProcessDefinition>()
|
||||
const tasks = ref<Task[]>([])
|
||||
|
||||
const activityNodes = ref<ApprovalNodeInfo[]>([]) // 审批节点信息
|
||||
|
||||
const operationButtonRef = ref() // 操作按钮组件 ref
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages/bpm/index')
|
||||
}
|
||||
|
||||
/** 获取状态图标 */
|
||||
function getStatusIcon(status?: number): string {
|
||||
// 状态映射: 1-审批中, 2-审批通过, 3-审批不通过, 4-已取消. -1 未开始不会出现
|
||||
const iconMap: Record<number, string> = {
|
||||
[BpmProcessInstanceStatus.RUNNING]: '/static/my-icons/bpm/bpm-running.svg', // 待审批
|
||||
[BpmProcessInstanceStatus.APPROVE]: '/static/my-icons/bpm/bpm-approve.svg', // 审批通过
|
||||
[BpmProcessInstanceStatus.REJECT]: '/static/my-icons/bpm/bpm-reject.svg', // 审批不通过
|
||||
[BpmProcessInstanceStatus.CANCEL]: '/static/my-icons/bpm/bpm-cancel.svg', // 已取消
|
||||
}
|
||||
return iconMap[status ?? 1]
|
||||
}
|
||||
|
||||
/** 加载流程实例 */
|
||||
async function loadProcessInstance() {
|
||||
const data = await getApprovalDetail({
|
||||
processInstanceId: props.id,
|
||||
taskId: props.taskId,
|
||||
})
|
||||
if (!data || !data.processInstance) {
|
||||
toast.show('查询不到审批详情信息')
|
||||
return
|
||||
}
|
||||
processInstance.value = data.processInstance
|
||||
processDefinition.value = data.processDefinition
|
||||
// 获取审批节点,显示 Timeline 的数据
|
||||
activityNodes.value = data.activityNodes
|
||||
|
||||
operationButtonRef.value?.init(data.processInstance, data.todoTask)
|
||||
}
|
||||
|
||||
/** 加载任务列表 */
|
||||
async function loadTasks() {
|
||||
tasks.value = await getTaskListByProcessInstanceId(props.id)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
if (!props.id) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
await Promise.all([loadProcessInstance(), loadTasks()])
|
||||
})
|
||||
</script>
|
||||
126
src/pages-bpm/processInstance/detail/process-cancel/index.vue
Normal file
126
src/pages-bpm/processInstance/detail/process-cancel/index.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="取消流程"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 友情提醒 -->
|
||||
<view class="mb-24rpx border border-[#ffd591] rounded-16rpx bg-[#fff7e6] p-24rpx">
|
||||
<view class="mb-12rpx flex items-center">
|
||||
<wd-icon name="warning" color="#faad14" size="32rpx" />
|
||||
<text class="ml-12rpx text-28rpx text-[#faad14] font-bold">友情提醒</text>
|
||||
</view>
|
||||
<text class="text-26rpx text-[#666]">取消后,该审批流程将自动结束。</text>
|
||||
</view>
|
||||
|
||||
<!-- 取消理由 -->
|
||||
<wd-textarea
|
||||
v-model="formData.cancelReason"
|
||||
prop="cancelReason"
|
||||
label="取消理由:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入取消理由"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
确认取消
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { cancelProcessInstanceByStartUser } from '@/api/bpm/processInstance'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId?: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const taskId = computed(() => props.taskId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const formData = reactive({
|
||||
cancelReason: '',
|
||||
})
|
||||
const formRules = {
|
||||
cancelReason: [
|
||||
{ required: true, message: '取消理由不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
const backUrl = taskId.value
|
||||
? `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`
|
||||
: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}`
|
||||
navigateBackPlus(backUrl)
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
await cancelProcessInstanceByStartUser(
|
||||
processInstanceId.value,
|
||||
formData.cancelReason,
|
||||
)
|
||||
toast.success('流程取消成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
143
src/pages-bpm/processInstance/detail/reassign/index.vue
Normal file
143
src/pages-bpm/processInstance/detail/reassign/index.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="isDelegate ? '委派任务' : '转办任务'"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 用户选择 -->
|
||||
<UserPicker
|
||||
v-model="formData.userId"
|
||||
prop="userId"
|
||||
type="radio"
|
||||
:label="`${isDelegate ? '接收人' : '新审批人'}:`"
|
||||
:placeholder="`请选择${isDelegate ? '接收人' : '新审批人'}`"
|
||||
/>
|
||||
|
||||
<!-- 审批意见 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="审批意见:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入审批意见"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isDelegate ? '委派' : '转办' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { delegateTask, transferTask } from '@/api/bpm/task'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
type: string // 'delegate' 或 'transfer'
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const operationType = computed(() => props.type || 'transfer') // 默认转办
|
||||
const isDelegate = computed(() => operationType.value === 'delegate')
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const formData = reactive({
|
||||
userId: undefined as number | undefined,
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
userId: [
|
||||
{ required: true, message: `请选择${isDelegate.value ? '接收人' : '新审批人'}` },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '审批意见不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
id: taskId.value as string,
|
||||
reason: formData.reason,
|
||||
}
|
||||
if (isDelegate.value) {
|
||||
await delegateTask({
|
||||
...data,
|
||||
delegateUserId: String(formData.userId),
|
||||
})
|
||||
} else {
|
||||
await transferTask({
|
||||
...data,
|
||||
assigneeUserId: String(formData.userId),
|
||||
})
|
||||
}
|
||||
toast.success(`${isDelegate.value ? '委派' : '转办'}成功`)
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
140
src/pages-bpm/processInstance/detail/return/index.vue
Normal file
140
src/pages-bpm/processInstance/detail/return/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="退回任务"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 操作表单 -->
|
||||
<view class="p-24rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<!-- 退回节点选择 -->
|
||||
<wd-picker
|
||||
v-model="formData.targetActivityId"
|
||||
label="退回节点:"
|
||||
prop="targetActivityId"
|
||||
:columns="activityOptions"
|
||||
value-key="taskDefinitionKey"
|
||||
label-key="name"
|
||||
placeholder="请选择退回节点"
|
||||
/>
|
||||
|
||||
<!-- 退回原因 -->
|
||||
<wd-textarea
|
||||
v-model="formData.reason"
|
||||
prop="reason"
|
||||
label="退回原因:"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入退回原因"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<!-- 提交按钮 -->
|
||||
<view class="mt-48rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
:disabled="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
退回
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getTaskListByReturn, returnTask } from '@/api/bpm/task'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
processInstanceId: string
|
||||
taskId: string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const taskId = computed(() => props.taskId)
|
||||
const processInstanceId = computed(() => props.processInstanceId)
|
||||
const toast = useToast()
|
||||
const formLoading = ref(false)
|
||||
const activityOptions = ref<any[]>([])
|
||||
const formData = reactive({
|
||||
targetActivityId: '',
|
||||
reason: '',
|
||||
})
|
||||
const formRules = {
|
||||
targetActivityId: [
|
||||
{ required: true, message: '退回节点不能为空' },
|
||||
],
|
||||
reason: [
|
||||
{ required: true, message: '退回原因不能为空' },
|
||||
],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus(`/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`)
|
||||
}
|
||||
|
||||
/** 获取可退回的节点列表 */
|
||||
async function loadReturnTaskList() {
|
||||
const result = await getTaskListByReturn(taskId.value)
|
||||
activityOptions.value = result
|
||||
}
|
||||
|
||||
/** 提交操作 */
|
||||
async function handleSubmit() {
|
||||
if (formLoading.value) {
|
||||
return
|
||||
}
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
await returnTask({
|
||||
id: taskId.value as string,
|
||||
targetTaskDefinitionKey: formData.targetActivityId,
|
||||
reason: formData.reason,
|
||||
})
|
||||
|
||||
toast.success('退回成功')
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/pages-bpm/processInstance/detail/index?id=${processInstanceId.value}&taskId=${taskId.value}`,
|
||||
})
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面加载时获取可退回节点列表 */
|
||||
onMounted(() => {
|
||||
/** 初始化校验 */
|
||||
if (!props.taskId || !props.processInstanceId) {
|
||||
toast.show('参数错误')
|
||||
return
|
||||
}
|
||||
loadReturnTaskList()
|
||||
})
|
||||
</script>
|
||||
222
src/pages-bpm/processInstance/manager/components/search-form.vue
Normal file
222
src/pages-bpm/processInstance/manager/components/search-form.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发起人
|
||||
</view>
|
||||
<UserPicker
|
||||
v-model="formData.startUserId"
|
||||
type="radio"
|
||||
placeholder="请选择发起人"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
流程名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入流程名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view v-if="processDefinitionList.length > 0" class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
所属流程
|
||||
</view>
|
||||
<wd-picker
|
||||
v-model="formData.processDefinitionId"
|
||||
:columns="processDefinitionList"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
label=""
|
||||
/>
|
||||
</view>
|
||||
<!-- 流程分类 -->
|
||||
<view v-if="categoryList.length > 0" class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
流程分类
|
||||
</view>
|
||||
<wd-picker
|
||||
v-model="formData.category"
|
||||
:columns="categoryList"
|
||||
label-key="name"
|
||||
value-key="code"
|
||||
label=""
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
流程状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发起时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Category } from '@/api/bpm/category'
|
||||
import type { ProcessDefinition } from '@/api/bpm/definition'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { getCategorySimpleList } from '@/api/bpm/category'
|
||||
import { getProcessDefinitionList } from '@/api/bpm/definition'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
startUserId: undefined as number | undefined, // 发起人
|
||||
name: undefined as string | undefined, // 流程名称
|
||||
processDefinitionId: undefined as string | undefined, // 所属流程
|
||||
category: undefined as string | undefined, // 流程分类
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined], // 发起时间
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索流程实例'
|
||||
})
|
||||
|
||||
const categoryList = ref<Category[]>([])
|
||||
const processDefinitionList = ref<ProcessDefinition[]>([])
|
||||
|
||||
// 时间选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 获取流程分类列表 */
|
||||
async function getCategoryList() {
|
||||
try {
|
||||
categoryList.value = await getCategorySimpleList()
|
||||
} catch (error) {
|
||||
console.error('获取流程分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取流程定义列表 */
|
||||
async function getProcessDefinitions() {
|
||||
try {
|
||||
processDefinitionList.value = await getProcessDefinitionList({ suspensionState: 1 })
|
||||
} catch (error) {
|
||||
console.error('获取流程定义失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.startUserId = undefined
|
||||
formData.name = undefined
|
||||
formData.processDefinitionId = undefined
|
||||
formData.category = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getCategoryList()
|
||||
getProcessDefinitions()
|
||||
})
|
||||
</script>
|
||||
231
src/pages-bpm/processInstance/manager/index.vue
Normal file
231
src/pages-bpm/processInstance/manager/index.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="流程管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 流程实例列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="mr-16rpx flex-1">
|
||||
<view class="line-clamp-1 text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="mt-8rpx text-24rpx text-[#999]">
|
||||
{{ item.categoryName || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
<DictTag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center">
|
||||
<view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-20rpx text-white">
|
||||
{{ item.startUser?.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-28rpx text-[#333]">
|
||||
{{ item.startUser?.nickname || '-' }}
|
||||
</view>
|
||||
<view class="text-24rpx text-[#999]">
|
||||
{{ item.startUser?.deptName || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx rounded-8rpx bg-[#f7f8f9] p-16rpx">
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">发起时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.startTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.endTime" class="flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">结束时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.endTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.tasks && item.tasks.length > 0" class="mb-12rpx">
|
||||
<view class="mb-8rpx text-26rpx text-[#999]">
|
||||
当前审批任务
|
||||
</view>
|
||||
<view class="flex flex-wrap gap-8rpx">
|
||||
<wd-tag
|
||||
v-for="task in item.tasks"
|
||||
:key="task.id"
|
||||
type="primary"
|
||||
plain
|
||||
@click.stop="handleTaskDetail(item, task)"
|
||||
>
|
||||
{{ task.name }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="item.status === BpmProcessInstanceStatus.RUNNING"
|
||||
class="flex items-center justify-end border-t border-[#f0f0f0] -mt-8"
|
||||
>
|
||||
<wd-button size="small" type="error" plain @click.stop="handleCancel(item)">
|
||||
取消流程
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无流程实例" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ProcessInstance } from '@/api/bpm/processInstance'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import {
|
||||
cancelProcessInstanceByAdmin,
|
||||
getProcessInstanceManagerPage,
|
||||
} from '@/api/bpm/processInstance'
|
||||
import DictTag from '@/components/dict-tag/dict-tag.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
// 流程实例状态枚举
|
||||
const BpmProcessInstanceStatus = {
|
||||
RUNNING: 1, // 进行中
|
||||
APPROVE: 2, // 审批通过
|
||||
REJECT: 3, // 审批不通过
|
||||
CANCEL: 4, // 已取消
|
||||
}
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const total = ref(0)
|
||||
const list = ref<(ProcessInstance & { tasks?: { id: string, name: string }[] })[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询流程实例列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getProcessInstanceManagerPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: ProcessInstance) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${item.id}` })
|
||||
}
|
||||
|
||||
/** 查看任务详情 */
|
||||
function handleTaskDetail(row: ProcessInstance, task: { id: string, name: string }) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${row.id}&taskId=${task.id}` })
|
||||
}
|
||||
|
||||
/** 取消流程实例 */
|
||||
function handleCancel(item: ProcessInstance) {
|
||||
uni.showModal({
|
||||
title: '取消流程',
|
||||
editable: true,
|
||||
placeholderText: '请输入取消原因',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
const reason = res.content?.trim()
|
||||
if (!reason) {
|
||||
toast.error('请输入取消原因')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await cancelProcessInstanceByAdmin(item.id, reason)
|
||||
toast.success('取消成功')
|
||||
// 刷新列表
|
||||
queryParams.value.pageNo = 1
|
||||
list.value = []
|
||||
await getList()
|
||||
} catch (error) {
|
||||
console.error('取消流程失败:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
128
src/pages-bpm/task/manager/components/search-form.vue
Normal file
128
src/pages-bpm/task/manager/components/search-form.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
任务名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined, // 任务名称
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined], // 创建时间
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索任务'
|
||||
})
|
||||
|
||||
// 时间选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
166
src/pages-bpm/task/manager/index.vue
Normal file
166
src/pages-bpm/task/manager/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="任务管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="mr-16rpx flex-1">
|
||||
<view class="line-clamp-1 text-32rpx text-[#333] font-semibold">
|
||||
{{ item.processInstance?.name || '-' }}
|
||||
</view>
|
||||
<view class="mt-8rpx text-24rpx text-[#999]">
|
||||
当前任务:{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<DictTag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center">
|
||||
<view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-20rpx text-white">
|
||||
{{ item.processInstance?.startUser?.nickname?.[0] || '?' }}
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-28rpx text-[#333]">
|
||||
发起人:{{ item.processInstance?.startUser?.nickname || '-' }}
|
||||
</view>
|
||||
<view class="text-24rpx text-[#999]">
|
||||
审批人:{{ item.assigneeUser?.nickname || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded-8rpx bg-[#f7f8f9] p-16rpx">
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">任务开始时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.endTime" class="mb-8rpx flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">任务结束时间</text>
|
||||
<text class="text-[#333]">{{ formatDateTime(item.endTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.reason" class="flex items-center justify-between text-26rpx">
|
||||
<text class="text-[#999]">审批建议</text>
|
||||
<text class="line-clamp-1 ml-16rpx flex-1 text-right text-[#333]">{{ item.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无任务" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Task } from '@/api/bpm/task'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getTaskManagerPage } from '@/api/bpm/task'
|
||||
import DictTag from '@/components/dict-tag/dict-tag.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<Task[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询任务列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getTaskManagerPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情(历史) */
|
||||
function handleDetail(item: Task) {
|
||||
if (item.processInstance?.id) {
|
||||
uni.navigateTo({ url: `/pages-bpm/processInstance/detail/index?id=${item.processInstance.id}` })
|
||||
}
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
153
src/pages-bpm/user-group/components/search-form.vue
Normal file
153
src/pages-bpm/user-group/components/search-form.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
组名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入组名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`组名:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索用户分组'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
155
src/pages-bpm/user-group/detail/index.vue
Normal file
155
src/pages-bpm/user-group/detail/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="用户分组详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="组名" :value="formData?.name" />
|
||||
<wd-cell title="描述" :value="formData?.description || '-'" />
|
||||
<wd-cell title="成员">
|
||||
<view class="flex flex-wrap gap-8rpx justify-end">
|
||||
<wd-tag
|
||||
v-for="userId in (formData?.userIds || [])"
|
||||
:key="userId"
|
||||
type="primary"
|
||||
plain
|
||||
>
|
||||
{{ getUserNickname(userId) }}
|
||||
</wd-tag>
|
||||
<text v-if="!formData?.userIds?.length">-</text>
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:user-group:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['bpm:user-group:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserGroup } from '@/api/bpm/user-group'
|
||||
import type { SimpleUser } from '@/api/system/user'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteUserGroup, getUserGroup } from '@/api/bpm/user-group'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<UserGroup>()
|
||||
const deleting = ref(false)
|
||||
const userList = ref<SimpleUser[]>([])
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/user-group/index')
|
||||
}
|
||||
|
||||
/** 获取用户昵称 */
|
||||
function getUserNickname(userId: number) {
|
||||
const user = userList.value.find(u => u.id === userId)
|
||||
return user?.nickname || userId
|
||||
}
|
||||
|
||||
/** 加载用户列表 */
|
||||
async function loadUserList() {
|
||||
userList.value = await getSimpleUserList()
|
||||
}
|
||||
|
||||
/** 加载用户分组详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getUserGroup(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑用户分组 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/user-group/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除用户分组 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该用户分组吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteUserGroup(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
151
src/pages-bpm/user-group/form/index.vue
Normal file
151
src/pages-bpm/user-group/form/index.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="组名"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入组名"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
label="描述"
|
||||
label-width="180rpx"
|
||||
prop="description"
|
||||
clearable
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
<UserPicker
|
||||
ref="userPickerRef"
|
||||
v-model="formData.userIds"
|
||||
label="成员"
|
||||
type="checkbox"
|
||||
placeholder="请选择成员"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { UserGroup } from '@/api/bpm/user-group'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createUserGroup, getUserGroup, updateUserGroup } from '@/api/bpm/user-group'
|
||||
import { UserPicker } from '@/components/system-select'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑用户分组' : '新增用户分组')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<UserGroup>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
description: '',
|
||||
userIds: [],
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '组名不能为空' }],
|
||||
userIds: [{ required: true, message: '成员不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
const userPickerRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-bpm/user-group/index')
|
||||
}
|
||||
|
||||
/** 加载用户分组详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getUserGroup(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateUserGroup(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createUserGroup(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
190
src/pages-bpm/user-group/index.vue
Normal file
190
src/pages-bpm/user-group/index.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="用户分组管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 用户分组列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">描述:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.description || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">成员:</text>
|
||||
<view class="min-w-0 flex flex-1 flex-wrap gap-8rpx">
|
||||
<wd-tag
|
||||
v-for="userId in (item.userIds || []).slice(0, 3)"
|
||||
:key="userId"
|
||||
type="primary"
|
||||
plain
|
||||
>
|
||||
{{ getUserNickname(userId) }}
|
||||
</wd-tag>
|
||||
<wd-tag v-if="(item.userIds || []).length > 3" type="info" plain>
|
||||
+{{ (item.userIds || []).length - 3 }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无用户分组数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['bpm:user-group:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UserGroup } from '@/api/bpm/user-group'
|
||||
import type { SimpleUser } from '@/api/system/user'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getUserGroupPage } from '@/api/bpm/user-group'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<UserGroup[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const userList = ref<SimpleUser[]>([])
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 获取用户昵称 */
|
||||
function getUserNickname(userId: number) {
|
||||
const user = userList.value.find(u => u.id === userId)
|
||||
return user?.nickname || userId
|
||||
}
|
||||
|
||||
/** 加载用户列表 */
|
||||
async function loadUserList() {
|
||||
userList.value = await getSimpleUserList()
|
||||
}
|
||||
|
||||
/** 查询用户分组列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getUserGroupPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增用户分组 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-bpm/user-group/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: UserGroup) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-bpm/user-group/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadUserList()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user