From 64b52fa928e11640e8d6aad49bd39cd27c896543 Mon Sep 17 00:00:00 2001 From: wupengfei <834520024@qq.com> Date: 星期五, 21 二月 2025 18:42:13 +0800 Subject: [PATCH] feat: 订单 --- packages/components/src/views/electricBillRecharge/electricBillRecharge.vue | 41 ++- packages/components/src/components/Card/OrderCardItem.vue | 47 ++++ packages/components/src/components/Card/OrderCard.vue | 59 +++++ packages/components/src/views/Order/components/ElectricOrder.vue | 34 +++ packages/components/src/components/InfiniteLoading/InfiniteLoading.vue | 235 +++++++++++++++++++++++ packages/components/src/views/Order/components/PhoneOrder.vue | 33 +++ apps/taro/src/subpackages/order/order/InnerPage.vue | 20 ++ apps/taro/src/app.config.ts | 4 apps/taro/src/pages/mine/index.vue | 8 packages/components/src/utils/lifeRechargeConstants.ts | 30 +++ apps/taro/src/subpackages/order/order/order.vue | 14 + apps/taro/src/constants/router.ts | 1 packages/components/src/views/Order/Order.vue | 24 ++ packages/components/src/index.ts | 1 apps/taro/src/subpackages/order/order/order.config.ts | 3 packages/services/api/LifePay.ts | 2 16 files changed, 539 insertions(+), 17 deletions(-) diff --git a/apps/taro/src/app.config.ts b/apps/taro/src/app.config.ts index 1452de5..1f44a78 100644 --- a/apps/taro/src/app.config.ts +++ b/apps/taro/src/app.config.ts @@ -60,6 +60,10 @@ 'rechargeResult/rechargeResult', ], }, + { + root: 'subpackages/order', + pages: ['order/order'], + }, ], // preloadRule: { // 'pages/mine/index': { diff --git a/apps/taro/src/constants/router.ts b/apps/taro/src/constants/router.ts index e7dbf76..35682a6 100644 --- a/apps/taro/src/constants/router.ts +++ b/apps/taro/src/constants/router.ts @@ -10,6 +10,7 @@ phoneBillRecharge = '/subpackages/recharge/phoneBillRecharge/phoneBillRecharge', electricBillRecharge = '/subpackages/recharge/electricBillRecharge/electricBillRecharge', + order = '/subpackages/order/order/order', selectPayType = '/subpackages/recharge/selectPayType/selectPayType', rechargeResult = '/subpackages/recharge/rechargeResult/rechargeResult', } diff --git a/apps/taro/src/pages/mine/index.vue b/apps/taro/src/pages/mine/index.vue index e86f178..2717095 100644 --- a/apps/taro/src/pages/mine/index.vue +++ b/apps/taro/src/pages/mine/index.vue @@ -18,9 +18,9 @@ </div> </div> <ContentScrollView> - <List class="mine-list-wrapper" v-if="isLogin"> + <List class="mine-list-wrapper"> <ListItem title="璁㈠崟绠$悊" @click="goOrderManage"></ListItem> - <ListItem title="閫�鍑虹櫥褰�" @click="goLogout"></ListItem> + <ListItem v-if="isLogin" title="閫�鍑虹櫥褰�" @click="goLogout"></ListItem> </List> </ContentScrollView> </PageLayoutWithBg> @@ -28,7 +28,7 @@ <script setup lang="ts"> import { TransparentNavigationBar, List, ListItem } from '@/components'; -import { useUser, useIsLogin, useGoLogin } from '@/hooks'; +import { useUser, useIsLogin, useGoLogin, useAccessLogin } from '@/hooks'; import Taro from '@tarojs/taro'; import { RouterPath, OssAssets } from '@/constants'; import DefaultAvatar from '@/assets/components/icon-default-avatar.png'; @@ -69,7 +69,7 @@ }; }); -function goOrderManage() {} +const goOrderManage = useAccessLogin(() => goPage(RouterPath.order)); function goLogout() { userStore.logout(); diff --git a/apps/taro/src/subpackages/order/order/InnerPage.vue b/apps/taro/src/subpackages/order/order/InnerPage.vue new file mode 100644 index 0000000..0bdfd9d --- /dev/null +++ b/apps/taro/src/subpackages/order/order/InnerPage.vue @@ -0,0 +1,20 @@ +<template> + <ContentScrollView :paddingH="false"> + <Order /> + </ContentScrollView> +</template> + +<script setup lang="ts"> +import { Order } from '@life-payment/components'; +import Taro from '@tarojs/taro'; + +defineOptions({ + name: 'InnerPage', +}); + +function goPay() { + Taro.navigateTo({ + url: RouterPath.selectPayType, + }); +} +</script> diff --git a/apps/taro/src/subpackages/order/order/order.config.ts b/apps/taro/src/subpackages/order/order/order.config.ts new file mode 100644 index 0000000..305fdb1 --- /dev/null +++ b/apps/taro/src/subpackages/order/order/order.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + disableScroll: true, +}); diff --git a/apps/taro/src/subpackages/order/order/order.vue b/apps/taro/src/subpackages/order/order/order.vue new file mode 100644 index 0000000..fe1cf70 --- /dev/null +++ b/apps/taro/src/subpackages/order/order/order.vue @@ -0,0 +1,14 @@ +<template> + <PageLayout title="璁㈠崟绠$悊" class="order-page-wrapper" hasBorder> + <InnerPage /> + </PageLayout> +</template> + +<script setup lang="ts"> +import { PageLayout } from '@/components'; +import InnerPage from './InnerPage.vue'; + +defineOptions({ + name: 'order', +}); +</script> diff --git a/packages/components/src/components/Card/OrderCard.vue b/packages/components/src/components/Card/OrderCard.vue new file mode 100644 index 0000000..14ed16e --- /dev/null +++ b/packages/components/src/components/Card/OrderCard.vue @@ -0,0 +1,59 @@ +<template> + <div class="order-card"> + <div class="order-card-title"> + <div class="order-card-title-top"> + <div class="order-card-title-text">{{ title }}</div> + <div class="order-card-title-status">{{ status }}</div> + </div> + <div class="order-card-title-ordernum"> + {{ `璁㈠崟缂栧彿锛�${'JF202502191515350002'}` }} + </div> + </div> + <div class="order-card-content"> + <slot></slot> + </div> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: 'OrderCard', +}); + +type Props = { + title: string; + status: string; +}; + +const props = withDefaults(defineProps<Props>(), {}); +</script> +<style lang="scss"> +.order-card { + border: 1px solid #e8e8e8; + border-radius: 12px; + padding: 20px; + + .order-card-title { + display: flex; + flex-direction: column; + border-bottom: 1px solid #e8e8e8; + padding-bottom: 20px; + margin-bottom: 20px; + + .order-card-title-top { + display: flex; + justify-content: space-between; + color: #333333; + font-size: 28px; + line-height: 40px; + font-weight: 600; + margin-bottom: 12px; + } + + .order-card-title-ordernum { + font-size: 24px; + color: #999999; + } + } +} +</style> diff --git a/packages/components/src/components/Card/OrderCardItem.vue b/packages/components/src/components/Card/OrderCardItem.vue new file mode 100644 index 0000000..fc4dd47 --- /dev/null +++ b/packages/components/src/components/Card/OrderCardItem.vue @@ -0,0 +1,47 @@ +<template> + <div class="order-card-item"> + <div class="order-card-item-label" :style="{ width: labelWidth, textAlign: textAlign }"> + <slot name="label">{{ label }}</slot> + </div> + <div class="order-card-item-value"> + <slot name="value">{{ value }}</slot> + </div> + </div> +</template> + +<script setup lang="ts"> +defineOptions({ + name: 'OrderCardItem', +}); + +type Props = { + label: string; + value: string; + labelWidth?: any; + textAlign?: any; +}; + +const props = withDefaults(defineProps<Props>(), { + labelWidth: '80px', + textAlign: 'left', +}); +</script> + +<style lang="scss"> +.order-card-item { + display: flex; + font-size: 28px; + line-height: 40px; + margin-bottom: 15px; + + .order-card-item-label { + color: #333333; + } + + .order-card-item-value { + color: #666666; + flex: 1; + min-width: 0; + } +} +</style> diff --git a/packages/components/src/components/InfiniteLoading/InfiniteLoading.vue b/packages/components/src/components/InfiniteLoading/InfiniteLoading.vue new file mode 100644 index 0000000..5a8aebe --- /dev/null +++ b/packages/components/src/components/InfiniteLoading/InfiniteLoading.vue @@ -0,0 +1,235 @@ +<template> + <scroll-view + :scroll-y="true" + :refresher-background="'transparent'" + :lowerThreshold="100" + @scrolltolower="lower" + :refresherEnabled="refresherEnabled" + :refresherTriggered="state.triggered" + @refresherrefresh="onRefresherRefresh" + :show-scrollbar="false" + :style="scrollViewStyle" + :enable-back-to-top="true" + :class="[scrollViewClassName]" + @scroll="onScroll" + :scroll-top="scrollDistance" + enhanced + :scroll-with-animation="true" + v-bind="{ ...$attrs }" + > + <slot name="header"></slot> + <LoadingLayout :loading="isLoading" :error="isError" :loadError="() => refetch?.()"> + <NoData v-if="hasNoData" /> + <div + :class="['infinite-list-inner', { noShowMoreText: !showMoreText, hasPaddingTop }]" + v-else + > + <slot name="extra" /> + <slot v-if="$slots.default" /> + <template v-else> + <template v-for="(group, index) in listData.pages" :key="index"> + <template v-for="(item, i) in group.data" :key="i"> + <slot + name="renderItem" + :item="item" + :groupIndex="index" + :itemIndex="i" + :index="findDataIndex(item)" + /> + </template> + </template> + </template> + </div> + <div + v-if="!hasNoData && showMoreText && listData?.pages?.length > 0 && !hasMore" + class="loading-more-tips" + > + {{ noMoreText }} + </div> + <div v-if="isFetching && hasMore && enabledLoadingMore" class="infiniting-tips"> + <Loading class="infiniting-tips-icon"></Loading>鏁版嵁鍔犺浇涓�... + </div> + </LoadingLayout> + </scroll-view> + <div class="back-top-wrapper" @click.stop="backToTop" v-show="oldScrollDistance > 100"> + <img class="back-top-img" :src="IconBackTop" /> + <div class="back-top-text">杩斿洖椤堕儴</div> + </div> +</template> + +<script lang="ts" setup generic="T"> +import { VNode, CSSProperties, computed, reactive } from 'vue'; +import NoData from '../NoData/NoData.vue'; +import LoadingLayout from '../Layout/LoadingLayout.vue'; +import { FetchNextPageOptions } from '@tanstack/vue-query'; +import { Loading } from '@nutui/icons-vue-taro'; +import { useScrollDistance } from 'senin-mini/hooks'; +import IconBackTop from '@/assets/components/icon-back-top.png'; + +defineOptions({ + name: 'InfiniteLoading', +}); + +type Page = { + data?: T[]; + pageModel?: { + rows?: number; + page?: number; + totalCount?: number; + totalPage?: number; + }; + [key: string]: any; +}; + +type TData = { + pages: Page[]; +}; + +type Props = { + // list?: TData; + listData?: TData; + flattenListData?: T[]; + renderItem?: (item: T, index: number) => VNode; + refresherEnabled?: boolean; + hasMore?: boolean; + isLoading?: boolean; + isError?: boolean; + isFetching?: boolean; + isFetchingNextPage?: boolean; + refetch?: (options?: any) => Promise<any>; + scrollViewStyle?: string | CSSProperties; + scrollViewClassName?: string; + fetchNextPage?: (options?: FetchNextPageOptions) => Promise<any>; + showMoreText?: boolean; + hasPaddingTop?: boolean; + noMoreText?: string; + enabledLoadingMore?: boolean; + customNoData?: boolean; +}; + +const props = withDefaults(defineProps<Props>(), { + renderItem: () => () => null, + refresherEnabled: true, + showMoreText: true, + hasPaddingTop: false, + noMoreText: '娌℃湁鏇村鍐呭浜唦', + enabledLoadingMore: true, + customNoData: undefined, +}); + +const emit = defineEmits<{ + (e: 'refresh', done: () => any): void; + (e: 'loadMore'): void; +}>(); + +const hasNoData = computed(() => { + if (props.customNoData !== undefined) return props.customNoData; + if (props.listData?.pages?.length) { + return props.listData?.pages[0].data.length === 0; + } + return true; +}); + +const state = reactive({ + triggered: false, +}); + +function lower() { + if (props.hasMore && props.enabledLoadingMore) { + props.fetchNextPage?.(); + emit('loadMore'); + } +} + +// eslint-disable-next-line no-unused-vars +async function onRefresherRefresh() { + try { + // 姝e浜庡埛鏂扮姸鎬� + if (state.triggered) return; + state.triggered = true; + + await props.refetch?.(); + + state.triggered = false; + } catch (error) {} +} + +const { onScroll, scrollDistance, oldScrollDistance, setScrollDistance } = useScrollDistance(); + +function backToTop() { + setScrollDistance(0); +} + +const scrollToBottom = (dis = 300) => { + setScrollDistance(scrollDistance.value + dis); +}; + +function findDataIndex(item: T) { + return props.flattenListData?.findIndex((data) => data === item); +} + +defineExpose({ backToTop, scrollToBottom }); +</script> + +<style lang="scss"> +@import '@/styles/common.scss'; + +.loading-more-tips { + color: boleGetCssVar('text-color', 'primary'); + padding: 18px 10px; + width: auto; + font-size: 24px; + text-align: center; +} + +.infinite-list-inner { + // padding: 30px 30px 0; + + &.hasPaddingTop { + padding-top: 20px; + } + + &.noShowMoreText { + padding-bottom: 30px; + } +} + +.infiniting-tips { + color: boleGetCssVar('text-color', 'primary'); + padding: 18px 10px; + width: auto; + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + + .infiniting-tips-icon { + margin-right: 10px; + } +} + +.back-top-wrapper { + width: 92px; + height: 92px; + background: #ffffff; + box-shadow: 0px 0px 28px 0px rgba(0, 0, 0, 0.18); + position: fixed; + border-radius: 50%; + right: boleGetCssVar('size', 'body-padding-h'); + bottom: 390px; + + .back-top-img { + width: 44px; + height: 44px; + margin: 12px auto 2px; + } + + .back-top-text { + font-weight: 400; + font-size: 16px; + color: boleGetCssVar('text-color', 'regular'); + line-height: 22px; + text-align: center; + } +} +</style> diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 8027acd..83f40ea 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,4 +1,5 @@ export { default as RechargeGrid } from './views/RechargeGrid/RechargeGrid.vue'; +export { default as Order } from './views/Order/Order.vue'; export { default as PhoneBillRecharge } from './views/PhoneBillRecharge/PhoneBillRecharge.vue'; export { default as electricBillRecharge } from './views/electricBillRecharge/electricBillRecharge.vue'; export { default as SelectPayTypeView } from './views/SelectPayTypeView/SelectPayTypeView.vue'; diff --git a/packages/components/src/utils/lifeRechargeConstants.ts b/packages/components/src/utils/lifeRechargeConstants.ts index c6dfd5b..87bb7be 100644 --- a/packages/components/src/utils/lifeRechargeConstants.ts +++ b/packages/components/src/utils/lifeRechargeConstants.ts @@ -46,4 +46,34 @@ [IspCode.dianxin]: '涓浗鐢典俊', [IspCode.liantong]: '涓浗鑱旈��', }; + + export enum ElectricType { + /**鍥藉鐢电綉 */ + guowang = 'guowang', + /**鍗楁柟鐢电綉 */ + nanwang = 'nanwang', + } + + export const ElectricTypeText = { + [ElectricType.guowang]: '鍥藉鐢电綉', + [ElectricType.nanwang]: '鍗楁柟鐢电綉', + }; + + export enum ElectricAccountType { + /**浣忓畢 */ + house = 'house', + /**浼佷簨涓� */ + enterprise = 'enterprise', + /**搴楅摵 */ + shop = 'shop', + /**搴楅摵-涓嶅缓璁娇鐢� */ + nanwashopng = 'nanwashopng', + } + + export const ElectricAccountTypeText = { + [ElectricAccountType.house]: '浣忓畢', + [ElectricAccountType.enterprise]: '浼佷簨涓�', + [ElectricAccountType.shop]: '搴楅摵', + [ElectricAccountType.nanwashopng]: '搴楅摵-涓嶅缓璁娇鐢�', + }; } diff --git a/packages/components/src/views/Order/Order.vue b/packages/components/src/views/Order/Order.vue new file mode 100644 index 0000000..ecfc8de --- /dev/null +++ b/packages/components/src/views/Order/Order.vue @@ -0,0 +1,24 @@ +<template> + <div class="order-wrapper"> + <nut-tabs v-model="orderType"> + <nut-tab-pane title="璇濊垂璁㈠崟" pane-key="1"> + <PhoneOrder></PhoneOrder> + </nut-tab-pane> + <nut-tab-pane title="鐢佃垂璁㈠崟" pane-key="2"> + <ElectricOrder></ElectricOrder> + </nut-tab-pane> + </nut-tabs> + </div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue'; +import PhoneOrder from './components/PhoneOrder.vue'; +import ElectricOrder from './components/ElectricOrder.vue'; + +defineOptions({ + name: 'Order', +}); + +const orderType = ref(1); +</script> diff --git a/packages/components/src/views/Order/components/ElectricOrder.vue b/packages/components/src/views/Order/components/ElectricOrder.vue new file mode 100644 index 0000000..1ab4dc3 --- /dev/null +++ b/packages/components/src/views/Order/components/ElectricOrder.vue @@ -0,0 +1,34 @@ +<template> + <!-- <InfiniteLoading scrollViewClassName="common-infinite-scroll-list" v-bind="infiniteLoadingProps"> + <template #renderItem="{ item }"> --> + <OrderCard title="鐢佃垂鍏呭��" status="寰呮敮浠�"> + <OrderCardItem label="鍏呭�煎湴鍖�:" :value="'18888888888'" /> + <OrderCardItem label="鍏呭�兼埛鍙�:" :value="'18888888888'" /> + <OrderCardItem label="涓嬪崟鏃堕棿:" :value="'2025-02-19 17:15:54'" /> + <OrderCardItem label="鍏呭�奸噾棰�:" :value="'18888888888'" /> + <OrderCardItem label="浼樻儬閲戦:" :value="'18888888888'" /> + <OrderCardItem label="瀹炰粯閲戦:" :value="'18888888888'" /> + <OrderCardItem label="鏀粯鏃堕棿:" :value="'2025-02-19 17:15:54'" /> + <OrderCardItem label="瀹屾垚鏃堕棿:" :value="'2025-02-19 17:15:54'" /> + </OrderCard> + <!-- </template> + </InfiniteLoading> --> +</template> + +<script setup lang="ts"> +import InfiniteLoading from '../../../components/InfiniteLoading/InfiniteLoading.vue'; +import OrderCard from '../../../components/Card/OrderCard.vue'; +import OrderCardItem from '../../../components/Card/OrderCardItem.vue'; + +defineOptions({ + name: 'ElectricOrder', +}); + +// type Props = {}; + +// const props = withDefaults(defineProps<Props>(), {}); + +const infiniteLoadingProps = {}; +</script> + +<style lang="scss"></style> diff --git a/packages/components/src/views/Order/components/PhoneOrder.vue b/packages/components/src/views/Order/components/PhoneOrder.vue new file mode 100644 index 0000000..530a7cf --- /dev/null +++ b/packages/components/src/views/Order/components/PhoneOrder.vue @@ -0,0 +1,33 @@ +<template> + <!-- <InfiniteLoading scrollViewClassName="common-infinite-scroll-list" v-bind="infiniteLoadingProps"> + <template #renderItem="{ item }"> --> + <OrderCard title="璇濊垂鍏呭��" status="寰呮敮浠�"> + <OrderCardItem label="鍏呭�艰处鍙�:" :value="'18888888888'" /> + <OrderCardItem label="涓嬪崟鏃堕棿:" :value="'18888888888'" /> + <OrderCardItem label="鍏呭�奸噾棰�:" :value="'18888888888'" /> + <OrderCardItem label="浼樻儬閲戦:" :value="'18888888888'" /> + <OrderCardItem label="瀹炰粯閲戦:" :value="'18888888888'" /> + <OrderCardItem label="鏀粯鏃堕棿:" :value="'2025-02-19 17:15:54'" /> + <OrderCardItem label="瀹屾垚鏃堕棿:" :value="'2025-02-19 17:15:54'" /> + </OrderCard> + <!-- </template> + </InfiniteLoading> --> +</template> + +<script setup lang="ts"> +import InfiniteLoading from '../../../components/InfiniteLoading/InfiniteLoading.vue'; +import OrderCard from '../../../components/Card/OrderCard.vue'; +import OrderCardItem from '../../../components/Card/OrderCardItem.vue'; + +defineOptions({ + name: 'PhoneOrder', +}); + +// type Props = {}; + +// const props = withDefaults(defineProps<Props>(), {}); + +const infiniteLoadingProps = {}; +</script> + +<style lang="scss"></style> diff --git a/packages/components/src/views/electricBillRecharge/electricBillRecharge.vue b/packages/components/src/views/electricBillRecharge/electricBillRecharge.vue index bd6ffdc..c92e80a 100644 --- a/packages/components/src/views/electricBillRecharge/electricBillRecharge.vue +++ b/packages/components/src/views/electricBillRecharge/electricBillRecharge.vue @@ -6,32 +6,46 @@ label-position="top" class="order-bill-recharge electric" > - <FormItem label="鎵�鍦ㄥ煄甯�" class="bole-form-item" prop="type" required> + <FormItem label="鎵�鍦ㄥ煄甯�" class="bole-form-item" prop="province" required> <ChooseInputWithPicker - v-model="form.type" + v-model="form.province" placeholder="璇烽�夋嫨鍩庡競" :value-enum="IspCodeText" /> </FormItem> - <FormItem label="鐢电綉绫诲瀷" class="bole-form-item" prop="type" required> + <FormItem label="鐢电綉绫诲瀷" class="bole-form-item" prop="electricType" required> <ChooseInputWithPicker - v-model="form.type" + v-model="form.electricType" placeholder="璇烽�夋嫨鐢电綉绫诲瀷" - :value-enum="IspCodeText" + :value-enum="blLifeRecharge.constants.ElectricTypeText" /> </FormItem> - <FormItem label="鐢佃垂绫诲瀷" class="bole-form-item" prop="type" required> + <FormItem label="鐢佃垂绫诲瀷" class="bole-form-item" prop="electricAccountType" required> <ChooseInputWithPicker - v-model="form.type" + v-model="form.electricAccountType" placeholder="璇烽�夋嫨鐢佃垂绫诲瀷" - :value-enum="IspCodeText" + :value-enum="blLifeRecharge.constants.ElectricAccountTypeText" /> </FormItem> - <FormItem label="鐢电綉鎴峰彿" class="bole-form-item" prop="phone" required> + <FormItem label="鐢电綉鎴峰彿" class="bole-form-item" prop="electricAccount" required> <Input - v-model.trim="form.phone" + v-model.trim="form.electricAccount" class="bole-input-text" placeholder="璇疯緭鍏�13浣嶆暟瀛楃紪鍙�" + type="text" + /> + </FormItem> + <FormItem + v-if="form.electricType === blLifeRecharge.constants.ElectricType.nanwang" + label="韬唤璇佸悗鍏綅" + class="bole-form-item" + prop="sixID" + required + > + <Input + v-model.trim="form.sixID" + class="bole-input-text" + placeholder="璇疯緭鍏ヨ韩浠借瘉鍚庡叚浣�" type="text" /> </FormItem> @@ -107,9 +121,12 @@ const form = reactive({ ispCode: IspCode.yidong, - phone: '', parValue: 100, - type: IspCodeText.yidong, + electricAccount: '', + electricType: '', + electricAccountType: '', + province: '', + sixID: '', }); const rate = 0.96; diff --git a/packages/services/api/LifePay.ts b/packages/services/api/LifePay.ts index 301d9d3..7abf054 100644 --- a/packages/services/api/LifePay.ts +++ b/packages/services/api/LifePay.ts @@ -118,7 +118,7 @@ body: API.RefundLifePayOrderInput, options?: API.RequestConfig ) { - return request<any>('/api/LifePay/RefundLifePayOrder', { + return request<number>('/api/LifePay/RefundLifePayOrder', { method: 'POST', headers: { 'Content-Type': 'application/json', -- Gitblit v1.9.1