feat(gitea): 新增 Gitea 对接页面(申请/改密/头像迁移/管理)并禁用接口 HTTP 缓存

This commit is contained in:
TermiNexus committed 2026-08-09 18:28:55 +08:00
1 parent f428afa495
commit a813e34a4f
5 files changed
+460

No files matched your search

+69
View File
@@ -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 }),
}
+4
View File
@@ -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) {
+13
View File
@@ -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',
+272
View File
@@ -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>
+102
View File
@@ -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>