From a375ecc695efe1739871e9d9d0bf981454aa8061 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期二, 11 八月 2026 10:20:35 +0800
Subject: [PATCH] fix: restore features overwritten by merge
---
talent-card-admin/src/views/BusinessManagementView.vue | 76 ++++++++++++++++---
talent-card-admin/src/views/FamilyCardApplicationDetailView.vue | 15 ---
talent-card-admin/src/types/admin.ts | 8 ++
talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyDetailResponse.java | 46 +++++++++++
rencaika/pages/external-webview/external-webview.uvue | 31 +++++++
talent-card-admin/src/router/index.ts | 9 +-
talent-card-admin/src/layouts/AdminLayout.vue | 23 +++++
7 files changed, 172 insertions(+), 36 deletions(-)
diff --git a/rencaika/pages/external-webview/external-webview.uvue b/rencaika/pages/external-webview/external-webview.uvue
index 3a553b2..f1ef6a6 100644
--- a/rencaika/pages/external-webview/external-webview.uvue
+++ b/rencaika/pages/external-webview/external-webview.uvue
@@ -12,7 +12,8 @@
</template>
<script setup lang="uts">
- import { getCarWashMerchantDetail, getTalentCardProfile } from '../../services/portal.uts'
+ import { getAppHome, getCarWashMerchantDetail, getTalentCardProfile } from '../../services/portal.uts'
+ import { getApiBaseUrl } from '../../config/api.uts'
import { safeBack } from '../../utils/navigation.uts'
type PageState = 'loading' | 'ready' | 'error' | 'invalid'
@@ -30,6 +31,7 @@
merchantId.value = options != null && options['merchantId'] != null ? options['merchantId'] as string : ''
talentId.value = options != null && options['talentId'] != null ? options['talentId'] as string : ''
talentCardId.value = options != null && options['talentCardId'] != null ? options['talentCardId'] as string : ''
+ if (source.value == 'homeMiniProgram') uni.setNavigationBarTitle({ title: '首页服务' })
if (!isValidSource()) {
showInvalid('服务地址无效', '请从已配置的服务入口重新进入')
return
@@ -38,7 +40,14 @@
})
const isSafeWebUrl = (value: string): boolean => /^https:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/|$)/.test(value)
- const isValidSource = (): boolean => (source.value == 'carWash' && /^\d+$/.test(merchantId.value)) || (source.value == 'advisor' && /^\d+$/.test(talentId.value))
+ const isSafeHomeLinkUrl = (value: string): boolean => {
+ if (isSafeWebUrl(value)) return true
+ const apiHost = getApiBaseUrl().replace(/^https?:\/\//, '').split('/')[0].split(':')[0]
+ if (apiHost == '') return false
+ const escapedHost = apiHost.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ return new RegExp('^http://' + escapedHost + '(?::\\d+)?(?:/|$)').test(value)
+ }
+ const isValidSource = (): boolean => (source.value == 'carWash' && /^\d+$/.test(merchantId.value)) || (source.value == 'advisor' && /^\d+$/.test(talentId.value)) || source.value == 'homeMiniProgram'
const loadExternalService = () => {
if (!isValidSource()) {
showInvalid('服务地址无效', '请从已配置的服务入口重新进入')
@@ -48,6 +57,7 @@
stateTitle.value = '正在打开服务'
stateDescription.value = '请稍候'
if (source.value == 'advisor') { loadAdvisorService(); return }
+ if (source.value == 'homeMiniProgram') { loadHomeMiniProgram(); return }
getCarWashMerchantDetail(merchantId.value, talentCardId.value, (data: any) => {
if (data == null || data.available !== true) {
showInvalid('服务暂不可用', data != null && data.unavailableReason != null ? data.unavailableReason as string : '请稍后重试')
@@ -56,6 +66,21 @@
const url = data.externalLinkType == 'web' && data.externalWebUrl != null ? data.externalWebUrl as string : ''
if (!isSafeWebUrl(url)) {
showInvalid('服务暂不可用', '服务地址暂未配置或未通过安全校验')
+ return
+ }
+ targetUrl.value = url
+ pageState.value = 'ready'
+ }, () => {
+ pageState.value = 'error'
+ stateTitle.value = '服务加载失败'
+ stateDescription.value = '请检查网络后重新加载'
+ })
+ }
+ const loadHomeMiniProgram = () => {
+ getAppHome((data: any) => {
+ const url = data != null && data.miniProgramUrl != null ? data.miniProgramUrl as string : ''
+ if (!isSafeHomeLinkUrl(url)) {
+ showInvalid('首页跳转链接不可用', '请在后台配置有效链接;生产环境请使用 HTTPS')
return
}
targetUrl.value = url
@@ -93,7 +118,7 @@
stateTitle.value = '网页加载失败'
stateDescription.value = '请稍后重新加载或返回服务详情'
}
- const goBack = () => safeBack(source.value == 'advisor' ? '/pages/card/card?preview=member&talentId=' + encodeURIComponent(talentId.value) : '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(merchantId.value))
+ const goBack = () => safeBack(source.value == 'advisor' ? '/pages/card/card?preview=member&talentId=' + encodeURIComponent(talentId.value) : (source.value == 'homeMiniProgram' ? '/pages/index/index' : '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(merchantId.value)))
</script>
<style>
diff --git a/talent-card-admin/src/layouts/AdminLayout.vue b/talent-card-admin/src/layouts/AdminLayout.vue
index 6220948..87b55bd 100644
--- a/talent-card-admin/src/layouts/AdminLayout.vue
+++ b/talent-card-admin/src/layouts/AdminLayout.vue
@@ -34,6 +34,7 @@
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
+const avatarText = computed(() => (appStore.user.name || 'A').trim().slice(0, 1).toUpperCase())
interface HeaderNotice {
id: number
@@ -128,6 +129,24 @@
{ index: 'operation-management', label: '运营管理', icon: Operation, items: operationMenus },
{ index: 'system-management', label: '系统管理', icon: Setting, items: systemMenus },
]
+
+// Keep the navigation in sync with the permissions returned at login. Newer
+// routes without a dedicated permission code intentionally remain visible.
+const routePermissions: Record<string, string> = {
+ '/talents': 'admin.talents.view', '/family-card-applications': 'admin.family-cards.view', '/upgrade-applications': 'admin.talents.view', '/family-cards': 'admin.family-cards.view', '/child-cards': 'admin.family-cards.view',
+ '/merchants': 'admin.merchants.view', '/products': 'admin.products.view', '/orders': 'admin.orders.view', '/refund-records': 'admin.refunds.view', '/hotel-free-verifications': 'admin.orders.view',
+ '/medical-reservations': 'admin.medical-reservations.view', '/medical-applications': 'admin.medical-reservations.view', '/parking-plates': 'admin.parking-plates.view', '/fuel-services': 'admin.parking-plates.view', '/spouse-employment': 'admin.parking-plates.view', '/hotel-reservations': 'admin.medical-reservations.view',
+ '/merchant-settlement': 'admin.merchants.view', '/equity-images': 'admin.equity-images.view', '/talent-news-management': 'admin.news.view', '/talent-policy-management': 'admin.news.view', '/movie-tickets': 'admin.news.view',
+ '/system-settings': 'admin.settings.view', '/protocol-settings': 'admin.settings.view', '/roles': 'admin.roles.view', '/staff': 'admin.staff.view',
+}
+const canAccess = (path?: string) => {
+ if (!path || appStore.user.role === '平台管理') return true
+ const required = routePermissions[path]
+ return !required || Boolean(appStore.user.permissionCodes?.includes('*') || appStore.user.permissionCodes?.includes(required))
+}
+const visibleMenuGroups = computed(() => menuGroups
+ .map((group) => ({ ...group, items: group.items.filter((item) => canAccess(item.path)) }))
+ .filter((group) => group.items.length > 0))
const defaultOpeneds = computed(() => {
const currentGroup = menuGroups.find((item) => findMenuLabel(item.items, activeMenu.value))
@@ -288,7 +307,7 @@
<el-tooltip content="退出登录" placement="bottom">
<el-button :icon="SwitchButton" circle @click="logout" />
</el-tooltip>
- <div class="admin-avatar" title="平台管理员" aria-label="平台管理员头像">A</div>
+ <div class="admin-avatar" :title="appStore.user.name" :aria-label="`${appStore.user.name}头像`">{{ avatarText }}</div>
</div>
</el-header>
@@ -306,7 +325,7 @@
<span>首页</span>
</el-menu-item>
- <el-sub-menu v-for="group in menuGroups" :key="group.index" :index="group.index">
+ <el-sub-menu v-for="group in visibleMenuGroups" :key="group.index" :index="group.index">
<template #title>
<el-icon>
<component :is="group.icon" />
diff --git a/talent-card-admin/src/router/index.ts b/talent-card-admin/src/router/index.ts
index 5e60950..84705d7 100644
--- a/talent-card-admin/src/router/index.ts
+++ b/talent-card-admin/src/router/index.ts
@@ -4,6 +4,7 @@
const familyCardView = () => import('../views/FamilyCardView.vue')
const businessManagementView = () => import('../views/BusinessManagementView.vue')
+const cardPlaceholderView = () => import('../views/CardPlaceholderView.vue')
const router = createRouter({
history: createWebHistory(),
@@ -154,7 +155,7 @@
{
path: 'appointment-management',
name: 'appointment-management',
- component: () => import('../views/AppointmentManagementView.vue'),
+ component: cardPlaceholderView,
meta: { title: '预约服务管理', businessKind: 'appointmentManagement' },
},
{
@@ -196,7 +197,7 @@
{
path: 'car-wash-benefits',
name: 'car-wash-benefits',
- component: () => import('../views/CarWashBenefitView.vue'),
+ component: cardPlaceholderView,
meta: { title: '洗车权益管理', businessKind: 'carWashBenefit' },
},
{
@@ -238,13 +239,13 @@
{
path: 'benefit-coupons',
name: 'benefit-coupons',
- component: () => import('../views/BenefitCouponManagementView.vue'),
+ component: cardPlaceholderView,
meta: { title: '优惠券管理', businessKind: 'benefitCoupons' },
},
{
path: 'display-services',
name: 'display-services',
- component: () => import('../views/DisplayServiceManagementView.vue'),
+ component: cardPlaceholderView,
meta: { title: '展示类服务管理', businessKind: 'displayServices' },
},
{
diff --git a/talent-card-admin/src/types/admin.ts b/talent-card-admin/src/types/admin.ts
index ccc237e..7ab8dbd 100644
--- a/talent-card-admin/src/types/admin.ts
+++ b/talent-card-admin/src/types/admin.ts
@@ -560,21 +560,29 @@
export interface FamilyCardApplication {
id: number | string
applicationNo?: string
+ applyNo?: string
talentId?: number | string
talentName?: string
talentMobile?: string
talentCardNo?: string
cardLevel?: string
familyName?: string
+ dependentName?: string
familyMobile?: string
+ dependentMobile?: string
relationship?: string
+ relation?: string
idCardNo?: string
+ dependentIdCardNo?: string
applyTime?: string
+ createdAt?: string
status?: string | number
statusText?: string
auditTime?: string
auditorName?: string
+ auditBy?: string
auditRemark?: string
+ proofFiles?: string | string[]
familyTalentId?: number | string
familyCardId?: number | string
cardId?: number | string
diff --git a/talent-card-admin/src/views/BusinessManagementView.vue b/talent-card-admin/src/views/BusinessManagementView.vue
index 324e906..a3e7f77 100644
--- a/talent-card-admin/src/views/BusinessManagementView.vue
+++ b/talent-card-admin/src/views/BusinessManagementView.vue
@@ -2,14 +2,17 @@
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Download, Edit, Plus, Refresh, Search, View } from '@element-plus/icons-vue'
-import { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
import {
createAdminRole,
createAdminStaff,
+ deleteAdminRole,
getAdminRoles,
getAdminStaff,
getUpgradeApplications,
updateAdminRole,
+ updateAdminRoleStatus,
updateAdminStaff,
} from '../api/admin'
import type { AdminRole, AdminStaff } from '../types/admin'
@@ -25,25 +28,54 @@
const rows = ref<Record<string, unknown>[]>([])
const dialogVisible = ref(false)
const editVisible = ref(false)
+const formRef = ref<FormInstance>()
const activeRow = ref<Record<string, unknown>>()
const editingId = ref<number | string>()
const filters = reactive({ keyword: '', cardLevel: '', status: '', timeRange: [] as string[] })
const pagination = reactive({ page: 1, size: 10, total: 0 })
const form = reactive({ name: '', code: '', password: '', remark: '', status: 1 })
+const accountPattern = /^[A-Za-z0-9]+$/
+const rules = computed<FormRules>(() => ({
+ name: [{ required: true, message: isRole.value ? '请输入角色名称' : '请输入员工姓名', trigger: 'blur' }],
+ code: [
+ { required: true, message: isRole.value ? '请输入角色编码' : '请输入登录账号', trigger: 'blur' },
+ {
+ validator: (_rule, value, callback) => !isRole.value && value && !accountPattern.test(String(value).trim())
+ ? callback(new Error('登录账号仅支持英文、数字或英文+数字'))
+ : callback(),
+ trigger: 'blur',
+ },
+ ],
+ password: [{
+ validator: (_rule, value, callback) => !isRole.value && !editingId.value && !value
+ ? callback(new Error('请设置登录密码'))
+ : callback(),
+ trigger: 'blur',
+ }],
+}))
+
+const filteredRows = computed(() => isRole.value
+ ? rows.value.filter((row) => (!filters.keyword || String(row.roleName || '').includes(filters.keyword)) && (!filters.status || String(row.status) === filters.status))
+ : rows.value)
+const pagedRows = computed(() => isUpgrade.value
+ ? rows.value
+ : filteredRows.value.slice((pagination.page - 1) * pagination.size, pagination.page * pagination.size))
+const pageTotal = computed(() => isRole.value ? filteredRows.value.length : pagination.total)
const columns = computed(() => isUpgrade.value
? [['applicationNo', '申请编号'], ['talentName', '持卡人名称'], ['talentMobile', '持卡人手机号'], ['targetLevel', '卡片等级'], ['applyTime', '申请时间'], ['status', '状态']]
: isRole.value
- ? [['roleCode', '角色编码'], ['roleName', '角色名称'], ['remark', '备注'], ['statusText', '状态'], ['createdAt', '创建时间']]
+ ? [['serialNo', '序号'], ['roleName', '角色名称'], ['remark', '备注'], ['statusText', '状态'], ['createdAt', '创建时间']]
: [['account', '登录账号'], ['displayName', '员工姓名'], ['roleCode', '所属角色'], ['statusText', '状态'], ['createdAt', '创建时间']])
function statusText(value: unknown) { return Number(value) === 1 ? '启用' : '停用' }
function formatDateTime(value: unknown) {
return typeof value === 'string' && value ? value.replace('T', ' ').slice(0, 19) : ''
}
-function normalizeRole(row: AdminRole) { return { ...row, createdAt: formatDateTime(row.createdAt), statusText: statusText(row.status) } }
+function normalizeRole(row: AdminRole, index: number) { return { ...row, serialNo: index + 1, createdAt: formatDateTime(row.createdAt), statusText: statusText(row.status) } }
function normalizeStaff(row: AdminStaff) { return { ...row, createdAt: formatDateTime(row.createdAt), statusText: statusText(row.status) } }
function normalizeUpgrade(row: Record<string, unknown>) { return { ...row, applyTime: formatDateTime(row.applyTime) } }
+function isPlatformAdminRole(row: Record<string, unknown>) { return String(row.roleCode || '') === 'platform_admin' }
async function loadData() {
loading.value = true
@@ -64,6 +96,7 @@
function search() { pagination.page = 1; loadData() }
function reset() { filters.keyword = ''; filters.cardLevel = ''; filters.status = ''; filters.timeRange = []; search() }
+function changePage() { if (isUpgrade.value) loadData() }
function view(row: Record<string, unknown>) {
if (isUpgrade.value) {
router.push(`/upgrade-applications/${row.id}`)
@@ -88,7 +121,8 @@
}
async function save() {
- if (!form.name.trim() || !form.code.trim()) { ElMessage.warning('请填写名称和编码/账号'); return }
+ const valid = await formRef.value?.validate().catch(() => false)
+ if (!valid) return
if (isRole.value) {
const payload = { roleName: form.name.trim(), roleCode: form.code.trim(), remark: form.remark.trim(), status: form.status }
if (editingId.value) await updateAdminRole(editingId.value, payload); else await createAdminRole(payload)
@@ -100,6 +134,20 @@
ElMessage.success('保存成功')
editVisible.value = false
loadData()
+}
+
+async function changeRoleStatus(row: Record<string, unknown>) {
+ const status = Number(row.status) === 1 ? 0 : 1
+ await updateAdminRoleStatus(row.id as number | string, status)
+ ElMessage.success(status === 1 ? '已启用' : '已停用')
+ await loadData()
+}
+
+async function removeRole(row: Record<string, unknown>) {
+ await ElMessageBox.confirm(`确认删除角色“${row.roleName}”?`, '删除角色', { type: 'warning' })
+ await deleteAdminRole(row.id as number | string)
+ ElMessage.success('已删除')
+ await loadData()
}
async function download() {
@@ -157,29 +205,33 @@
<el-button v-if="!isUpgrade" type="primary" :icon="Plus" @click="openCreate">新建</el-button>
<el-button :icon="Download" @click="download">下载</el-button>
</div>
- <el-table v-loading="loading" :data="rows" border class="business-fill-table" :empty-text="`暂无${title}数据`">
+ <el-table v-loading="loading" :data="pagedRows" border class="business-fill-table" :empty-text="`暂无${title}数据`">
<el-table-column v-for="[key, label] in columns" :key="key" :prop="key" :label="label" min-width="130" show-overflow-tooltip />
<el-table-column label="操作" width="150" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" :icon="View" @click="view(row)">查看</el-button>
<el-button v-if="!isUpgrade" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
+ <template v-if="isRole && !isPlatformAdminRole(row)">
+ <el-button link type="primary" @click="changeRoleStatus(row)">{{ Number(row.status) === 1 ? '停用' : '启用' }}</el-button>
+ <el-button link type="danger" @click="removeRole(row)">删除</el-button>
+ </template>
</template>
</el-table-column>
</el-table>
<div class="pagination-row">
- <el-pagination v-if="isUpgrade" v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" />
+ <el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" :total="pageTotal" @change="changePage" />
</div>
</section>
- <el-dialog v-model="dialogVisible" :title="title" width="520px">
+ <el-dialog v-model="dialogVisible" :title="title" width="520px" :close-on-click-modal="false">
<el-descriptions v-if="activeRow" :column="1" border>
<el-descriptions-item v-for="[key, label] in columns" :key="key" :label="label">{{ activeRow[key] || '-' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
- <el-dialog v-model="editVisible" :title="`编辑${title}`" width="480px" destroy-on-close>
- <el-form label-width="92px">
- <el-form-item :label="isRole ? '角色名称' : '员工姓名'"><el-input v-model="form.name" /></el-form-item>
- <el-form-item :label="isRole ? '角色编码' : '登录账号'"><el-input v-model="form.code" /></el-form-item>
- <el-form-item v-if="!isRole" label="登录密码"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item>
+ <el-dialog v-model="editVisible" :title="`编辑${title}`" width="480px" destroy-on-close :close-on-click-modal="false">
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="92px">
+ <el-form-item :label="isRole ? '角色名称' : '员工姓名'" prop="name"><el-input v-model="form.name" /></el-form-item>
+ <el-form-item :label="isRole ? '角色编码' : '登录账号'" prop="code"><el-input v-model="form.code" :placeholder="isRole ? '' : '仅支持英文和数字'" maxlength="32" /></el-form-item>
+ <el-form-item v-if="!isRole" label="登录密码" prop="password"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item>
<el-form-item v-if="isRole" label="备注"><el-input v-model="form.remark" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
</el-form>
diff --git a/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue b/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
index 01525ce..53e9997 100644
--- a/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
+++ b/talent-card-admin/src/views/FamilyCardApplicationDetailView.vue
@@ -29,20 +29,6 @@
return text(status)
}
-function formatFiles(value?: string | string[] | null) {
- if (!value) return [] as string[]
- if (Array.isArray(value)) return value.filter(Boolean)
- const raw = String(value).trim()
- if (!raw) return [] as string[]
- try {
- const parsed = JSON.parse(raw)
- if (Array.isArray(parsed)) return parsed.filter(Boolean).map(String)
- } catch {
- // ignore
- }
- return raw.split(/[;,\n]/).map(item => item.trim()).filter(Boolean)
-}
-
function familyName() {
return detail.value?.familyName || detail.value?.dependentName || '-'
}
@@ -86,7 +72,6 @@
{ label: '审核备注:', value: text(detail.value?.auditRemark) },
])
-const proofFiles = computed(() => formatFiles(detail.value?.proofFiles as unknown as string | string[] | null))
const isPending = computed(() => detail.value?.status === 0 || detail.value?.status === '0' || detail.value?.statusText === '待审核')
async function loadDetail() {
diff --git a/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyDetailResponse.java b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyDetailResponse.java
new file mode 100644
index 0000000..d04a138
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyDetailResponse.java
@@ -0,0 +1,46 @@
+package com.talentcard.server.response;
+
+import java.time.LocalDateTime;
+
+import com.talentcard.server.entity.DependentCardApply;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@Schema(name = "DependentCardApplyDetailResponse", description = "家属附属卡申请详情")
+public record DependentCardApplyDetailResponse(
+ Long id,
+ String applyNo,
+ String dependentName,
+ String dependentIdCardNo,
+ String dependentMobile,
+ String relation,
+ Integer status,
+ String statusText,
+ String auditRemark,
+ LocalDateTime applyTime
+) {
+ public static DependentCardApplyDetailResponse from(DependentCardApply apply) {
+ return new DependentCardApplyDetailResponse(
+ apply.getId(),
+ apply.getApplyNo(),
+ apply.getDependentName(),
+ apply.getDependentIdCardNo(),
+ apply.getDependentMobile(),
+ apply.getRelation(),
+ apply.getStatus(),
+ statusText(apply.getStatus()),
+ apply.getAuditRemark(),
+ apply.getApplyTime()
+ );
+ }
+
+ private static String statusText(Integer status) {
+ if (status == null) return "未知";
+ return switch (status) {
+ case DependentCardApply.STATUS_PENDING_REVIEW -> "待审核";
+ case DependentCardApply.STATUS_APPROVED -> "已通过";
+ case DependentCardApply.STATUS_REJECTED -> "已驳回";
+ case DependentCardApply.STATUS_CANCELLED -> "已取消";
+ default -> "未知";
+ };
+ }
+}
--
Gitblit v1.7.1