feat: 租户应用订阅支持批量添加全部应用
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Delete, Plus, CopyDocument } from '@element-plus/icons-vue'
|
import { Delete, Plus, CopyDocument, Grid } from '@element-plus/icons-vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
@@ -126,6 +126,41 @@ const tokenDialogVisible = ref(false)
|
|||||||
const currentToken = ref('')
|
const currentToken = ref('')
|
||||||
const currentAppUrl = ref('')
|
const currentAppUrl = ref('')
|
||||||
|
|
||||||
|
// 批量添加对话框
|
||||||
|
const batchDialogVisible = ref(false)
|
||||||
|
const batchLoading = ref(false)
|
||||||
|
const batchForm = reactive({
|
||||||
|
tenant_id: '',
|
||||||
|
selected_apps: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取租户尚未订阅的应用列表
|
||||||
|
const availableAppsForBatch = computed(() => {
|
||||||
|
if (!batchForm.tenant_id) return appList.value
|
||||||
|
|
||||||
|
// 获取该租户已订阅的应用
|
||||||
|
const subscribedApps = new Set(
|
||||||
|
tableData.value
|
||||||
|
.filter(row => row.tenant_id === batchForm.tenant_id)
|
||||||
|
.map(row => row.app_code)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 返回未订阅的应用
|
||||||
|
return appList.value.filter(app => !subscribedApps.has(app.app_code))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否全选
|
||||||
|
const isAllSelected = computed(() => {
|
||||||
|
return availableAppsForBatch.value.length > 0 &&
|
||||||
|
batchForm.selected_apps.length === availableAppsForBatch.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否部分选中
|
||||||
|
const isIndeterminate = computed(() => {
|
||||||
|
return batchForm.selected_apps.length > 0 &&
|
||||||
|
batchForm.selected_apps.length < availableAppsForBatch.value.length
|
||||||
|
})
|
||||||
|
|
||||||
async function fetchTenants() {
|
async function fetchTenants() {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/api/tenants', { params: { size: 1000 } })
|
const res = await api.get('/api/tenants', { params: { size: 1000 } })
|
||||||
@@ -390,6 +425,123 @@ async function copyTokenLink(row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开批量添加对话框
|
||||||
|
function handleBatchCreate() {
|
||||||
|
batchForm.tenant_id = ''
|
||||||
|
batchForm.selected_apps = []
|
||||||
|
batchDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听批量添加租户变化
|
||||||
|
watch(() => batchForm.tenant_id, () => {
|
||||||
|
batchForm.selected_apps = []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 全选/取消全选
|
||||||
|
function toggleSelectAll(checked) {
|
||||||
|
if (checked) {
|
||||||
|
batchForm.selected_apps = availableAppsForBatch.value.map(app => app.app_code)
|
||||||
|
} else {
|
||||||
|
batchForm.selected_apps = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量创建订阅
|
||||||
|
async function handleBatchSubmit() {
|
||||||
|
if (!batchForm.tenant_id) {
|
||||||
|
ElMessage.warning('请选择租户')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (batchForm.selected_apps.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一个应用')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
batchLoading.value = true
|
||||||
|
const results = { success: 0, failed: 0 }
|
||||||
|
const createdTokens = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const appCode of batchForm.selected_apps) {
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/tenant-apps', {
|
||||||
|
tenant_id: batchForm.tenant_id,
|
||||||
|
app_code: appCode,
|
||||||
|
app_name: '',
|
||||||
|
custom_configs: []
|
||||||
|
})
|
||||||
|
results.success++
|
||||||
|
|
||||||
|
// 收集生成的 token 信息
|
||||||
|
if (res.data.access_token) {
|
||||||
|
createdTokens.push({
|
||||||
|
app_code: appCode,
|
||||||
|
app_name: appMap.value[appCode] || appCode,
|
||||||
|
token: res.data.access_token,
|
||||||
|
base_url: appBaseUrl.value[appCode] || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
results.failed++
|
||||||
|
console.error(`创建 ${appCode} 订阅失败:`, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.success > 0) {
|
||||||
|
ElMessage.success(`成功创建 ${results.success} 个订阅${results.failed > 0 ? `,${results.failed} 个失败` : ''}`)
|
||||||
|
batchDialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
|
||||||
|
// 如果有创建成功的,显示批量 Token 结果
|
||||||
|
if (createdTokens.length > 0) {
|
||||||
|
showBatchTokens(createdTokens)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error('创建失败,可能应用已被订阅')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
batchLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量 Token 显示
|
||||||
|
const batchTokenDialogVisible = ref(false)
|
||||||
|
const batchTokenList = ref([])
|
||||||
|
|
||||||
|
function showBatchTokens(tokens) {
|
||||||
|
batchTokenList.value = tokens
|
||||||
|
batchTokenDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制单个 Token 链接
|
||||||
|
function copyBatchTokenLink(item) {
|
||||||
|
if (!item.base_url) {
|
||||||
|
ElMessage.warning('该应用未配置访问地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const url = `${item.base_url}?token=${item.token}`
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
ElMessage.success(`${item.app_name} 链接已复制`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制所有 Token 链接
|
||||||
|
function copyAllTokenLinks() {
|
||||||
|
const links = batchTokenList.value
|
||||||
|
.filter(item => item.base_url)
|
||||||
|
.map(item => `${item.app_name}: ${item.base_url}?token=${item.token}`)
|
||||||
|
.join('\n\n')
|
||||||
|
|
||||||
|
if (!links) {
|
||||||
|
ElMessage.warning('没有可复制的链接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(links).then(() => {
|
||||||
|
ElMessage.success('所有链接已复制')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchTenants()
|
fetchTenants()
|
||||||
fetchApps()
|
fetchApps()
|
||||||
@@ -401,11 +553,17 @@ onMounted(() => {
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="title">租户应用订阅</div>
|
<div class="title">租户应用订阅</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button v-if="authStore.isOperator" type="success" @click="handleBatchCreate">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
批量添加
|
||||||
|
</el-button>
|
||||||
<el-button v-if="authStore.isOperator" type="primary" @click="handleCreate">
|
<el-button v-if="authStore.isOperator" type="primary" @click="handleCreate">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-icon><Plus /></el-icon>
|
||||||
新建订阅
|
新建订阅
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="page-tip">
|
<div class="page-tip">
|
||||||
<el-alert type="info" :closable="false">
|
<el-alert type="info" :closable="false">
|
||||||
@@ -730,6 +888,119 @@ onMounted(() => {
|
|||||||
<el-button @click="tokenDialogVisible = false">关闭</el-button>
|
<el-button @click="tokenDialogVisible = false">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 批量添加对话框 -->
|
||||||
|
<el-dialog v-model="batchDialogVisible" title="批量添加应用订阅" width="600px">
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="选择租户" required>
|
||||||
|
<el-select
|
||||||
|
v-model="batchForm.tenant_id"
|
||||||
|
placeholder="请选择租户"
|
||||||
|
filterable
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="tenant in tenantList"
|
||||||
|
:key="tenant.code"
|
||||||
|
:label="`${tenant.name} (${tenant.code})`"
|
||||||
|
:value="tenant.code"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="选择应用" required>
|
||||||
|
<div class="batch-apps-container">
|
||||||
|
<div class="batch-apps-header">
|
||||||
|
<el-checkbox
|
||||||
|
:model-value="isAllSelected"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
@change="toggleSelectAll"
|
||||||
|
:disabled="!batchForm.tenant_id || availableAppsForBatch.length === 0"
|
||||||
|
>
|
||||||
|
全选 ({{ batchForm.selected_apps.length }}/{{ availableAppsForBatch.length }})
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty
|
||||||
|
v-if="!batchForm.tenant_id"
|
||||||
|
description="请先选择租户"
|
||||||
|
:image-size="60"
|
||||||
|
/>
|
||||||
|
<el-empty
|
||||||
|
v-else-if="availableAppsForBatch.length === 0"
|
||||||
|
description="该租户已订阅所有应用"
|
||||||
|
:image-size="60"
|
||||||
|
/>
|
||||||
|
<el-checkbox-group
|
||||||
|
v-else
|
||||||
|
v-model="batchForm.selected_apps"
|
||||||
|
class="batch-apps-list"
|
||||||
|
>
|
||||||
|
<el-checkbox
|
||||||
|
v-for="app in availableAppsForBatch"
|
||||||
|
:key="app.app_code"
|
||||||
|
:value="app.app_code"
|
||||||
|
class="batch-app-item"
|
||||||
|
>
|
||||||
|
{{ app.app_name }}
|
||||||
|
<span class="app-code">({{ app.app_code }})</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="batchDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="batchLoading"
|
||||||
|
:disabled="!batchForm.tenant_id || batchForm.selected_apps.length === 0"
|
||||||
|
@click="handleBatchSubmit"
|
||||||
|
>
|
||||||
|
创建 {{ batchForm.selected_apps.length }} 个订阅
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 批量 Token 结果对话框 -->
|
||||||
|
<el-dialog v-model="batchTokenDialogVisible" title="批量创建成功" width="700px">
|
||||||
|
<el-alert type="success" :closable="false" style="margin-bottom: 16px">
|
||||||
|
已成功创建 {{ batchTokenList.length }} 个应用订阅,以下是访问链接:
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<div class="batch-token-list">
|
||||||
|
<div
|
||||||
|
v-for="item in batchTokenList"
|
||||||
|
:key="item.app_code"
|
||||||
|
class="batch-token-item"
|
||||||
|
>
|
||||||
|
<div class="batch-token-info">
|
||||||
|
<span class="batch-token-name">{{ item.app_name }}</span>
|
||||||
|
<el-tag v-if="!item.base_url" type="warning" size="small">无链接</el-tag>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.base_url" class="batch-token-url">
|
||||||
|
{{ item.base_url }}?token={{ item.token.slice(0, 20) }}...
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
v-if="item.base_url"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="copyBatchTokenLink(item)"
|
||||||
|
>
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="copyAllTokenLinks" type="success">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
复制全部链接
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="batchTokenDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -775,6 +1046,94 @@ onMounted(() => {
|
|||||||
color: #303133;
|
color: #303133;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 批量添加对话框样式 */
|
||||||
|
.batch-apps-container {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-apps-header {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-apps-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-app-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-app-item:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-code {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 批量 Token 结果样式 */
|
||||||
|
.batch-token-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-token-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-token-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-token-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-token-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-token-url {
|
||||||
|
flex: 1;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.token-dialog-content {
|
.token-dialog-content {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user