|
@@ -9,78 +9,75 @@
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<!-- 搜索框 -->
|
|
<!-- 搜索框 -->
|
|
|
- <view class="bg-white p-24rpx">
|
|
|
|
|
- <wd-search
|
|
|
|
|
- v-model="searchName"
|
|
|
|
|
- placeholder="请输入流程名称"
|
|
|
|
|
- placeholder-left
|
|
|
|
|
- hide-cancel
|
|
|
|
|
- @search="handleSearch"
|
|
|
|
|
- @clear="handleSearch"
|
|
|
|
|
- />
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <wd-search
|
|
|
|
|
+ v-model="searchName"
|
|
|
|
|
+ placeholder="请输入流程名称"
|
|
|
|
|
+ placeholder-left
|
|
|
|
|
+ hide-cancel
|
|
|
|
|
+ @search="handleSearch"
|
|
|
|
|
+ @clear="handleSearch"
|
|
|
|
|
+ />
|
|
|
|
|
|
|
|
<!-- 分类标签 -->
|
|
<!-- 分类标签 -->
|
|
|
- <!-- TODO @AI:可以使用 https://wot-ui.cn/component/index-bar.html 组件么? -->
|
|
|
|
|
- <view class="flex overflow-x-auto bg-white px-16rpx">
|
|
|
|
|
- <view
|
|
|
|
|
- v-for="(item, index) in categoryList"
|
|
|
|
|
- :key="item.id"
|
|
|
|
|
- class="relative whitespace-nowrap px-24rpx py-20rpx text-28rpx"
|
|
|
|
|
- :class="activeIndex === index ? 'font-bold text-[#1890ff]' : 'text-[#666]'"
|
|
|
|
|
- @click="switchCategory(index)"
|
|
|
|
|
- >
|
|
|
|
|
- {{ item.name }}
|
|
|
|
|
- <view
|
|
|
|
|
- v-if="activeIndex === index"
|
|
|
|
|
- class="absolute bottom-0 left-24rpx right-24rpx h-4rpx bg-[#1890ff]"
|
|
|
|
|
- />
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
|
|
+ <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-view
|
|
|
scroll-y
|
|
scroll-y
|
|
|
- class="h-[calc(100vh-280rpx)]"
|
|
|
|
|
|
|
+ class="h-[calc(100vh-320rpx)]"
|
|
|
:scroll-into-view="scrollIntoView"
|
|
:scroll-into-view="scrollIntoView"
|
|
|
scroll-with-animation
|
|
scroll-with-animation
|
|
|
|
|
+ @scroll="handleScroll"
|
|
|
>
|
|
>
|
|
|
<view
|
|
<view
|
|
|
- v-for="(definitions, category) in groupedDefinitions"
|
|
|
|
|
- :id="`category-${category}`"
|
|
|
|
|
- :key="category"
|
|
|
|
|
- class="mx-24rpx mt-24rpx"
|
|
|
|
|
|
|
+ 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 justify-between">
|
|
|
|
|
- <text class="text-28rpx text-[#333] font-bold">{{ getCategoryName(category as string) }}</text>
|
|
|
|
|
- <wd-icon
|
|
|
|
|
- :name="expandedCategories[category as string] ? 'arrow-up' : 'arrow-down'"
|
|
|
|
|
- size="32rpx"
|
|
|
|
|
- @click="toggleCategory(category as string)"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <view class="mb-16rpx flex items-center">
|
|
|
|
|
+ <text class="text-28rpx text-[#333] font-bold">{{ item.name }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
<!-- 流程列表 -->
|
|
<!-- 流程列表 -->
|
|
|
- <view v-if="expandedCategories[category as string]" class="overflow-hidden rounded-16rpx bg-white">
|
|
|
|
|
|
|
+ <view v-if="groupedDefinitions[item.code]?.length" class="overflow-hidden rounded-16rpx bg-white">
|
|
|
<view
|
|
<view
|
|
|
- v-for="(item, index) in definitions"
|
|
|
|
|
- :key="item.id"
|
|
|
|
|
|
|
+ v-for="definition in groupedDefinitions[item.code]"
|
|
|
|
|
+ :key="definition.id"
|
|
|
class="flex items-center border-b border-[#f5f5f5] p-24rpx last:border-b-0"
|
|
class="flex items-center border-b border-[#f5f5f5] p-24rpx last:border-b-0"
|
|
|
- @click="handleSelect(item)"
|
|
|
|
|
|
|
+ @click="handleSelect(definition)"
|
|
|
>
|
|
>
|
|
|
|
|
+ <image
|
|
|
|
|
+ v-if="definition.icon"
|
|
|
|
|
+ :src="definition.icon"
|
|
|
|
|
+ class="mr-16rpx h-64rpx w-64rpx rounded-12rpx object-contain"
|
|
|
|
|
+ mode="aspectFit"
|
|
|
|
|
+ />
|
|
|
<view
|
|
<view
|
|
|
|
|
+ v-else
|
|
|
class="mr-16rpx h-64rpx w-64rpx flex items-center justify-center rounded-12rpx"
|
|
class="mr-16rpx h-64rpx w-64rpx flex items-center justify-center rounded-12rpx"
|
|
|
- :style="{ backgroundColor: getIconColor(index) }"
|
|
|
|
|
|
|
+ :style="{ backgroundColor: getIconColor(definition.name) }"
|
|
|
>
|
|
>
|
|
|
- <wd-icon :name="getIconName(index)" size="40rpx" color="#fff" />
|
|
|
|
|
|
|
+ <text class="text-24rpx text-white font-bold">{{ getIconText(definition.name) }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
- <text class="text-28rpx text-[#333]">{{ item.name }}</text>
|
|
|
|
|
|
|
+ <text class="text-28rpx text-[#333]">{{ definition.name }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</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>
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
<!-- 空状态 -->
|
|
|
- <view v-if="Object.keys(groupedDefinitions).length === 0" class="py-100rpx">
|
|
|
|
|
|
|
+ <view v-if="categoryList.length === 0" class="py-100rpx">
|
|
|
<wd-status-tip image="content" tip="暂无可发起的流程" />
|
|
<wd-status-tip image="content" tip="暂无可发起的流程" />
|
|
|
</view>
|
|
</view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
@@ -91,7 +88,7 @@
|
|
|
import type { Category } from '@/api/bpm/category'
|
|
import type { Category } from '@/api/bpm/category'
|
|
|
import type { ProcessDefinition } from '@/api/bpm/definition'
|
|
import type { ProcessDefinition } from '@/api/bpm/definition'
|
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
|
-import { computed, ref } from 'vue'
|
|
|
|
|
|
|
+import { computed, nextTick, ref } from 'vue'
|
|
|
import { useToast } from 'wot-design-uni'
|
|
import { useToast } from 'wot-design-uni'
|
|
|
import { getCategorySimpleList } from '@/api/bpm/category'
|
|
import { getCategorySimpleList } from '@/api/bpm/category'
|
|
|
import { getProcessDefinitionList } from '@/api/bpm/definition'
|
|
import { getProcessDefinitionList } from '@/api/bpm/definition'
|
|
@@ -121,23 +118,29 @@ definePage({
|
|
|
const toast = useToast()
|
|
const toast = useToast()
|
|
|
|
|
|
|
|
const searchName = ref('')
|
|
const searchName = ref('')
|
|
|
-const activeIndex = ref(0)
|
|
|
|
|
-const scrollIntoView = ref('')
|
|
|
|
|
|
|
+const activeCategory = ref('')
|
|
|
const categoryList = ref<Category[]>([])
|
|
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[]>([])
|
|
const definitionList = ref<ProcessDefinition[]>([])
|
|
|
-const expandedCategories = ref<Record<string, boolean>>({})
|
|
|
|
|
-
|
|
|
|
|
-/** 图标配置 */
|
|
|
|
|
-// TODO @芋艿:【流程定义图标】支持显示流程定义的自定义图标 definition.icon
|
|
|
|
|
-// 对应 vben 第 175-189 行:优先显示 definition.icon,无图标时显示流程名称前两个字
|
|
|
|
|
-// TODO @AI:优化下,图标使用 vben 对应的逻辑;
|
|
|
|
|
-const iconConfig = [
|
|
|
|
|
- { icon: 'warning', color: '#D98469' },
|
|
|
|
|
- { icon: 'heart', color: '#7BC67C' },
|
|
|
|
|
- { icon: 'cart', color: '#4A7FEB' },
|
|
|
|
|
- { icon: 'home', color: '#4A7FEB' },
|
|
|
|
|
- { icon: 'location', color: '#4A9DEB' },
|
|
|
|
|
-]
|
|
|
|
|
|
|
+
|
|
|
|
|
+/** 根据流程名称获取图标背景色 */
|
|
|
|
|
+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(() => {
|
|
const filteredDefinitions = computed(() => {
|
|
@@ -159,13 +162,7 @@ const groupedDefinitions = computed<Record<string, ProcessDefinition[]>>(() => {
|
|
|
grouped[item.category] = []
|
|
grouped[item.category] = []
|
|
|
grouped[item.category].push(item)
|
|
grouped[item.category].push(item)
|
|
|
})
|
|
})
|
|
|
- // 按 categoryList 顺序排序
|
|
|
|
|
- const ordered: Record<string, ProcessDefinition[]> = {}
|
|
|
|
|
- categoryList.value.forEach((cat) => {
|
|
|
|
|
- if (grouped[cat.code])
|
|
|
|
|
- ordered[cat.code] = grouped[cat.code]
|
|
|
|
|
- })
|
|
|
|
|
- return ordered
|
|
|
|
|
|
|
+ return grouped
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
/** 返回上一页 */
|
|
/** 返回上一页 */
|
|
@@ -174,46 +171,63 @@ function handleBack() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 搜索 */
|
|
/** 搜索 */
|
|
|
-function handleSearch() {
|
|
|
|
|
- // 搜索时展开所有分类
|
|
|
|
|
- categoryList.value.forEach((cat) => {
|
|
|
|
|
- expandedCategories.value[cat.code] = true
|
|
|
|
|
- })
|
|
|
|
|
|
|
+async function handleSearch() {
|
|
|
|
|
+ // 搜索后重新计算分类位置
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ updateCategoryPositions()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 切换分类 */
|
|
|
|
|
-// TODO @AI:目前有个 bug;滚动到为止后,选中的 category 不会变;
|
|
|
|
|
-function switchCategory(index: number) {
|
|
|
|
|
- activeIndex.value = index
|
|
|
|
|
- const category = categoryList.value[index]
|
|
|
|
|
- if (category) {
|
|
|
|
|
- expandedCategories.value[category.code] = true
|
|
|
|
|
- // 滚动到对应分类
|
|
|
|
|
- scrollIntoView.value = ''
|
|
|
|
|
|
|
+/** Tab 点击 */
|
|
|
|
|
+function handleTabClick({ name }: { index: number, name: string }) {
|
|
|
|
|
+ isTabClicking.value = true
|
|
|
|
|
+ // 滚动到对应分类
|
|
|
|
|
+ scrollIntoView.value = ''
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ scrollIntoView.value = `category-${name}`
|
|
|
|
|
+ // 300ms 后恢复滚动监听
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
- scrollIntoView.value = `category-${category.code}`
|
|
|
|
|
- }, 50)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/** 切换分类展开/收起 */
|
|
|
|
|
-function toggleCategory(code: string) {
|
|
|
|
|
- expandedCategories.value[code] = !expandedCategories.value[code]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/** 获取分类名称 */
|
|
|
|
|
-function getCategoryName(code: string) {
|
|
|
|
|
- return categoryList.value.find(item => item.code === code)?.name || code
|
|
|
|
|
|
|
+ isTabClicking.value = false
|
|
|
|
|
+ }, 300)
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 获取图标名称 */
|
|
|
|
|
-function getIconName(index: number) {
|
|
|
|
|
- return iconConfig[index % iconConfig.length].icon
|
|
|
|
|
|
|
+/** 滚动事件 - 自动切换 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 getIconColor(index: number) {
|
|
|
|
|
- return iconConfig[index % iconConfig.length].color
|
|
|
|
|
|
|
+/** 更新分类区域位置信息 */
|
|
|
|
|
+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
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 选择流程定义 */
|
|
/** 选择流程定义 */
|
|
@@ -231,10 +245,6 @@ function handleSelect(item: ProcessDefinition) {
|
|
|
/** 加载分类列表 */
|
|
/** 加载分类列表 */
|
|
|
async function loadCategoryList() {
|
|
async function loadCategoryList() {
|
|
|
categoryList.value = await getCategorySimpleList()
|
|
categoryList.value = await getCategorySimpleList()
|
|
|
- // 默认展开所有分类
|
|
|
|
|
- categoryList.value.forEach((cat) => {
|
|
|
|
|
- expandedCategories.value[cat.code] = true
|
|
|
|
|
- })
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 加载流程定义列表 */
|
|
/** 加载流程定义列表 */
|
|
@@ -245,5 +255,17 @@ async function loadDefinitionList() {
|
|
|
/** 初始化 */
|
|
/** 初始化 */
|
|
|
onLoad(async () => {
|
|
onLoad(async () => {
|
|
|
await Promise.all([loadCategoryList(), loadDefinitionList()])
|
|
await Promise.all([loadCategoryList(), loadDefinitionList()])
|
|
|
|
|
+ // 默认选中第一个分类
|
|
|
|
|
+ if (categoryList.value.length > 0) {
|
|
|
|
|
+ activeCategory.value = categoryList.value[0].code
|
|
|
|
|
+ }
|
|
|
|
|
+ // 等待 DOM 渲染后计算分类位置
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ updateCategoryPositions()
|
|
|
|
|
+ }, 100)
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+</style>
|