feat(gitea): 新增 Gitea 对接页面(申请/改密/头像迁移/管理)并禁用接口 HTTP 缓存
This commit is contained in:
1 parent
f428afa495
commit
a813e34a4f
5 files changed
+460
No files matched your search
@@ -0,0 +1,69 @@
|
||||
import { get, post } from './request'
|
||||
|
||||
/** Gitea 连接配置 */
|
||||
export interface GiteaSessionConfig {
|
||||
BaseUrl: string
|
||||
Org: string
|
||||
OwnerRole: string
|
||||
}
|
||||
|
||||
/** 我的 Gitea 账号信息 */
|
||||
export interface GiteaAccountInfo {
|
||||
GiteaLogin: string
|
||||
Email: string
|
||||
Status: number
|
||||
HomeUrl: string
|
||||
Org: string
|
||||
CreateTime: string | null
|
||||
}
|
||||
|
||||
/** 我的 Gitea 接口响应 */
|
||||
export interface MyGiteaResult {
|
||||
Account: GiteaAccountInfo | null
|
||||
Session: GiteaSessionConfig
|
||||
}
|
||||
|
||||
/** 管理员 Gitea 账号列表项 */
|
||||
export interface GiteaAdminItem {
|
||||
UserGiteaAccountUUID: string
|
||||
UserName: string
|
||||
GiteaLogin: string
|
||||
Email: string
|
||||
Status: number
|
||||
CreateTime: string | null
|
||||
}
|
||||
|
||||
/** 申请结果 */
|
||||
export interface GiteaApplyResult {
|
||||
GiteaLogin: string
|
||||
Org: string
|
||||
IsOwner: boolean
|
||||
Claimed: boolean
|
||||
Email: string
|
||||
}
|
||||
|
||||
/** Gitea 对接 API 客户端(与"我的邮箱"同模式:请求全部走后端,管理 Token 存服务端) */
|
||||
export const giteaApi = {
|
||||
/** 获取 Gitea 连接配置 */
|
||||
session: (): Promise<GiteaSessionConfig> => get<GiteaSessionConfig>('/API/Gitea/Session'),
|
||||
|
||||
/** 我的 Gitea 账号信息 */
|
||||
myAccount: (): Promise<MyGiteaResult> => get<MyGiteaResult>('/API/Gitea/MyAccount'),
|
||||
|
||||
/** 申请 Gitea 账号(自选用户名 + 自设密码,创建后加入组织,负责人自动成为 Owner) */
|
||||
apply: (data: { GiteaLogin: string; Password: string }): Promise<GiteaApplyResult> =>
|
||||
post<GiteaApplyResult>('/API/Gitea/Apply', data),
|
||||
|
||||
/** 修改 Gitea 账号密码(管理员 API,无需旧密码) */
|
||||
changePassword: (NewPassword: string): Promise<void> => post<void>('/API/Gitea/ChangePassword', { NewPassword }),
|
||||
|
||||
/** 一键迁移 GERP 头像到 Gitea(需输入 Gitea 密码,Basic Auth 认证,密码仅此一次传输) */
|
||||
uploadAvatar: (Password: string): Promise<void> => post<void>('/API/Gitea/Avatar', { Password }),
|
||||
|
||||
/** 管理员:全部账号列表 */
|
||||
adminList: (): Promise<{ list: GiteaAdminItem[]; total: number }> =>
|
||||
get<{ list: GiteaAdminItem[]; total: number }>('/API/Gitea/AdminList'),
|
||||
|
||||
/** 管理员:禁用账号(删除 Gitea 用户) */
|
||||
disable: (UserGiteaAccountUUID: string): Promise<void> => post<void>('/API/Gitea/Disable', { UserGiteaAccountUUID }),
|
||||
}
|
||||
@@ -50,6 +50,10 @@ const request: AxiosInstance = axios.create({
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
config.headers['X-User-Timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai'
|
||||
// 动态接口禁止 HTTP 缓存:后端接口均无 Cache-Control,浏览器可能启发式缓存
|
||||
// (如菜单树变更后刷新仍返回旧数据),统一加 no-cache 头保证实时性
|
||||
config.headers['Cache-Control'] = 'no-cache'
|
||||
config.headers['Pragma'] = 'no-cache'
|
||||
// 合并 server 作用域参数到 query(不覆盖调用方已显式传入的同名参数)
|
||||
const scopeParams = getServerScopeParams()
|
||||
if (Object.keys(scopeParams).length) {
|
||||
|
||||
@@ -186,6 +186,19 @@ export const staticRoutes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/mail/MailAdmin.vue'),
|
||||
meta: { title: '邮箱管理', icon: 'Message', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
// === Gitea 代码托管对接 ===
|
||||
{
|
||||
path: 'gitea/mine',
|
||||
name: 'GiteaMine',
|
||||
component: () => import('@/views/gitea/Gitea.vue'),
|
||||
meta: { title: '我的 Gitea', icon: 'Connection', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'gitea/admin',
|
||||
name: 'GiteaAdmin',
|
||||
component: () => import('@/views/gitea/GiteaAdmin.vue'),
|
||||
meta: { title: 'Gitea 账号管理', icon: 'Connection', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'system/tenant-create',
|
||||
name: 'SystemTenantCreate',
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 我的 Gitea(员工端)
|
||||
*
|
||||
* 与"我的邮箱"同模式:未申请 → 申请弹窗(自选用户名 + 自设密码);
|
||||
* 已申请 → 展示账号信息(用户名/主页/所属组织)。请求全部走后端,
|
||||
* 管理 Token 存服务端 secrets.ini,前端不接触明文凭据。
|
||||
*
|
||||
* 接口:
|
||||
* - GET /API/Gitea/MyAccount → { Account, Session }
|
||||
* - POST /API/Gitea/Apply → body { GiteaLogin, Password }
|
||||
* - POST /API/Gitea/ChangePassword → body { NewPassword }
|
||||
* - POST /API/Gitea/Avatar → body { Password }(一键迁移 GERP 头像)
|
||||
*/
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { giteaApi, type GiteaAccountInfo, type GiteaSessionConfig } from '@/api/gitea'
|
||||
|
||||
const loading = ref(true)
|
||||
const account = ref<GiteaAccountInfo | null>(null)
|
||||
const session = ref<GiteaSessionConfig | null>(null)
|
||||
|
||||
const applyVisible = ref(false)
|
||||
const applying = ref(false)
|
||||
const applyForm = reactive({ GiteaLogin: '', Password: '', ConfirmPassword: '' })
|
||||
|
||||
const changePwdVisible = ref(false)
|
||||
const changingPwd = ref(false)
|
||||
const changePwdForm = reactive({ NewPassword: '', ConfirmPassword: '' })
|
||||
|
||||
const avatarVisible = ref(false)
|
||||
const uploadingAvatar = ref(false)
|
||||
const avatarForm = reactive({ Password: '' })
|
||||
|
||||
async function init(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await giteaApi.myAccount()
|
||||
account.value = result.Account
|
||||
session.value = result.Session
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(init)
|
||||
|
||||
async function submitApply(): Promise<void> {
|
||||
const f = applyForm
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{2,38}$/.test(f.GiteaLogin)) {
|
||||
ElMessage.warning('用户名需为字母/数字/点/下划线/连字符,长度 3~39')
|
||||
return
|
||||
}
|
||||
if (!/^(?=.*[A-Za-z])(?=.*\d).{8,64}$/.test(f.Password)) {
|
||||
ElMessage.warning('密码需 8~64 位且同时包含字母和数字')
|
||||
return
|
||||
}
|
||||
if (f.Password !== f.ConfirmPassword) {
|
||||
ElMessage.warning('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
applying.value = true
|
||||
try {
|
||||
const result = await giteaApi.apply({ GiteaLogin: f.GiteaLogin, Password: f.Password })
|
||||
ElMessage.success(
|
||||
result.Claimed
|
||||
? `已认领 Gitea 账号 ${result.GiteaLogin}(绑定工作邮箱 ${result.Email})`
|
||||
: `Gitea 账号 ${result.GiteaLogin} 申请成功,已加入组织 ${result.Org}`,
|
||||
)
|
||||
applyVisible.value = false
|
||||
applyForm.GiteaLogin = ''
|
||||
applyForm.Password = ''
|
||||
applyForm.ConfirmPassword = ''
|
||||
await init()
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改 Gitea 密码(管理员 API,无需旧密码) */
|
||||
async function submitChangePassword(): Promise<void> {
|
||||
const f = changePwdForm
|
||||
if (!/^(?=.*[A-Za-z])(?=.*\d).{8,64}$/.test(f.NewPassword)) {
|
||||
ElMessage.warning('密码需 8~64 位且同时包含字母和数字')
|
||||
return
|
||||
}
|
||||
if (f.NewPassword !== f.ConfirmPassword) {
|
||||
ElMessage.warning('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
changingPwd.value = true
|
||||
try {
|
||||
await giteaApi.changePassword(f.NewPassword)
|
||||
ElMessage.success('Gitea 密码修改成功')
|
||||
changePwdVisible.value = false
|
||||
changePwdForm.NewPassword = ''
|
||||
changePwdForm.ConfirmPassword = ''
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
changingPwd.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 一键迁移 GERP 头像到 Gitea(输入 Gitea 密码做 Basic Auth,密码仅此一次传输) */
|
||||
async function submitUploadAvatar(): Promise<void> {
|
||||
if (!avatarForm.Password) {
|
||||
ElMessage.warning('请输入 Gitea 密码')
|
||||
return
|
||||
}
|
||||
uploadingAvatar.value = true
|
||||
try {
|
||||
await giteaApi.uploadAvatar(avatarForm.Password)
|
||||
ElMessage.success('头像迁移成功,请在 Gitea 刷新查看')
|
||||
avatarVisible.value = false
|
||||
avatarForm.Password = ''
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
uploadingAvatar.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gitea-page">
|
||||
<div v-loading="loading" class="gitea-container">
|
||||
<!-- 未申请 -->
|
||||
<el-empty v-if="!loading && !account" description="您还没有 Gitea 账号">
|
||||
<el-button type="primary" @click="applyVisible = true">申请 Gitea 账号</el-button>
|
||||
</el-empty>
|
||||
|
||||
<!-- 已申请:账号信息卡片 -->
|
||||
<template v-else-if="account">
|
||||
<el-card shadow="never" class="account-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>我的 Gitea 账号</span>
|
||||
<el-tag :type="account.Status === 1 ? 'success' : 'danger'">
|
||||
{{ account.Status === 1 ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="用户名">
|
||||
<b>{{ account.GiteaLogin }}</b>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="工作邮箱">{{ account.Email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属组织">{{ account.Org }}</el-descriptions-item>
|
||||
<el-descriptions-item label="个人主页">
|
||||
<a :href="account.HomeUrl" target="_blank" rel="noopener" class="home-link">
|
||||
{{ account.HomeUrl }} <i class="el-icon"><el-icon><Link /></el-icon></i>
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ account.CreateTime ? new Date(account.CreateTime).toLocaleString('zh-CN', { hour12: false }) : '' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="account-tip">
|
||||
可在 <a :href="session?.BaseUrl || ''" target="_blank" rel="noopener">{{ session?.BaseUrl || '' }}</a>
|
||||
使用该用户名与密码直接登录,进行代码托管操作。
|
||||
</div>
|
||||
<div class="account-actions">
|
||||
<el-button :icon="'Key'" @click="changePwdVisible = true">修改密码</el-button>
|
||||
<el-button :icon="'Picture'" type="primary" plain @click="avatarVisible = true">一键迁移 GERP 头像</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 申请弹窗 -->
|
||||
<el-dialog v-model="applyVisible" title="申请 Gitea 账号" width="420px">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="applyForm.GiteaLogin" placeholder="如 zhangsan(3~39 位)" maxlength="39" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="applyForm.Password" type="password" show-password placeholder="8~64 位,含字母和数字" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码">
|
||||
<el-input v-model="applyForm.ConfirmPassword" type="password" show-password placeholder="再次输入密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="apply-tip">
|
||||
申请成功后自动加入组织 {{ session?.Org || 'GVSDS' }},绑定您的<b>工作邮箱(我的邮箱)</b>;
|
||||
未申请企业邮箱请先到「我的邮箱」申请。
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="applyVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="applying" @click="submitApply">申请</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<el-dialog v-model="changePwdVisible" title="修改 Gitea 密码" width="400px">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="changePwdForm.NewPassword" type="password" show-password placeholder="8~64 位,含字母和数字" />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码">
|
||||
<el-input v-model="changePwdForm.ConfirmPassword" type="password" show-password placeholder="再次输入新密码" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="changePwdVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="changingPwd" @click="submitChangePassword">确认修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 头像迁移弹窗 -->
|
||||
<el-dialog v-model="avatarVisible" title="一键迁移 GERP 头像" width="420px">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="Gitea 密码">
|
||||
<el-input v-model="avatarForm.Password" type="password" show-password placeholder="用于认证您的 Gitea 账号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="apply-tip">
|
||||
将您的 GERP 头像上传到 Gitea;密码仅此一次传输,用于认证,不落库。
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="avatarVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="uploadingAvatar" @click="submitUploadAvatar">开始迁移</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gitea-page {
|
||||
padding: 16px;
|
||||
}
|
||||
.gitea-container {
|
||||
min-height: 320px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.account-card {
|
||||
max-width: 640px;
|
||||
}
|
||||
.home-link {
|
||||
color: var(--el-color-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
.account-tip {
|
||||
margin-top: 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.account-tip a {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.account-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.apply-tip {
|
||||
margin-top: -6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Gitea 账号管理(管理员)
|
||||
*
|
||||
* 展示所有员工 Gitea 账号(用户名/账号/状态/创建时间),支持禁用(删除 Gitea 用户)。
|
||||
*
|
||||
* 接口:
|
||||
* - GET /API/Gitea/AdminList → { list, total }
|
||||
* - POST /API/Gitea/Disable → body { UserGiteaAccountUUID }
|
||||
*/
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { giteaApi, type GiteaAdminItem } from '@/api/gitea'
|
||||
|
||||
const loading = ref(true)
|
||||
const rows = ref<GiteaAdminItem[]>([])
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await giteaApi.adminList()
|
||||
rows.value = result.list || []
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function disable(row: GiteaAdminItem): Promise<void> {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定禁用 Gitea 账号 ${row.GiteaLogin} 吗?禁用后该账号将无法登录 Gitea。`, '禁用确认', {
|
||||
type: 'warning',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await giteaApi.disable(row.UserGiteaAccountUUID)
|
||||
ElMessage.success('已禁用')
|
||||
await load()
|
||||
} catch {
|
||||
// 拦截器已提示
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gitea-admin">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Gitea 账号管理</span>
|
||||
<el-button :icon="'Refresh'" @click="load">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="rows">
|
||||
<el-table-column prop="UserName" label="用户名" min-width="140" />
|
||||
<el-table-column prop="GiteaLogin" label="Gitea 账号" min-width="150" />
|
||||
<el-table-column prop="Email" label="工作邮箱" min-width="200">
|
||||
<template #default="{ row }">{{ row.Email || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.Status === 1 ? 'success' : 'danger'">{{ row.Status === 1 ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="CreateTime" label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.CreateTime ? new Date(row.CreateTime).toLocaleString('zh-CN', { hour12: false }) : '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.Status === 1" text type="danger" size="small" @click="disable(row as GiteaAdminItem)">禁用</el-button>
|
||||
<span v-else class="disabled-text">已禁用</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无 Gitea 账号" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gitea-admin {
|
||||
padding: 16px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.disabled-text {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user