This commit is contained in:
2025-12-19 16:57:49 +08:00
parent cd634e6a3e
commit 734e71976e
6 changed files with 377 additions and 18 deletions

97
components.d.ts vendored
View File

@@ -0,0 +1,97 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AppBreadcrumb: typeof import('./src/components/layout/AppBreadcrumb.vue')['default']
AppHeader: typeof import('./src/components/layout/AppHeader.vue')['default']
AppLoading: typeof import('./src/components/common/AppLoading.vue')['default']
AppSidebar: typeof import('./src/components/layout/AppSidebar.vue')['default']
BusinessConsultationDialog: typeof import('./src/components/common/BusinessConsultationDialog.vue')['default']
CertificationBanner: typeof import('./src/components/common/CertificationBanner.vue')['default']
CertificationNotice: typeof import('./src/components/common/CertificationNotice.vue')['default']
ChartCard: typeof import('./src/components/statistics/ChartCard.vue')['default']
CodeDisplay: typeof import('./src/components/common/CodeDisplay.vue')['default']
CustomSteps: typeof import('./src/components/common/CustomSteps.vue')['default']
DanmakuBar: typeof import('./src/components/common/DanmakuBar.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLoading: typeof import('element-plus/es')['ElLoading']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree']
ElUpload: typeof import('element-plus/es')['ElUpload']
ExportDialog: typeof import('./src/components/common/ExportDialog.vue')['default']
FileUpload: typeof import('./src/components/common/FileUpload.vue')['default']
FilterItem: typeof import('./src/components/common/FilterItem.vue')['default']
FilterSection: typeof import('./src/components/common/FilterSection.vue')['default']
FloatingCustomerService: typeof import('./src/components/common/FloatingCustomerService.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
ListPageLayout: typeof import('./src/components/common/ListPageLayout.vue')['default']
MarkdownEditor: typeof import('./src/components/common/MarkdownEditor.vue')['default']
NotificationPanel: typeof import('./src/components/layout/NotificationPanel.vue')['default']
PermissionGuard: typeof import('./src/components/auth/PermissionGuard.vue')['default']
ProductApiConfigDialog: typeof import('./src/components/admin/ProductApiConfigDialog.vue')['default']
ProductCard: typeof import('./src/components/product/ProductCard.vue')['default']
ProductDocumentationDialog: typeof import('./src/components/admin/ProductDocumentationDialog.vue')['default']
ProductFormDialog: typeof import('./src/components/admin/ProductFormDialog.vue')['default']
ResponsiveActionColumn: typeof import('./src/components/common/ResponsiveActionColumn.vue')['default']
RichTextEditor: typeof import('./src/components/common/RichTextEditor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
StatCard: typeof import('./src/components/statistics/StatCard.vue')['default']
StatisticsDashboard: typeof import('./src/components/statistics/StatisticsDashboard.vue')['default']
TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
VersionInfo: typeof import('./src/components/common/VersionInfo.vue')['default']
WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

99
src/api/ui-component.js Normal file
View File

@@ -0,0 +1,99 @@
import request from '@/utils/request'
export const uiComponentApi = {
// 获取UI组件列表
getUIComponentList(params) {
return request.get('/admin/ui-components', { params })
},
// 获取UI组件详情
getUIComponentDetail(id) {
return request.get(`/admin/ui-components/${id}`)
},
// 创建UI组件
createUIComponent(data) {
// 确保发送的数据结构与后端期望的完全匹配
const requestData = {
component_code: data.component_code || '',
component_name: data.component_name || '',
description: data.description || '',
version: data.version || '',
is_active: data.is_active !== undefined ? data.is_active : true,
sort_order: data.sort_order !== undefined ? data.sort_order : 0
}
// 添加调试日志
console.log('创建UI组件请求数据:', requestData)
return request.post('/admin/ui-components', requestData)
},
// 更新UI组件
updateUIComponent(id, data) {
// 确保发送的数据结构与后端期望的完全匹配
const requestData = {
id: id,
component_code: data.component_code || '',
component_name: data.component_name || '',
description: data.description || '',
version: data.version || '',
is_active: data.is_active !== undefined ? data.is_active : true,
sort_order: data.sort_order !== undefined ? data.sort_order : 0
}
// 添加调试日志
console.log('更新UI组件请求数据:', requestData)
return request.put(`/admin/ui-components/${id}`, requestData)
},
// 删除UI组件
deleteUIComponent(id) {
return request.delete(`/admin/ui-components/${id}`)
},
// 上传UI组件文件
uploadUIComponentFile(id, formData) {
return request.post(`/admin/ui-components/${id}/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
},
// 上传并解压UI组件文件
uploadAndExtractUIComponentFile(id, formData) {
return request.post(`/admin/ui-components/${id}/upload-extract`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
},
// 下载UI组件文件
downloadUIComponentFile(id) {
return request.get(`/admin/ui-components/${id}/download`, {
responseType: 'blob'
})
},
// 获取UI组件文件夹内容
getUIComponentFolderContent(id) {
return request.get(`/admin/ui-components/${id}/folder-content`)
},
// 删除UI组件文件夹
deleteUIComponentFolder(id) {
return request.delete(`/admin/ui-components/${id}/folder`)
},
// 创建UI组件并上传文件合并操作
createUIComponentWithFile(formData) {
return request.post('/admin/ui-components/create-with-file', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
}

View File

@@ -124,7 +124,8 @@ export const getUserAccessibleMenuItems = (userType = 'user') => {
{ name: '调用记录', path: '/admin/usage', icon: Clipboard }, { name: '调用记录', path: '/admin/usage', icon: Clipboard },
{ name: '消费记录', path: '/admin/transactions', icon: Clipboard }, { name: '消费记录', path: '/admin/transactions', icon: Clipboard },
{ name: '充值记录', path: '/admin/recharge-records', icon: CreditCard }, { name: '充值记录', path: '/admin/recharge-records', icon: CreditCard },
{ name: '发票管理', path: '/admin/invoices', icon: Wallet } { name: '发票管理', path: '/admin/invoices', icon: Wallet },
{ name: '组件管理', path: '/admin/ui-components', icon: Cube }
] ]
}) })
} }

View File

@@ -73,7 +73,7 @@
上传文件 上传文件
</el-button> </el-button>
<el-button <el-button
v-if="row.file_path && !row.is_extracted" v-if="row.file_path && !row.is_extracted && isZipFileFromPath(row.file_path)"
size="small" size="small"
type="warning" type="warning"
@click="handleUploadExtract(row)" @click="handleUploadExtract(row)"
@@ -162,6 +162,36 @@
<el-form-item label="排序"> <el-form-item label="排序">
<el-input-number v-model="form.sort_order" :min="0" /> <el-input-number v-model="form.sort_order" :min="0" />
</el-form-item> </el-form-item>
<el-form-item label="上传模式" v-if="!isEdit">
<el-radio-group v-model="uploadMode">
<el-radio label="files">文件上传</el-radio>
<el-radio label="folder">文件夹上传</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="组件文件" v-if="!isEdit">
<el-upload
ref="createUploadRef"
:auto-upload="false"
:multiple="uploadMode === 'files'"
:webkitdirectory="uploadMode === 'folder'"
:on-change="handleCreateFileChange"
:on-remove="handleCreateFileRemove"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip" v-if="uploadMode === 'files'">
可以上传多个文件每个文件不超过100MBZIP文件可以自动解压其他文件类型仅保存
</div>
<div class="el-upload__tip" v-else>
可以上传整个文件夹保持原有目录结构ZIP文件可以自动解压每个文件不超过100MB
</div>
</template>
</el-upload>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@@ -183,7 +213,6 @@
ref="uploadRef" ref="uploadRef"
:auto-upload="false" :auto-upload="false"
:limit="1" :limit="1"
accept=".zip"
:on-change="handleFileChange" :on-change="handleFileChange"
:on-remove="handleFileRemove" :on-remove="handleFileRemove"
drag drag
@@ -194,7 +223,7 @@
</div> </div>
<template #tip> <template #tip>
<div class="el-upload__tip"> <div class="el-upload__tip">
只能上传zip文件且不超过100MB 文件夹使用压缩为zip格式可以批量上传文件
</div> </div>
</template> </template>
</el-upload> </el-upload>
@@ -263,8 +292,12 @@ const uploading = ref(false)
const isEdit = ref(false) const isEdit = ref(false)
const currentComponent = ref(null) const currentComponent = ref(null)
const selectedFile = ref(null) const selectedFile = ref(null)
const selectedCreateFile = ref(null)
const selectedCreateFiles = ref([])
const uploadMode = ref('files') // 'files' 或 'folder'
const formRef = ref(null) const formRef = ref(null)
const uploadRef = ref(null) const uploadRef = ref(null)
const createUploadRef = ref(null)
const folderTree = ref([]) const folderTree = ref([])
// 筛选表单 // 筛选表单
@@ -411,9 +444,33 @@ const handleSubmit = async () => {
await uiComponentApi.updateUIComponent(form.id, form) await uiComponentApi.updateUIComponent(form.id, form)
ElMessage.success('更新成功') ElMessage.success('更新成功')
} else { } else {
// 检查是否上传了文件
if (selectedCreateFiles.value && selectedCreateFiles.value.length > 0) {
// 使用合并接口,同时创建组件和上传文件
const formData = new FormData()
formData.append('component_code', form.component_code)
formData.append('component_name', form.component_name)
formData.append('description', form.description || '')
formData.append('version', form.version || '')
formData.append('is_active', form.is_active ? 'true' : 'false')
formData.append('sort_order', form.sort_order.toString())
// 添加所有文件
selectedCreateFiles.value.forEach(file => {
formData.append('files', file.raw)
if (uploadMode.value === 'folder') {
formData.append('paths', file.path)
}
})
await uiComponentApi.createUIComponentWithFile(formData)
ElMessage.success('创建并上传成功')
} else {
// 只创建组件,不上传文件
await uiComponentApi.createUIComponent(form) await uiComponentApi.createUIComponent(form)
ElMessage.success('创建成功') ElMessage.success('创建成功')
} }
}
dialogVisible.value = false dialogVisible.value = false
fetchComponentList() fetchComponentList()
@@ -441,9 +498,15 @@ const resetForm = () => {
is_active: true, is_active: true,
sort_order: 0 sort_order: 0
}) })
selectedCreateFile.value = null
selectedCreateFiles.value = []
uploadMode.value = 'files' // 重置为文件上传模式
if (formRef.value) { if (formRef.value) {
formRef.value.resetFields() formRef.value.resetFields()
} }
if (createUploadRef.value) {
createUploadRef.value.clearFiles()
}
} }
const handleUpload = (row) => { const handleUpload = (row) => {
@@ -472,6 +535,20 @@ const handleFileRemove = () => {
selectedFile.value = null selectedFile.value = null
} }
const handleCreateFileChange = (file, fileList) => {
selectedCreateFiles.value = fileList.map(f => ({
raw: f.raw,
name: f.name,
path: uploadMode.value === 'folder' ? (f.raw.webkitRelativePath || f.name) : f.name
}))
selectedCreateFile.value = fileList.length > 0 ? fileList[0].raw : null // 保留兼容性
}
const handleCreateFileRemove = () => {
selectedCreateFiles.value = []
selectedCreateFile.value = null // 保留兼容性
}
const handleFileSubmit = async () => { const handleFileSubmit = async () => {
if (!selectedFile.value) { if (!selectedFile.value) {
ElMessage.warning('请选择要上传的文件') ElMessage.warning('请选择要上传的文件')
@@ -512,7 +589,17 @@ const handleDownload = async (row) => {
const url = window.URL.createObjectURL(new Blob([response.data])) const url = window.URL.createObjectURL(new Blob([response.data]))
const link = document.createElement('a') const link = document.createElement('a')
link.href = url link.href = url
link.setAttribute('download', `${row.component_name}.zip`)
// 根据文件类型确定下载文件名
let fileName = row.component_name
if (row.file_path) {
const fileExtension = row.file_path.substring(row.file_path.lastIndexOf('.'))
fileName += fileExtension
} else {
fileName += '.zip' // 默认扩展名
}
link.setAttribute('download', fileName)
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
document.body.removeChild(link) document.body.removeChild(link)
@@ -620,6 +707,18 @@ const formatDateTime = (dateTime) => {
return date.toLocaleString('zh-CN') return date.toLocaleString('zh-CN')
} }
// 判断文件是否为ZIP类型
const isZipFile = (file) => {
if (!file) return false
return file.toLowerCase().endsWith('.zip')
}
// 判断文件路径是否为ZIP类型
const isZipFileFromPath = (path) => {
if (!path) return false
return path.toLowerCase().endsWith('.zip')
}
// 生命周期 // 生命周期
onMounted(() => { onMounted(() => {
fetchComponentList() fetchComponentList()

View File

@@ -550,14 +550,29 @@ const loadAlertSettings = async () => {
try { try {
// 调用API获取用户的余额预警设置 // 调用API获取用户的余额预警设置
const response = await balanceAlertApi.getUserAlertSettings() const response = await balanceAlertApi.getUserAlertSettings()
// 添加日志追踪:接收到的原始数据
console.log('[余额预警] 接收到的原始数据:', JSON.stringify(response))
if (response.success) { if (response.success) {
balanceAlertSettings.value = response.data // 转换字段名确保前端使用camelCase
originalAlertSettings.value = JSON.parse(JSON.stringify(response.data)) const transformedData = {
enabled: response.data.enabled,
threshold: response.data.threshold,
alertPhone: response.data.alert_phone || response.data.alertPhone || '' // 支持两种字段名优先使用alert_phone
}
// 添加日志追踪:转换后的数据
console.log('[余额预警] 转换后的数据:', JSON.stringify(transformedData))
balanceAlertSettings.value = transformedData
originalAlertSettings.value = JSON.parse(JSON.stringify(transformedData))
} else { } else {
console.error('[余额预警] 获取余额预警设置失败:', response.message)
ElMessage.error('获取余额预警设置失败') ElMessage.error('获取余额预警设置失败')
} }
} catch (error) { } catch (error) {
console.error('获取余额预警设置失败:', error) console.error('[余额预警] 获取余额预警设置异常:', error)
ElMessage.error('获取余额预警设置失败') ElMessage.error('获取余额预警设置失败')
} }
} }
@@ -565,20 +580,31 @@ const loadAlertSettings = async () => {
// 处理预警开关变化 // 处理预警开关变化
const handleAlertEnabledChange = (value) => { const handleAlertEnabledChange = (value) => {
if (!value) { if (!value) {
// 禁用预警时,可以清空手机号 // 禁用预警时,保留手机号不清空,但记录日志
balanceAlertSettings.value.alertPhone = '' console.log('[余额预警] 预警已禁用,保留手机号:', balanceAlertSettings.value.alertPhone)
} else { } else {
// 启用预警时,使用用户手机号作为默认值 // 启用预警时,如果用户从未设置过预警手机号,则默认使用用户手机号
if (!balanceAlertSettings.value.alertPhone && userInfo.value?.phone) { // 如果原始数据中alertPhone为空说明用户从未设置过此时才设置默认值
if (!originalAlertSettings.value.alertPhone && userInfo.value?.phone) {
balanceAlertSettings.value.alertPhone = userInfo.value.phone balanceAlertSettings.value.alertPhone = userInfo.value.phone
console.log('[余额预警] 首次启用预警,设置默认手机号:', userInfo.value.phone)
} else {
console.log('[余额预警] 预警已启用,保持现有手机号:', balanceAlertSettings.value.alertPhone)
} }
} }
} }
// 处理阈值变化 // 处理阈值变化
const handleThresholdChange = (value) => { const handleThresholdChange = (value) => {
if (value < 0) { // 确保值是数字类型
const numValue = Number(value)
if (isNaN(numValue) || numValue < 0) {
balanceAlertSettings.value.threshold = 0 balanceAlertSettings.value.threshold = 0
console.log('[余额预警] 阈值已重置为0输入值无效:', value)
} else {
balanceAlertSettings.value.threshold = numValue
console.log('[余额预警] 阈值已更新:', numValue)
} }
} }
@@ -588,7 +614,12 @@ const handlePhoneChange = (event) => {
// 简单的手机号验证 // 简单的手机号验证
if (phone && !/^1[3-9]\d{9}$/.test(phone)) { if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
ElMessage.warning('请输入正确的手机号格式') ElMessage.warning('请输入正确的手机号格式')
console.log('[余额预警] 手机号格式不正确:', phone)
} }
// 更新数据模型中的手机号
balanceAlertSettings.value.alertPhone = phone
console.log('[余额预警] 手机号已更新:', phone)
} }
// 保存余额预警设置 // 保存余额预警设置
@@ -600,16 +631,42 @@ const saveAlertSettings = async () => {
alertSettingsLoading.value = true alertSettingsLoading.value = true
try { try {
// 准备发送给后端的数据,确保字段名使用后端风格 (snake_case)
const dataToSave = {
enabled: balanceAlertSettings.value.enabled,
threshold: balanceAlertSettings.value.threshold,
alert_phone: balanceAlertSettings.value.alertPhone // 转换字段名
}
// 添加日志追踪:准备发送的数据
console.log('[余额预警] 准备发送的数据:', JSON.stringify(dataToSave))
console.log('[余额预警] 当前表单数据:', JSON.stringify(balanceAlertSettings.value))
// 调用API保存用户的余额预警设置 // 调用API保存用户的余额预警设置
const response = await balanceAlertApi.updateUserAlertSettings(balanceAlertSettings.value) const response = await balanceAlertApi.updateUserAlertSettings(dataToSave)
// 添加日志追踪API响应
console.log('[余额预警] API响应:', JSON.stringify(response))
if (response.success) { if (response.success) {
originalAlertSettings.value = JSON.parse(JSON.stringify(balanceAlertSettings.value)) // 确保数据结构一致,只保留前端使用的字段
const updatedData = {
enabled: balanceAlertSettings.value.enabled,
threshold: balanceAlertSettings.value.threshold,
alertPhone: balanceAlertSettings.value.alertPhone // 确保使用前端字段名
}
// 添加日志追踪:更新后的数据
console.log('[余额预警] 更新后的数据:', JSON.stringify(updatedData))
originalAlertSettings.value = JSON.parse(JSON.stringify(updatedData))
ElMessage.success('余额预警设置保存成功') ElMessage.success('余额预警设置保存成功')
} else { } else {
console.error('[余额预警] 保存失败:', response.message)
ElMessage.error('保存失败:' + response.message) ElMessage.error('保存失败:' + response.message)
} }
} catch (error) { } catch (error) {
console.error('保存余额预警设置失败:', error) console.error('[余额预警] 保存余额预警设置失败:', error)
ElMessage.error('保存余额预警设置失败') ElMessage.error('保存余额预警设置失败')
} finally { } finally {
alertSettingsLoading.value = false alertSettingsLoading.value = false

View File

@@ -286,6 +286,12 @@ const routes = [
name: 'AdminStatistics', name: 'AdminStatistics',
component: () => import('@/pages/admin/statistics/SystemStatisticsPage.vue'), component: () => import('@/pages/admin/statistics/SystemStatisticsPage.vue'),
meta: { title: '系统统计' } meta: { title: '系统统计' }
},
{
path: 'ui-components',
name: 'AdminUIComponents',
component: () => import('@/pages/admin/ui-components/index.vue'),
meta: { title: '组件管理' }
} }
] ]
}, },