fix: 恢复租户应用订阅页面被误删的功能
All checks were successful
continuous-integration/drone/push Build is passing

恢复在 104487f 提交中被误删的代码,包括:
- 批量添加应用订阅功能
- 租户标签快速筛选
- 应用配置 Schema 支持
- 自定义配置管理
- 批量 Token 结果显示
This commit is contained in:
2026-01-29 16:24:27 +08:00
parent b0f7d1ba9e
commit 158481ff75

View File

@@ -1,6 +1,7 @@
<script setup>
import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Plus, CopyDocument, Grid } from '@element-plus/icons-vue'
import api from '@/api'
import { useAuthStore } from '@/stores/auth'
@@ -18,11 +19,14 @@ const query = reactive({
// 租户列表
const tenantList = ref([])
const tenantMap = ref({}) // code -> name 映射
// 应用列表(从应用管理获取)
const appList = ref([])
const appMap = ref({}) // app_code -> app_name 映射
const appRequireJssdk = ref({}) // app_code -> require_jssdk
const appBaseUrl = ref({}) // app_code -> base_url
const appConfigSchema = ref({}) // app_code -> config_schema
// 企微应用列表(按租户)
const wechatAppList = ref([])
@@ -36,7 +40,8 @@ const form = reactive({
tenant_id: '',
app_code: '',
app_name: '',
wechat_app_id: null
wechat_app_id: null,
custom_configs: [] // 自定义配置 [{key, value, remark}]
})
// 当前选择的应用是否需要 JS-SDK
@@ -44,6 +49,44 @@ const currentAppRequireJssdk = computed(() => {
return appRequireJssdk.value[form.app_code] || false
})
// 当前应用的配置项定义
const currentConfigSchema = computed(() => {
return appConfigSchema.value[form.app_code] || []
})
// 配置值映射(方便读写)
const configValues = computed(() => {
const map = {}
form.custom_configs.forEach(c => {
map[c.key] = c
})
return map
})
// 获取配置值
function getConfigValue(key) {
return configValues.value[key]?.value || ''
}
// 设置配置值
function setConfigValue(key, value, remark = '') {
const existing = form.custom_configs.find(c => c.key === key)
if (existing) {
existing.value = value
if (remark) existing.remark = remark
} else {
form.custom_configs.push({ key, value, remark })
}
}
// 获取选项显示名称
function getOptionLabel(schema, optionValue) {
if (schema.option_labels && schema.option_labels[optionValue]) {
return schema.option_labels[optionValue]
}
return optionValue
}
// 验证 app_code 必须是有效的应用
const validateAppCode = (rule, value, callback) => {
if (!value) {
@@ -70,35 +113,111 @@ watch(() => form.tenant_id, async (newVal) => {
form.wechat_app_id = null
})
// 监听应用选择变化,初始化配置默认值
watch(() => form.app_code, (newVal) => {
if (newVal && !editingId.value) {
// 新建时,根据 schema 初始化默认值
initConfigDefaults()
}
})
// 查看 Token 对话框
const tokenDialogVisible = ref(false)
const currentToken = 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() {
try {
const res = await api.get('/api/tenants', { params: { size: 1000 } })
tenantList.value = res.data.items || []
// 构建 code -> name 映射
const map = {}
tenantList.value.forEach(t => {
map[t.code] = t.name
})
tenantMap.value = map
} catch (e) {
console.error('获取租户列表失败:', e)
}
}
// 获取租户中文名
function getTenantName(code) {
return tenantMap.value[code] || code
}
// 获取应用中文名
function getAppName(code) {
return appMap.value[code] || code
}
async function fetchApps() {
try {
const res = await api.get('/api/apps', { params: { size: 100 } })
const apps = res.data.items || []
appList.value = apps.map(a => ({ app_code: a.app_code, app_name: a.app_name }))
// 构建 app_code -> app_name 映射
const map = {}
for (const app of apps) {
map[app.app_code] = app.app_name
appRequireJssdk.value[app.app_code] = app.require_jssdk || false
appBaseUrl.value[app.app_code] = app.base_url || ''
appConfigSchema.value[app.app_code] = app.config_schema || []
}
appMap.value = map
} catch (e) {
console.error('获取应用列表失败:', e)
}
}
// 根据 schema 初始化配置默认值
function initConfigDefaults() {
const schema = currentConfigSchema.value
if (!schema.length) return
form.custom_configs = schema.map(s => ({
key: s.key,
value: s.default || '',
remark: ''
}))
}
async function fetchWechatApps(tenantId) {
if (!tenantId) {
wechatAppList.value = []
@@ -142,7 +261,8 @@ function handleCreate() {
tenant_id: '',
app_code: '',
app_name: '',
wechat_app_id: null
wechat_app_id: null,
custom_configs: []
})
wechatAppList.value = []
dialogVisible.value = true
@@ -151,16 +271,49 @@ function handleCreate() {
async function handleEdit(row) {
editingId.value = row.id
dialogTitle.value = '编辑应用订阅'
// 先获取 schema
const schema = appConfigSchema.value[row.app_code] || []
// 合并已有配置和 schema 默认值
const existingConfigs = row.custom_configs || []
const existingMap = {}
existingConfigs.forEach(c => { existingMap[c.key] = c })
// 构建完整的配置列表(包含 schema 中的所有配置项)
const mergedConfigs = schema.map(s => ({
key: s.key,
value: existingMap[s.key]?.value ?? s.default ?? '',
remark: existingMap[s.key]?.remark ?? ''
}))
// 添加 schema 中没有但已存在的配置(兼容旧数据)
existingConfigs.forEach(c => {
if (!schema.find(s => s.key === c.key)) {
mergedConfigs.push({ ...c })
}
})
Object.assign(form, {
tenant_id: row.tenant_id,
app_code: row.app_code,
app_name: row.app_name || '',
wechat_app_id: row.wechat_app_id || null
wechat_app_id: row.wechat_app_id || null,
custom_configs: mergedConfigs
})
await fetchWechatApps(row.tenant_id)
dialogVisible.value = true
}
// 自定义配置管理(用于没有 schema 定义时的手动添加)
function addCustomConfig() {
form.custom_configs.push({ key: '', value: '', remark: '' })
}
function removeCustomConfig(index) {
form.custom_configs.splice(index, 1)
}
async function handleSubmit() {
await formRef.value.validate()
@@ -232,9 +385,161 @@ function handleCopyUrl() {
}
async function handleViewToken(row) {
// 这里需要后端返回真实 token暂时用 placeholder
// 实际生产中可能需要单独 API 获取
showToken(row.access_token === '******' ? '需要调用API获取' : row.access_token, row.app_code)
try {
const res = await api.get(`/api/tenant-apps/${row.id}/token`)
currentToken.value = res.data.access_token
currentAppUrl.value = res.data.base_url || ''
tokenDialogVisible.value = true
} catch (e) {
// 错误已在拦截器处理
}
}
// 快速选择租户标签
function selectTenant(code) {
if (query.tenant_id === code) {
query.tenant_id = '' // 再次点击取消选中
} else {
query.tenant_id = code
}
handleSearch()
}
// 复制带 token 的链接
async function copyTokenLink(row) {
try {
const res = await api.get(`/api/tenant-apps/${row.id}/token`)
const token = res.data.access_token
const baseUrl = res.data.base_url || appBaseUrl.value[row.app_code] || ''
if (!baseUrl) {
ElMessage.warning('该应用未配置访问地址')
return
}
const url = `${baseUrl}?token=${token}`
await navigator.clipboard.writeText(url)
ElMessage.success('链接已复制')
} catch (e) {
ElMessage.error('复制失败')
}
}
// 打开批量添加对话框
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(() => {
@@ -248,10 +553,16 @@ onMounted(() => {
<div class="page-container">
<div class="page-header">
<div class="title">租户应用订阅</div>
<el-button v-if="authStore.isOperator" type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建订阅
</el-button>
<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-icon><Plus /></el-icon>
新建订阅
</el-button>
</div>
</div>
<div class="page-tip">
@@ -260,16 +571,32 @@ onMounted(() => {
</el-alert>
</div>
<!-- 租户快速筛选标签 -->
<div class="tenant-tags">
<span class="tag-label">租户筛选</span>
<el-tag
v-for="tenant in tenantList"
:key="tenant.code"
:type="query.tenant_id === tenant.code ? '' : 'info'"
:effect="query.tenant_id === tenant.code ? 'dark' : 'plain'"
class="tenant-tag"
@click="selectTenant(tenant.code)"
>
{{ tenant.name }}
</el-tag>
<el-tag
v-if="query.tenant_id"
type="danger"
effect="plain"
class="tenant-tag clear-tag"
@click="selectTenant('')"
>
清除筛选
</el-tag>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<el-select v-model="query.tenant_id" placeholder="选择租户" clearable filterable style="width: 200px">
<el-option
v-for="tenant in tenantList"
:key="tenant.code"
:label="`${tenant.name} (${tenant.code})`"
:value="tenant.code"
/>
</el-select>
<el-select v-model="query.app_code" placeholder="选择应用" clearable style="width: 150px">
<el-option v-for="app in appList" :key="app.app_code" :label="app.app_name" :value="app.app_code" />
</el-select>
@@ -279,10 +606,33 @@ onMounted(() => {
<!-- 表格 -->
<el-table v-loading="loading" :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="tenant_id" label="租户ID" width="120" />
<el-table-column prop="app_code" label="应用代码" width="150" />
<el-table-column prop="app_name" label="备注名称" width="150" />
<el-table-column label="企微应用" width="180">
<el-table-column label="租户" width="120">
<template #default="{ row }">
<span class="cell-name">{{ getTenantName(row.tenant_id) }}</span>
</template>
</el-table-column>
<el-table-column label="应用" width="150">
<template #default="{ row }">
<span class="cell-name">{{ getAppName(row.app_code) }}</span>
</template>
</el-table-column>
<el-table-column prop="app_name" label="备注" width="120" />
<el-table-column label="快捷链接" width="100">
<template #default="{ row }">
<el-button
v-if="row.access_token && appBaseUrl[row.app_code]"
type="primary"
link
size="small"
@click="copyTokenLink(row)"
>
<el-icon><CopyDocument /></el-icon>
复制
</el-button>
<span v-else style="color: #c0c4cc; font-size: 12px">-</span>
</template>
</el-table-column>
<el-table-column label="企微应用" width="140">
<template #default="{ row }">
<template v-if="row.wechat_app">
<el-tag type="success" size="small">{{ row.wechat_app.name }}</el-tag>
@@ -290,24 +640,26 @@ onMounted(() => {
<el-tag v-else type="info" size="small">未关联</el-tag>
</template>
</el-table-column>
<el-table-column label="Token 状态" width="120">
<el-table-column label="配置" width="80">
<template #default="{ row }">
<el-tag v-if="row.access_token" type="success" size="small">已生成</el-tag>
<el-tag v-else type="danger" size="small">未生成</el-tag>
<el-tag v-if="row.custom_configs && row.custom_configs.length > 0" type="primary" size="small">
{{ row.custom_configs.length }}
</el-tag>
<span v-else style="color: #909399; font-size: 12px">-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<el-table-column label="状态" width="70">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button v-if="authStore.isOperator" type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="authStore.isOperator" type="success" link size="small" @click="handleViewToken(row)">查看Token</el-button>
<el-button v-if="authStore.isOperator" type="warning" link size="small" @click="handleRegenerateToken(row)">重置Token</el-button>
<el-button v-if="authStore.isOperator" type="success" link size="small" @click="handleViewToken(row)">Token</el-button>
<el-button v-if="authStore.isOperator" type="warning" link size="small" @click="handleRegenerateToken(row)">重置</el-button>
<el-button v-if="authStore.isOperator" type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
@@ -325,7 +677,7 @@ onMounted(() => {
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="750px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="租户" prop="tenant_id">
<el-select
@@ -374,6 +726,119 @@ onMounted(() => {
</div>
</el-form-item>
</template>
<!-- 自定义配置 -->
<template v-if="currentConfigSchema.length > 0 || form.custom_configs.length > 0">
<el-divider content-position="left">应用配置</el-divider>
<div class="custom-configs-section">
<!-- 根据 schema 渲染表单 -->
<template v-for="(schema, index) in currentConfigSchema" :key="schema.key">
<div class="config-item-schema">
<div class="config-label">
{{ schema.label }}
<span v-if="schema.required" class="required-star">*</span>
</div>
<!-- text 类型 -->
<template v-if="schema.type === 'text'">
<el-input
v-model="form.custom_configs[index].value"
type="textarea"
:rows="2"
:autosize="{ minRows: 2, maxRows: 12 }"
:placeholder="schema.placeholder || '请输入'"
/>
</template>
<!-- radio 类型 -->
<template v-else-if="schema.type === 'radio'">
<el-radio-group v-model="form.custom_configs[index].value">
<el-radio
v-for="opt in schema.options"
:key="opt"
:value="opt"
>
{{ getOptionLabel(schema, opt) }}
</el-radio>
</el-radio-group>
</template>
<!-- select 类型 -->
<template v-else-if="schema.type === 'select'">
<el-select v-model="form.custom_configs[index].value" placeholder="请选择" style="width: 100%">
<el-option
v-for="opt in schema.options"
:key="opt"
:label="getOptionLabel(schema, opt)"
:value="opt"
/>
</el-select>
</template>
<!-- switch 类型 -->
<template v-else-if="schema.type === 'switch'">
<el-switch
v-model="form.custom_configs[index].value"
active-value="true"
inactive-value="false"
/>
</template>
<!-- 备注输入 -->
<el-input
v-model="form.custom_configs[index].remark"
placeholder="备注(可选)"
style="margin-top: 8px; width: 300px"
size="small"
/>
</div>
</template>
<!-- 没有 schema 定义时显示手动配置 -->
<template v-if="currentConfigSchema.length === 0">
<div v-for="(config, index) in form.custom_configs" :key="index" class="config-item">
<div class="config-row">
<el-input
v-model="config.key"
placeholder="配置键 (如: industry)"
style="width: 150px"
/>
<el-input
v-model="config.remark"
placeholder="备注说明"
style="width: 180px; margin-left: 8px"
/>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
style="margin-left: 8px"
@click="removeCustomConfig(index)"
/>
</div>
<el-input
v-model="config.value"
type="textarea"
:rows="3"
:autosize="{ minRows: 2, maxRows: 10 }"
placeholder="配置值(支持超长文本,如提示词等)"
style="margin-top: 8px"
/>
</div>
<el-button type="primary" plain @click="addCustomConfig" style="margin-top: 8px">
<el-icon><Plus /></el-icon>
添加配置项
</el-button>
<div v-if="form.custom_configs.length === 0" class="config-empty-tip">
该应用暂无配置项定义
</div>
</template>
</div>
</template>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -423,6 +888,119 @@ onMounted(() => {
<el-button @click="tokenDialogVisible = false">关闭</el-button>
</template>
</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>
</template>
@@ -431,6 +1009,131 @@ onMounted(() => {
margin-bottom: 16px;
}
/* 租户标签筛选 */
.tenant-tags {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 8px;
}
.tag-label {
color: #606266;
font-size: 13px;
font-weight: 500;
margin-right: 4px;
}
.tenant-tag {
cursor: pointer;
transition: all 0.2s;
}
.tenant-tag:hover {
transform: translateY(-1px);
}
.clear-tag {
margin-left: 8px;
}
.cell-name {
font-weight: 500;
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 {
padding: 0 10px;
}
@@ -444,4 +1147,47 @@ onMounted(() => {
margin-bottom: 8px;
color: #303133;
}
/* 自定义配置样式 */
.custom-configs-section {
padding: 0 10px;
}
.config-item {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.config-item-schema {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.config-label {
font-weight: 500;
color: #303133;
margin-bottom: 10px;
font-size: 14px;
}
.required-star {
color: #f56c6c;
margin-left: 4px;
}
.config-row {
display: flex;
align-items: center;
}
.config-empty-tip {
color: #909399;
font-size: 13px;
text-align: center;
padding: 20px 0;
}
</style>