index.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <template>
  2. <!-- TODO vben 对应的地址:/Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/processInstance/create/index.vue -->
  3. <view class="yd-page-container">
  4. <!-- 顶部导航栏 -->
  5. <wd-navbar
  6. title="发起申请"
  7. left-arrow placeholder safe-area-inset-top fixed
  8. @click-left="handleBack"
  9. />
  10. <!-- 搜索框 -->
  11. <view class="bg-white p-24rpx">
  12. <wd-search
  13. v-model="searchName"
  14. placeholder="请输入流程名称"
  15. placeholder-left
  16. hide-cancel
  17. @search="handleSearch"
  18. @clear="handleSearch"
  19. />
  20. </view>
  21. <!-- 分类标签 -->
  22. <!-- TODO @AI:可以使用 https://wot-ui.cn/component/index-bar.html 组件么? -->
  23. <view class="flex overflow-x-auto bg-white px-16rpx">
  24. <view
  25. v-for="(item, index) in categoryList"
  26. :key="item.id"
  27. class="relative whitespace-nowrap px-24rpx py-20rpx text-28rpx"
  28. :class="activeIndex === index ? 'font-bold text-[#1890ff]' : 'text-[#666]'"
  29. @click="switchCategory(index)"
  30. >
  31. {{ item.name }}
  32. <view
  33. v-if="activeIndex === index"
  34. class="absolute bottom-0 left-24rpx right-24rpx h-4rpx bg-[#1890ff]"
  35. />
  36. </view>
  37. </view>
  38. <!-- 流程定义列表 -->
  39. <scroll-view
  40. scroll-y
  41. class="h-[calc(100vh-280rpx)]"
  42. :scroll-into-view="scrollIntoView"
  43. scroll-with-animation
  44. >
  45. <view
  46. v-for="(definitions, category) in groupedDefinitions"
  47. :id="`category-${category}`"
  48. :key="category"
  49. class="mx-24rpx mt-24rpx"
  50. >
  51. <!-- 分类标题 -->
  52. <view class="mb-16rpx flex items-center justify-between">
  53. <text class="text-28rpx text-[#333] font-bold">{{ getCategoryName(category as string) }}</text>
  54. <wd-icon
  55. :name="expandedCategories[category as string] ? 'arrow-up' : 'arrow-down'"
  56. size="32rpx"
  57. @click="toggleCategory(category as string)"
  58. />
  59. </view>
  60. <!-- 流程列表 -->
  61. <view v-if="expandedCategories[category as string]" class="overflow-hidden rounded-16rpx bg-white">
  62. <view
  63. v-for="(item, index) in definitions"
  64. :key="item.id"
  65. class="flex items-center border-b border-[#f5f5f5] p-24rpx last:border-b-0"
  66. @click="handleSelect(item)"
  67. >
  68. <view
  69. class="mr-16rpx h-64rpx w-64rpx flex items-center justify-center rounded-12rpx"
  70. :style="{ backgroundColor: getIconColor(index) }"
  71. >
  72. <wd-icon :name="getIconName(index)" size="40rpx" color="#fff" />
  73. </view>
  74. <text class="text-28rpx text-[#333]">{{ item.name }}</text>
  75. </view>
  76. </view>
  77. </view>
  78. <!-- 空状态 -->
  79. <view v-if="Object.keys(groupedDefinitions).length === 0" class="py-100rpx">
  80. <wd-status-tip image="content" tip="暂无可发起的流程" />
  81. </view>
  82. </scroll-view>
  83. </view>
  84. </template>
  85. <script lang="ts" setup>
  86. import type { Category } from '@/api/bpm/category'
  87. import type { ProcessDefinition } from '@/api/bpm/definition'
  88. import { onLoad } from '@dcloudio/uni-app'
  89. import { computed, ref } from 'vue'
  90. import { useToast } from 'wot-design-uni'
  91. import { getCategorySimpleList } from '@/api/bpm/category'
  92. import { getProcessDefinitionList } from '@/api/bpm/definition'
  93. import { navigateBackPlus } from '@/utils'
  94. // TODO @芋艿:【重新发起流程】支持通过 processInstanceId 参数重新发起已有流程
  95. // 对应 vben 第 44-60 行:从路由获取 processInstanceId,查询流程实例后自动选中对应流程定义并填充表单数据
  96. // TODO @芋艿:【流程表单填写】选择流程后跳转到表单填写页面
  97. // 对应 vben form.vue 全部:包含以下子功能:
  98. // - 表单渲染 (form-create):vben form.vue 第 145-152 行
  99. // - 审批流程预览时间线:vben form.vue 第 153-162 行
  100. // - 流程图预览 (BPMN/简易):vben form.vue 第 163-178 行
  101. // - 发起人自选审批人:vben form.vue 第 30-32, 85-95 行
  102. // - 表单字段权限控制 (读/写/隐藏):vben form.vue 第 119-131 行
  103. // - 业务表单跳转 (formCustomCreatePath):vben form.vue 第 79-85 行
  104. // - 表单值变化重新预测审批节点:vben form.vue 第 87-102 行
  105. // - 提交流程实例:vben form.vue 第 56-76 行
  106. definePage({
  107. style: {
  108. navigationBarTitleText: '',
  109. navigationStyle: 'custom',
  110. },
  111. })
  112. const toast = useToast()
  113. const searchName = ref('')
  114. const activeIndex = ref(0)
  115. const scrollIntoView = ref('')
  116. const categoryList = ref<Category[]>([])
  117. const definitionList = ref<ProcessDefinition[]>([])
  118. const expandedCategories = ref<Record<string, boolean>>({})
  119. /** 图标配置 */
  120. // TODO @芋艿:【流程定义图标】支持显示流程定义的自定义图标 definition.icon
  121. // 对应 vben 第 175-189 行:优先显示 definition.icon,无图标时显示流程名称前两个字
  122. // TODO @AI:优化下,图标使用 vben 对应的逻辑;
  123. const iconConfig = [
  124. { icon: 'warning', color: '#D98469' },
  125. { icon: 'heart', color: '#7BC67C' },
  126. { icon: 'cart', color: '#4A7FEB' },
  127. { icon: 'home', color: '#4A7FEB' },
  128. { icon: 'location', color: '#4A9DEB' },
  129. ]
  130. /** 过滤后的流程定义 */
  131. const filteredDefinitions = computed(() => {
  132. if (!searchName.value.trim()) {
  133. return definitionList.value
  134. }
  135. return definitionList.value.filter(item =>
  136. item.name.toLowerCase().includes(searchName.value.toLowerCase()),
  137. )
  138. })
  139. /** 按分类分组的流程定义 */
  140. const groupedDefinitions = computed<Record<string, ProcessDefinition[]>>(() => {
  141. const grouped: Record<string, ProcessDefinition[]> = {}
  142. filteredDefinitions.value.forEach((item) => {
  143. if (!item.category)
  144. return
  145. if (!grouped[item.category])
  146. grouped[item.category] = []
  147. grouped[item.category].push(item)
  148. })
  149. // 按 categoryList 顺序排序
  150. const ordered: Record<string, ProcessDefinition[]> = {}
  151. categoryList.value.forEach((cat) => {
  152. if (grouped[cat.code])
  153. ordered[cat.code] = grouped[cat.code]
  154. })
  155. return ordered
  156. })
  157. /** 返回上一页 */
  158. function handleBack() {
  159. navigateBackPlus('/pages/bpm/index')
  160. }
  161. /** 搜索 */
  162. function handleSearch() {
  163. // 搜索时展开所有分类
  164. categoryList.value.forEach((cat) => {
  165. expandedCategories.value[cat.code] = true
  166. })
  167. }
  168. /** 切换分类 */
  169. // TODO @AI:目前有个 bug;滚动到为止后,选中的 category 不会变;
  170. function switchCategory(index: number) {
  171. activeIndex.value = index
  172. const category = categoryList.value[index]
  173. if (category) {
  174. expandedCategories.value[category.code] = true
  175. // 滚动到对应分类
  176. scrollIntoView.value = ''
  177. setTimeout(() => {
  178. scrollIntoView.value = `category-${category.code}`
  179. }, 50)
  180. }
  181. }
  182. /** 切换分类展开/收起 */
  183. function toggleCategory(code: string) {
  184. expandedCategories.value[code] = !expandedCategories.value[code]
  185. }
  186. /** 获取分类名称 */
  187. function getCategoryName(code: string) {
  188. return categoryList.value.find(item => item.code === code)?.name || code
  189. }
  190. /** 获取图标名称 */
  191. function getIconName(index: number) {
  192. return iconConfig[index % iconConfig.length].icon
  193. }
  194. /** 获取图标颜色 */
  195. function getIconColor(index: number) {
  196. return iconConfig[index % iconConfig.length].color
  197. }
  198. /** 选择流程定义 */
  199. function handleSelect(item: ProcessDefinition) {
  200. // TODO @芋艿:【流程描述提示】显示流程描述 Tooltip
  201. // 对应 vben 第 190-195 行:鼠标悬停显示 definition.description
  202. // TODO @芋艿:【搜索高亮动画】搜索匹配时卡片有弹跳动画效果
  203. // 对应 vben 第 169-173 行:搜索时添加 animate-bounce-once 动画类
  204. // TODO @芋艿:跳转到流程表单页面
  205. toast.show(`选择了: ${item.name}`)
  206. }
  207. /** 加载分类列表 */
  208. async function loadCategoryList() {
  209. categoryList.value = await getCategorySimpleList()
  210. // 默认展开所有分类
  211. categoryList.value.forEach((cat) => {
  212. expandedCategories.value[cat.code] = true
  213. })
  214. }
  215. /** 加载流程定义列表 */
  216. async function loadDefinitionList() {
  217. definitionList.value = await getProcessDefinitionList({ suspensionState: 1 })
  218. }
  219. /** 初始化 */
  220. onLoad(async () => {
  221. await Promise.all([loadCategoryList(), loadDefinitionList()])
  222. })
  223. </script>