From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目
---
src/views/bpm/model/CategoryDraggableModel.vue | 665 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 665 insertions(+), 0 deletions(-)
diff --git a/src/views/bpm/model/CategoryDraggableModel.vue b/src/views/bpm/model/CategoryDraggableModel.vue
new file mode 100644
index 0000000..003e46f
--- /dev/null
+++ b/src/views/bpm/model/CategoryDraggableModel.vue
@@ -0,0 +1,665 @@
+<template>
+ <div class="flex items-center h-50px" v-memo="[categoryInfo.name, isCategorySorting]">
+ <!-- 澶撮儴锛氬垎绫诲悕 -->
+ <div class="flex items-center">
+ <el-tooltip content="鎷栧姩鎺掑簭" v-if="isCategorySorting">
+ <Icon
+ :size="22"
+ icon="ic:round-drag-indicator"
+ class="ml-10px category-drag-icon cursor-move text-#8a909c"
+ />
+ </el-tooltip>
+ <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
+ <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
+ </div>
+ <!-- 澶撮儴锛氭搷浣� -->
+ <div class="flex-1 flex" v-show="!isCategorySorting">
+ <div
+ v-if="categoryInfo.modelList.length > 0"
+ class="ml-20px flex items-center"
+ :class="[
+ 'transition-transform duration-300 cursor-pointer',
+ isExpand ? 'rotate-180' : 'rotate-0'
+ ]"
+ @click="isExpand = !isExpand"
+ >
+ <Icon icon="ep:arrow-down-bold" color="#999" />
+ </div>
+ <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
+ <template v-if="!isModelSorting">
+ <el-button
+ v-if="categoryInfo.modelList.length > 0"
+ link
+ type="info"
+ class="mr-20px"
+ @click.stop="handleModelSort"
+ >
+ <Icon icon="fa:sort-amount-desc" class="mr-5px" />
+ 鎺掑簭
+ </el-button>
+ <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
+ <Icon icon="fa:plus" class="mr-5px" />
+ 鏂板缓
+ </el-button>
+ <el-dropdown
+ @command="(command) => handleCategoryCommand(command, categoryInfo)"
+ placement="bottom"
+ >
+ <el-button link type="info">
+ <Icon icon="ep:setting" class="mr-5px" />
+ 鍒嗙被
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="handleRename"> 閲嶅懡鍚� </el-dropdown-item>
+ <el-dropdown-item command="handleDeleteCategory"> 鍒犻櫎璇ョ被 </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ <template v-else>
+ <el-button @click.stop="handleModelSortCancel"> 鍙� 娑� </el-button>
+ <el-button type="primary" @click.stop="handleModelSortSubmit"> 淇濆瓨鎺掑簭 </el-button>
+ </template>
+ </div>
+ </div>
+ </div>
+
+ <!-- 妯″瀷鍒楄〃 -->
+ <el-collapse-transition>
+ <div v-show="isExpand">
+ <el-table
+ v-if="modelList && modelList.length > 0"
+ :class="categoryInfo.name"
+ ref="tableRef"
+ :data="modelList"
+ row-key="id"
+ :header-cell-style="tableHeaderStyle"
+ :cell-style="tableCellStyle"
+ :row-style="{ height: '68px' }"
+ >
+ <el-table-column label="娴佺▼鍚�" prop="name" min-width="150">
+ <template #default="{ row }">
+ <div class="flex items-center">
+ <el-tooltip content="鎷栧姩鎺掑簭" v-if="isModelSorting">
+ <Icon
+ icon="ic:round-drag-indicator"
+ class="drag-icon cursor-move text-#8a909c mr-10px"
+ />
+ </el-tooltip>
+ <el-image v-if="row.icon" :src="row.icon" class="h-38px w-38px mr-10px rounded" />
+ <div v-else class="flow-icon">
+ <span style="font-size: 12px; color: #fff">{{ subString(row.name, 0, 2) }}</span>
+ </div>
+ {{ row.name }}
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙鑼冨洿" prop="startUserIds" min-width="150">
+ <template #default="{ row }">
+ <el-text v-if="!row.startUsers?.length && !row.startDepts?.length"> 鍏ㄩ儴鍙 </el-text>
+ <el-text v-else-if="row.startUsers.length === 1">
+ {{ row.startUsers[0].nickname }}
+ </el-text>
+ <el-text v-else-if="row.startDepts?.length === 1">
+ {{ row.startDepts[0].name }}
+ </el-text>
+ <el-text v-else-if="row.startDepts?.length > 1">
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="row.startDepts.map((dept: any) => dept.name).join('銆�')"
+ >
+ {{ row.startDepts[0].name }}绛� {{ row.startDepts.length }} 涓儴闂ㄥ彲瑙�
+ </el-tooltip>
+ </el-text>
+ <el-text v-else>
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="row.startUsers.map((user: any) => user.nickname).join('銆�')"
+ >
+ {{ row.startUsers[0].nickname }}绛� {{ row.startUsers.length }} 浜哄彲瑙�
+ </el-tooltip>
+ </el-text>
+ </template>
+ </el-table-column>
+ <el-table-column label="娴佺▼绫诲瀷" prop="type" min-width="120">
+ <template #default="{ row }">
+ <dict-tag :value="row.type" :type="DICT_TYPE.BPM_MODEL_TYPE" />
+ </template>
+ </el-table-column>
+ <el-table-column label="琛ㄥ崟淇℃伅" prop="formType" min-width="150">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.formType === BpmModelFormType.NORMAL"
+ type="primary"
+ link
+ @click="handleFormDetail(scope.row)"
+ >
+ <span>{{ scope.row.formName }}</span>
+ </el-button>
+ <el-button
+ v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+ type="primary"
+ link
+ @click="handleFormDetail(scope.row)"
+ >
+ <span>{{ scope.row.formCustomCreatePath }}</span>
+ </el-button>
+ <label v-else>鏆傛棤琛ㄥ崟</label>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈�鍚庡彂甯�" prop="deploymentTime" min-width="250">
+ <template #default="scope">
+ <div class="flex items-center">
+ <span v-if="scope.row.processDefinition" class="w-150px">
+ {{ formatDate(scope.row.processDefinition.deploymentTime) }}
+ </span>
+ <el-tag v-if="scope.row.processDefinition">
+ v{{ scope.row.processDefinition.version }}
+ </el-tag>
+ <el-tag v-else type="warning">鏈儴缃�</el-tag>
+ <el-tag
+ v-if="scope.row.processDefinition?.suspensionState === 2"
+ type="warning"
+ class="ml-10px"
+ >
+ 宸插仠鐢�
+ </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openModelForm('update', scope.row.id)"
+ :disabled="!isManagerUser(scope.row) && !hasPermiUpdate"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openModelForm('copy', scope.row.id)"
+ :disabled="!isManagerUser(scope.row) && !hasPermiUpdate"
+ >
+ 澶嶅埗
+ </el-button>
+ <el-button
+ link
+ class="!ml-5px"
+ type="primary"
+ @click="handleDeploy(scope.row)"
+ :disabled="!isManagerUser(scope.row) && !hasPermiDeploy"
+ >
+ 鍙戝竷
+ </el-button>
+ <el-dropdown
+ class="!align-middle ml-5px"
+ @command="(command) => handleModelCommand(command, scope.row)"
+ v-if="hasPermiMore"
+ >
+ <el-button type="primary" link>鏇村</el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="handleDefinitionList" v-if="hasPermiPdQuery">
+ 鍘嗗彶
+ </el-dropdown-item>
+ <el-dropdown-item
+ command="handleReport"
+ v-if="
+ checkPermi(['bpm:process-instance:manager-query']) &&
+ scope.row.processDefinition
+ "
+ :disabled="!isManagerUser(scope.row)"
+ >
+ 鎶ヨ〃
+ </el-dropdown-item>
+ <el-dropdown-item
+ command="handleChangeState"
+ v-if="hasPermiUpdate && scope.row.processDefinition"
+ :disabled="!isManagerUser(scope.row)"
+ >
+ {{ scope.row.processDefinition.suspensionState === 1 ? '鍋滅敤' : '鍚敤' }}
+ </el-dropdown-item>
+ <el-dropdown-item
+ type="danger"
+ command="handleClean"
+ v-if="checkPermi(['bpm:model:clean'])"
+ :disabled="!isManagerUser(scope.row)"
+ >
+ 娓呯悊
+ </el-dropdown-item>
+ <el-dropdown-item
+ type="danger"
+ command="handleDelete"
+ v-if="hasPermiDelete"
+ :disabled="!isManagerUser(scope.row)"
+ >
+ 鍒犻櫎
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-collapse-transition>
+
+ <!-- 寮圭獥锛氶噸鍛藉悕鍒嗙被 -->
+ <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
+ <template #title>
+ <div class="pl-10px font-bold text-18px"> 閲嶅懡鍚嶅垎绫� </div>
+ </template>
+ <div class="px-30px">
+ <el-input v-model="renameCategoryForm.name" />
+ </div>
+ <template #footer>
+ <div class="pr-25px pb-25px">
+ <el-button @click="renameCategoryVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleRenameConfirm">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </Dialog>
+
+ <!-- 寮圭獥锛氳〃鍗曡鎯� -->
+ <Dialog title="琛ㄥ崟璇︽儏" :fullscreen="true" v-model="formDetailVisible">
+ <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import Sortable from 'sortablejs'
+import { formatDate } from '@/utils/formatTime'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { useAppStore } from '@/store/modules/app'
+import { cloneDeep, isEqual } from 'lodash-es'
+import { useDebounceFn } from '@vueuse/core'
+import { subString } from '@/utils/index'
+
+defineOptions({ name: 'BpmModel' })
+
+// 浼樺寲 Props 绫诲瀷瀹氫箟
+interface UserInfo {
+ nickname: string
+ [key: string]: any
+}
+
+interface ProcessDefinition {
+ deploymentTime: string
+ version: number
+ suspensionState: number
+}
+
+interface ModelInfo {
+ id: number
+ name: string
+ icon?: string
+ startUsers?: UserInfo[]
+ processDefinition?: ProcessDefinition
+ formType?: number
+ formId?: number
+ formName?: string
+ formCustomCreatePath?: string
+ managerUserIds?: number[]
+ [key: string]: any
+}
+
+interface CategoryInfoProps {
+ id: number
+ name: string
+ modelList: ModelInfo[]
+}
+
+const props = defineProps<{
+ categoryInfo: CategoryInfoProps
+ isCategorySorting: boolean
+}>()
+
+const emit = defineEmits(['success'])
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { push } = useRouter() // 璺敱
+const userStore = useUserStoreWithOut() // 鐢ㄦ埛淇℃伅缂撳瓨
+const isDark = computed(() => useAppStore().getIsDark) // 鏄惁榛戞殫妯″紡
+const router = useRouter() // 璺敱
+
+const isModelSorting = ref(false) // 鏄惁姝e浜庢帓搴忕姸鎬�
+const originalData = ref<ModelInfo[]>([]) // 鍘熷鏁版嵁
+const modelList = ref<ModelInfo[]>([]) // 妯″瀷鍒楄〃
+const isExpand = ref(false) // 鏄惁澶勪簬灞曞紑鐘舵��
+
+// 浣跨敤 computed 浼樺寲琛ㄦ牸鏍峰紡璁$畻
+const tableHeaderStyle = computed(() => ({
+ backgroundColor: isDark.value ? '' : '#edeff0',
+ paddingLeft: '10px'
+}))
+
+const tableCellStyle = computed(() => ({
+ paddingLeft: '10px'
+}))
+
+/** 鏉冮檺鏍¢獙锛氶�氳繃 computed 瑙e喅鍒楄〃鐨勫崱椤块棶棰� */
+const hasPermiUpdate = computed(() => {
+ return checkPermi(['bpm:model:update'])
+})
+const hasPermiDelete = computed(() => {
+ return checkPermi(['bpm:model:delete'])
+})
+const hasPermiDeploy = computed(() => {
+ return checkPermi(['bpm:model:deploy'])
+})
+const hasPermiMore = computed(() => {
+ return checkPermi(['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete'])
+})
+const hasPermiPdQuery = computed(() => {
+ return checkPermi(['bpm:process-definition:query'])
+})
+
+/** '鏇村'鎿嶄綔鎸夐挳 */
+const handleModelCommand = (command: string, row: any) => {
+ switch (command) {
+ case 'handleDefinitionList':
+ handleDefinitionList(row)
+ break
+ case 'handleDelete':
+ handleDelete(row)
+ break
+ case 'handleChangeState':
+ handleChangeState(row)
+ break
+ case 'handleClean':
+ handleClean(row)
+ break
+ case 'handleReport':
+ router.push({
+ name: 'BpmProcessInstanceReport',
+ query: {
+ processDefinitionId: row.processDefinition.id,
+ processDefinitionKey: row.key
+ }
+ })
+ break
+ default:
+ break
+ }
+}
+
+/** '鍒嗙被'鎿嶄綔鎸夐挳 */
+const handleCategoryCommand = async (command: string, row: any) => {
+ switch (command) {
+ case 'handleRename':
+ renameCategoryForm.value = await CategoryApi.getCategory(row.id)
+ renameCategoryVisible.value = true
+ break
+ case 'handleDeleteCategory':
+ await handleDeleteCategory()
+ break
+ default:
+ break
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (row: any) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ModelApi.deleteModel(row.id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 娓呯悊鎸夐挳鎿嶄綔 */
+const handleClean = async (row: any) => {
+ try {
+ // 娓呯悊鐨勪簩娆$‘璁�
+ await message.confirm('鏄惁纭娓呯悊娴佺▼鍚嶅瓧涓�"' + row.name + '"鐨勬暟鎹」?')
+ // 鍙戣捣娓呯悊
+ await ModelApi.cleanModel(row.id)
+ message.success('娓呯悊鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 鏇存柊鐘舵�佹搷浣� */
+const handleChangeState = async (row: any) => {
+ const state = row.processDefinition.suspensionState
+ const newState = state === 1 ? 2 : 1
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const id = row.id
+ const statusState = state === 1 ? '鍋滅敤' : '鍚敤'
+ const content = '鏄惁纭' + statusState + '娴佺▼鍚嶅瓧涓�"' + row.name + '"鐨勬暟鎹」?'
+ await message.confirm(content)
+ // 鍙戣捣淇敼鐘舵��
+ await ModelApi.updateModelState(id, newState)
+ message.success(statusState + '鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 鍙戝竷娴佺▼ */
+const handleDeploy = async (row: any) => {
+ try {
+ await message.confirm('鏄惁纭鍙戝竷璇ユ祦绋嬶紵')
+ // 鍙戣捣閮ㄧ讲
+ await ModelApi.deployModel(row.id)
+ message.success(t('鍙戝竷鎴愬姛'))
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 璺宠浆鍒版寚瀹氭祦绋嬪畾涔夊垪琛� */
+const handleDefinitionList = (row: any) => {
+ push({
+ name: 'BpmProcessDefinition',
+ query: {
+ key: row.key
+ }
+ })
+}
+
+/** 娴佺▼琛ㄥ崟鐨勮鎯呮寜閽搷浣� */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+ rule: [],
+ option: {}
+})
+const handleFormDetail = async (row: any) => {
+ if (row.formType == BpmModelFormType.NORMAL) {
+ // 璁剧疆琛ㄥ崟
+ const data = await FormApi.getForm(row.formId)
+ setConfAndFields2(formDetailPreview, data.conf, data.fields)
+ // 寮圭獥鎵撳紑
+ formDetailVisible.value = true
+ } else {
+ await push({
+ path: row.formCustomCreatePath
+ })
+ }
+}
+
+/** 鍒ゆ柇鏄惁鍙互鎿嶄綔 */
+const isManagerUser = (row: any) => {
+ const userId = userStore.getUser.id
+ return row.managerUserIds && row.managerUserIds.includes(userId)
+}
+
+/** 澶勭悊妯″瀷鐨勬帓搴� **/
+const handleModelSort = () => {
+ if (isModelSorting.value) {
+ // 濡傛灉宸茬粡鍦ㄦ帓搴忕姸鎬侊紝鍒欏彇娑堟帓搴�
+ handleModelSortCancel()
+ } else {
+ // 淇濆瓨鍒濆鏁版嵁
+ originalData.value = cloneDeep(props.categoryInfo.modelList)
+ isModelSorting.value = true
+ initSort()
+ }
+}
+
+/** 澶勭悊妯″瀷鐨勬帓搴忔彁浜� */
+const handleModelSortSubmit = async () => {
+ // 淇濆瓨鎺掑簭
+ const ids = modelList.value.map((item: any) => item.id)
+ await ModelApi.updateModelSortBatch(ids)
+ // 鍒锋柊鍒楄〃
+ isModelSorting.value = false
+ message.success('鎺掑簭妯″瀷鎴愬姛')
+ emit('success')
+}
+
+/** 澶勭悊妯″瀷鐨勬帓搴忓彇娑� */
+const handleModelSortCancel = () => {
+ // 鎭㈠鍒濆鏁版嵁
+ modelList.value = cloneDeep(originalData.value)
+ isModelSorting.value = false
+}
+
+/** 鍒涘缓鎷栨嫿瀹炰緥 */
+const tableRef = ref()
+const initSort = useDebounceFn(() => {
+ const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
+ if (!table) return
+
+ Sortable.create(table, {
+ group: 'shared',
+ animation: 150,
+ draggable: '.el-table__row',
+ handle: '.drag-icon',
+ onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
+ if (oldDraggableIndex !== newDraggableIndex) {
+ modelList.value.splice(
+ newDraggableIndex,
+ 0,
+ modelList.value.splice(oldDraggableIndex, 1)[0]
+ )
+ }
+ }
+ })
+}, 200)
+
+/** 鏇存柊 modelList 妯″瀷鍒楄〃 */
+const updateModeList = useDebounceFn(() => {
+ const newModelList = props.categoryInfo.modelList
+ if (!isEqual(modelList.value, newModelList)) {
+ modelList.value = cloneDeep(newModelList)
+ if (newModelList?.length > 0) {
+ isExpand.value = true
+ }
+ }
+}, 100)
+
+/** 閲嶅懡鍚嶅脊绐楃‘瀹� */
+const renameCategoryVisible = ref(false)
+const renameCategoryForm = ref({
+ name: ''
+})
+const handleRenameConfirm = async () => {
+ if (renameCategoryForm.value?.name.length === 0) {
+ return message.warning('璇疯緭鍏ュ悕绉�')
+ }
+ // 鍙戣捣淇敼
+ await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
+ message.success('閲嶅懡鍚嶆垚鍔�')
+ // 鍒锋柊鍒楄〃
+ renameCategoryVisible.value = false
+ emit('success')
+}
+
+/** 鍒犻櫎鍒嗙被 */
+const handleDeleteCategory = async () => {
+ try {
+ if (props.categoryInfo.modelList.length > 0) {
+ return message.warning('璇ュ垎绫讳笅浠嶆湁娴佺▼瀹氫箟,涓嶅厑璁稿垹闄�')
+ }
+ await message.confirm('纭鍒犻櫎鍒嗙被鍚�?')
+ // 鍙戣捣鍒犻櫎
+ await CategoryApi.deleteCategory(props.categoryInfo.id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 娣诲姞/淇敼/澶嶅埗娴佺▼妯″瀷寮圭獥 */
+const openModelForm = async (type: string, id?: number) => {
+ if (type === 'create') {
+ await push({ name: 'BpmModelCreate' })
+ } else {
+ await push({
+ name: 'BpmModelUpdate',
+ params: { id, type }
+ })
+ }
+}
+
+watchEffect(() => {
+ if (props.categoryInfo?.modelList) {
+ updateModeList()
+ }
+
+ if (props.isCategorySorting) {
+ isExpand.value = false
+ }
+})
+</script>
+
+<style lang="scss">
+.rename-dialog.el-dialog {
+ padding: 0 !important;
+
+ .el-dialog__header {
+ border-bottom: none;
+ }
+
+ .el-dialog__footer {
+ border-top: none !important;
+ }
+}
+</style>
+<style lang="scss" scoped>
+.flow-icon {
+ display: flex;
+ width: 38px;
+ height: 38px;
+ margin-right: 10px;
+ background-color: var(--el-color-primary);
+ border-radius: 0.25rem;
+ align-items: center;
+ justify-content: center;
+}
+
+.category-draggable-model {
+ :deep(.el-table__cell) {
+ overflow: hidden;
+ border-bottom: none !important;
+ }
+
+ // 浼樺寲琛ㄦ牸娓叉煋鎬ц兘
+ :deep(.el-table__body) {
+ will-change: transform;
+ transform: translateZ(0);
+ }
+}
+</style>
--
Gitblit v1.8.0