初始化2
This commit is contained in:
682
app/pages/developer/resources/git.vue
Normal file
682
app/pages/developer/resources/git.vue
Normal file
@@ -0,0 +1,682 @@
|
||||
<template>
|
||||
<div class="dev-page">
|
||||
<!-- 工具栏 -->
|
||||
<div class="page-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<h3 class="page-title">代码仓库</h3>
|
||||
<a-tag color="geekblue">{{ list.length }} 个</a-tag>
|
||||
<a-select
|
||||
v-model:value="selectedAppId"
|
||||
placeholder="全部应用"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
@change="loadList"
|
||||
>
|
||||
<a-select-option v-for="app in appOptions" :key="app.productId" :value="app.productId">
|
||||
{{ app.productName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<a-input-search
|
||||
v-model:value="searchText"
|
||||
placeholder="搜索仓库名称"
|
||||
style="width: 200px"
|
||||
allow-clear
|
||||
@search="loadList"
|
||||
/>
|
||||
<PermissionGuard :app-id="selectedAppId" permission="canEditResource" :show-tip="true">
|
||||
<a-button type="primary" @click="showAdd = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加仓库
|
||||
</a-button>
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 仓库卡片列表 -->
|
||||
<div v-if="loading" class="card-loading">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<div v-else-if="list.length === 0" class="card-empty">
|
||||
<a-empty description="暂无可访问的代码仓库">
|
||||
<template v-if="!selectedAppId || getAppPermission(selectedAppId)?.canEditResource">
|
||||
<a-button type="primary" @click="showAdd = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加仓库
|
||||
</a-button>
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
<div v-else class="git-grid">
|
||||
<div
|
||||
v-for="item in filteredList"
|
||||
:key="item.resourceId"
|
||||
class="git-card"
|
||||
>
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<div class="card-header-left">
|
||||
<div class="repo-icon">🐙</div>
|
||||
<div class="repo-info">
|
||||
<div class="repo-name">{{ item.name }}</div>
|
||||
<div class="repo-path">{{ item.gitPath }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button type="text" size="small">
|
||||
<template #icon><EllipsisOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }: any) => handleMenuAction(key, item)">
|
||||
<a-menu-item key="copy-https">
|
||||
<template #icon><CopyOutlined />复制 HTTPS</template>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="item.gitWebUrl" key="open-web">
|
||||
<template #icon><GlobalOutlined />网页访问</template>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="item.isOwner || getAppPermission(item.appId)?.canEditResource" key="edit">
|
||||
<template #icon><EditOutlined />编辑</template>
|
||||
</a-menu-item>
|
||||
<a-menu-divider v-if="item.isOwner || getAppPermission(item.appId)?.canEditResource" />
|
||||
<a-menu-item v-if="item.isOwner || getAppPermission(item.appId)?.canEditResource" key="delete" danger>
|
||||
<template #icon><DeleteOutlined />移除</template>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 卡片信息区 -->
|
||||
<div class="card-info">
|
||||
<!-- Clone URL -->
|
||||
<div class="clone-section">
|
||||
<div class="clone-label">
|
||||
<GithubOutlined /> Clone URL
|
||||
<a-tag v-if="item.gitAccessLevel" size="small" :color="accessLevelColor[item.gitAccessLevel]">
|
||||
{{ accessLevelLabel[item.gitAccessLevel] || item.gitAccessLevel }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="clone-url-row">
|
||||
<a-tooltip :title="item.gitCloneUrl || '未配置'">
|
||||
<span class="clone-url">{{ item.gitCloneUrl || '未配置' }}</span>
|
||||
</a-tooltip>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
:disabled="!item.gitCloneUrl"
|
||||
@click="copyCloneUrl(item)"
|
||||
>
|
||||
<template #icon><CopyOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用信息 -->
|
||||
<div v-if="item.appName" class="info-row">
|
||||
<span class="info-label"><AppstoreOutlined /></span>
|
||||
<span class="info-value">{{ item.appName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 权限提示 -->
|
||||
<div v-if="!item.isOwner" class="info-row access-hint">
|
||||
<LockOutlined style="color: #faad14; font-size: 12px;" />
|
||||
<span class="access-hint-text">{{ (item.accessLevel ?? 1) >= 2 ? '协作者(开发者)' : '协作者(只读)' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部快捷操作 -->
|
||||
<div class="card-footer">
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:disabled="!item.gitCloneUrl"
|
||||
@click="copyCloneUrl(item)"
|
||||
>
|
||||
<template #icon><CopyOutlined /></template>
|
||||
复制
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="item.gitWebUrl"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openWebUrl(item.gitWebUrl!)"
|
||||
>
|
||||
<template #icon><GlobalOutlined /></template>
|
||||
访问
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑仓库弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showAdd"
|
||||
:title="editingItem ? '编辑代码仓库' : '添加代码仓库'"
|
||||
:confirm-loading="saving"
|
||||
ok-text="保存"
|
||||
cancel-text="取消"
|
||||
:mask-closable="false"
|
||||
width="520px"
|
||||
@ok="handleSave"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
class="mt-4"
|
||||
>
|
||||
<a-form-item label="仓库名称" name="name">
|
||||
<a-input v-model:value="form.name" placeholder="如:Core Repository" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="所属应用" name="appId">
|
||||
<a-select
|
||||
v-model:value="form.appId"
|
||||
placeholder="请选择所属应用"
|
||||
:loading="loadingApps"
|
||||
>
|
||||
<a-select-option v-for="app in appOptions" :key="app.productId" :value="app.productId">
|
||||
{{ app.productName }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="仓库路径" name="gitPath">
|
||||
<a-input v-model:value="form.gitPath" placeholder="如:websopy/core" />
|
||||
<div class="form-tip">Gitea 上的仓库路径,格式:用户名/仓库名</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Clone URL" name="gitCloneUrl">
|
||||
<a-input v-model:value="form.gitCloneUrl" placeholder="https://git.websoft.top/websopy/core.git" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Web 访问地址" name="gitWebUrl">
|
||||
<a-input v-model:value="form.gitWebUrl" placeholder="https://git.websoft.top/websopy/core" />
|
||||
<div class="form-tip">Gitea 网页访问地址,用于快速跳转到仓库页面</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="访问权限" name="gitAccessLevel">
|
||||
<a-select v-model:value="form.gitAccessLevel">
|
||||
<a-select-option value="read">只读 (read)</a-select-option>
|
||||
<a-select-option value="write">读写 (write)</a-select-option>
|
||||
<a-select-option value="admin">管理员 (admin)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="form.remark" :rows="2" placeholder="可选备注信息" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showDelete"
|
||||
title="确认删除"
|
||||
:confirm-loading="deleting"
|
||||
ok-text="确认删除"
|
||||
cancel-text="取消"
|
||||
ok-type="danger"
|
||||
@ok="handleDelete"
|
||||
>
|
||||
<p>确定要删除仓库 <strong>{{ deletingItem?.name }}</strong> 吗?此操作不可恢复。</p>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
EllipsisOutlined,
|
||||
CopyOutlined,
|
||||
GlobalOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
LockOutlined,
|
||||
AppstoreOutlined,
|
||||
GithubOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { pageAppResource, addAppResource, updateAppResource, removeAppResource } from '@/api/app/appResource'
|
||||
import { getMyAccessibleApps } from '@/api/app/appProduct'
|
||||
import type { AppResource } from '@/api/app/appResource/model'
|
||||
import { enrichResourcesWithPermission } from '@/composables/useResourceAccess'
|
||||
import { useAppPermission } from '@/composables/useAppPermission'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
definePageMeta({ layout: 'developer' })
|
||||
useHead({ title: '代码仓库 - 开发者中心' })
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const loadingApps = ref(false)
|
||||
const showAdd = ref(false)
|
||||
const showDelete = ref(false)
|
||||
const editingItem = ref<AppResource | null>(null)
|
||||
const deletingItem = ref<AppResource | null>(null)
|
||||
const searchText = ref('')
|
||||
const selectedAppId = ref<number | undefined>()
|
||||
const appOptions = ref<any[]>([])
|
||||
|
||||
const list = ref<AppResource[]>([])
|
||||
|
||||
const formRef = ref()
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
appId: undefined as number | undefined,
|
||||
gitPath: '',
|
||||
gitCloneUrl: '',
|
||||
gitWebUrl: '',
|
||||
gitAccessLevel: 'read',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入仓库名称', trigger: 'blur' }],
|
||||
appId: [{ required: true, message: '请选择所属应用', trigger: 'change' }],
|
||||
gitPath: [{ required: true, message: '请输入仓库路径', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const accessLevelLabel: Record<string, string> = {
|
||||
read: '只读',
|
||||
write: '读写',
|
||||
admin: '管理员',
|
||||
}
|
||||
|
||||
const accessLevelColor: Record<string, string> = {
|
||||
read: 'blue',
|
||||
write: 'green',
|
||||
admin: 'purple',
|
||||
}
|
||||
|
||||
// 权限控制
|
||||
const { getAppPermission } = useAppPermission()
|
||||
|
||||
// 过滤后的列表(支持搜索)
|
||||
const filteredList = computed(() => {
|
||||
if (!searchText.value) return list.value
|
||||
const keyword = searchText.value.toLowerCase()
|
||||
return list.value.filter(item =>
|
||||
item.name?.toLowerCase().includes(keyword) ||
|
||||
item.gitPath?.toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
// 加载仓库列表
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await pageAppResource({
|
||||
page: 1,
|
||||
limit: 100,
|
||||
resourceType: 'git',
|
||||
appId: selectedAppId.value,
|
||||
})
|
||||
list.value = enrichResourcesWithPermission(result?.list ?? [])
|
||||
} catch (e) {
|
||||
console.error('加载仓库列表失败', e)
|
||||
message.error('加载仓库列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载应用列表
|
||||
async function loadAppOptions() {
|
||||
loadingApps.value = true
|
||||
try {
|
||||
appOptions.value = await getMyAccessibleApps()
|
||||
} catch {
|
||||
appOptions.value = []
|
||||
} finally {
|
||||
loadingApps.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制 Clone URL
|
||||
async function copyCloneUrl(item: AppResource) {
|
||||
if (!item.gitCloneUrl) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(item.gitCloneUrl)
|
||||
message.success('已复制 Clone URL')
|
||||
} catch {
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
// 打开 Web URL
|
||||
function openWebUrl(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 菜单操作处理
|
||||
function handleMenuAction(key: string, item: AppResource) {
|
||||
switch (key) {
|
||||
case 'copy-https':
|
||||
copyCloneUrl(item)
|
||||
break
|
||||
case 'open-web':
|
||||
if (item.gitWebUrl) openWebUrl(item.gitWebUrl)
|
||||
break
|
||||
case 'edit':
|
||||
openEditModal(item)
|
||||
break
|
||||
case 'delete':
|
||||
openDeleteConfirm(item)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑弹窗
|
||||
function openEditModal(item: AppResource) {
|
||||
editingItem.value = item
|
||||
Object.assign(form, {
|
||||
name: item.name,
|
||||
appId: item.appId,
|
||||
gitPath: item.gitPath,
|
||||
gitCloneUrl: item.gitCloneUrl,
|
||||
gitWebUrl: item.gitWebUrl,
|
||||
gitAccessLevel: item.gitAccessLevel || 'read',
|
||||
remark: item.remark,
|
||||
})
|
||||
showAdd.value = true
|
||||
}
|
||||
|
||||
// 打开删除确认弹窗
|
||||
function openDeleteConfirm(item: AppResource) {
|
||||
deletingItem.value = item
|
||||
showDelete.value = true
|
||||
}
|
||||
|
||||
// 保存仓库
|
||||
async function handleSave() {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const data: Partial<AppResource> = {
|
||||
resourceType: 'git',
|
||||
name: form.name,
|
||||
appId: form.appId,
|
||||
gitPath: form.gitPath,
|
||||
gitCloneUrl: form.gitCloneUrl,
|
||||
gitWebUrl: form.gitWebUrl,
|
||||
gitAccessLevel: form.gitAccessLevel,
|
||||
remark: form.remark,
|
||||
status: 'running',
|
||||
}
|
||||
|
||||
if (editingItem.value?.resourceId) {
|
||||
data.resourceId = editingItem.value.resourceId
|
||||
await updateAppResource(data)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await addAppResource(data)
|
||||
message.success('添加成功')
|
||||
}
|
||||
|
||||
showAdd.value = false
|
||||
resetForm()
|
||||
await loadList()
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除仓库
|
||||
async function handleDelete() {
|
||||
if (!deletingItem.value?.resourceId) return
|
||||
|
||||
deleting.value = true
|
||||
try {
|
||||
await removeAppResource(deletingItem.value.resourceId)
|
||||
message.success('删除成功')
|
||||
showDelete.value = false
|
||||
deletingItem.value = null
|
||||
await loadList()
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '删除失败')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
editingItem.value = null
|
||||
Object.assign(form, {
|
||||
name: '',
|
||||
appId: undefined,
|
||||
gitPath: '',
|
||||
gitCloneUrl: '',
|
||||
gitWebUrl: '',
|
||||
gitAccessLevel: 'read',
|
||||
remark: '',
|
||||
})
|
||||
}
|
||||
|
||||
// 监听弹窗关闭,重置表单
|
||||
watch(showAdd, (val) => {
|
||||
if (!val) resetForm()
|
||||
})
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
loadAppOptions()
|
||||
loadList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dev-page {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.page-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 卡片网格 */
|
||||
.git-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.git-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.git-card:hover {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 2px 12px rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.card-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.repo-icon {
|
||||
font-size: 28px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #f0f5ff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.repo-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.repo-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.repo-path {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 卡片信息区 */
|
||||
.card-info {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.clone-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.clone-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.clone-url-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.clone-url {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clone-url:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.access-hint {
|
||||
background: #fffbe6;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.access-hint-text {
|
||||
font-size: 12px;
|
||||
color: #7c4a00;
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.card-empty,
|
||||
.card-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 24px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 表单提示 */
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user