🎨 更新用户版本
This commit is contained in:
@@ -206,7 +206,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="TableName" class="w-full">
|
||||
<el-form-item label="abbreviation" prop="abbreviation" class="w-full">
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="简称会作为入参对象名和路由group"
|
||||
@@ -268,7 +268,7 @@
|
||||
prop="package"
|
||||
class="w-full relative"
|
||||
>
|
||||
<el-select v-model="form.package" class="w-full pr-12">
|
||||
<el-select v-model="form.package" class="w-full pr-12" filterable>
|
||||
<el-option
|
||||
v-for="item in pkgs"
|
||||
:key="item.ID"
|
||||
@@ -307,6 +307,7 @@
|
||||
</template>
|
||||
<el-select
|
||||
v-model="form.businessDB"
|
||||
clearable
|
||||
placeholder="选择业务库"
|
||||
class="w-full"
|
||||
>
|
||||
@@ -534,7 +535,7 @@
|
||||
width="160"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-input :disabled="row.disabled" v-model="row.fieldName" />
|
||||
<el-input disabled v-model="row.fieldName" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -696,7 +697,7 @@
|
||||
style="width: 100%"
|
||||
placeholder="请选择字段查询条件"
|
||||
clearable
|
||||
:disabled="row.fieldType !== 'json' || row.disabled"
|
||||
:disabled="row.fieldType === 'json' || row.disabled"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in typeSearchOptions"
|
||||
@@ -1541,15 +1542,6 @@
|
||||
}
|
||||
init()
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
if (route.name === 'autoCodeEdit') {
|
||||
init()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(()=>form.value.generateServer,()=>{
|
||||
if(!form.value.generateServer){
|
||||
form.value.autoCreateApiToSql = false
|
||||
@@ -1566,6 +1558,7 @@
|
||||
|
||||
const catchData = () => {
|
||||
window.sessionStorage.setItem('autoCode', JSON.stringify(form.value))
|
||||
ElMessage.success('暂存成功')
|
||||
}
|
||||
|
||||
const getCatch = () => {
|
||||
@@ -1619,6 +1612,8 @@
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
form.value = JSON.parse(e.target.result)
|
||||
form.value.generateServer = true
|
||||
form.value.generateWeb = true
|
||||
ElMessage.success('JSON 文件导入成功')
|
||||
} catch (_) {
|
||||
ElMessage.error('无效的 JSON 文件')
|
||||
|
151
src/view/systemTools/autoCode/mcp.vue
Normal file
151
src/view/systemTools/autoCode/mcp.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="gva-form-box">
|
||||
<el-form :model="form" ref="formRef" label-width="100px" :rules="rules">
|
||||
<el-form-item label="工具名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="例:CurrentTime" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工具描述" prop="description">
|
||||
<el-input type="textarea" v-model="form.description" placeholder="请输入工具描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数列表">
|
||||
<el-table :data="form.params" style="width: 100%">
|
||||
<el-table-column prop="name" label="参数名" width="120">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.name" placeholder="参数名" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.description" placeholder="描述" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.type" placeholder="类型">
|
||||
<el-option label="string" value="string" />
|
||||
<el-option label="number" value="number" />
|
||||
<el-option label="boolean" value="boolean" />
|
||||
<el-option label="object" value="object" />
|
||||
<el-option label="array" value="array" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="默认值" width="300">
|
||||
<template #default="scope">
|
||||
<el-input :disabled="scope.row.type === 'object'" v-model="scope.row.default" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="required" label="必填" width="80">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.required" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="scope">
|
||||
<el-button type="text" @click="removeParam(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<div class="flex justify-end">
|
||||
<el-button type="primary" icon="plus" @click="addParam" style="margin-top: 10px;">添加参数</el-button>
|
||||
</div>
|
||||
<el-form-item label="返回参数">
|
||||
<el-table :data="form.response" style="width: 100%">
|
||||
<el-table-column prop="type" label="类型" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.type" placeholder="类型">
|
||||
<el-option label="text" value="text" />
|
||||
<el-option label="image" value="image" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="scope">
|
||||
<el-button type="text" @click="removeResponse(scope.$index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<div class="flex justify-end">
|
||||
<el-button type="primary" icon="plus" @click="addResponse" style="margin-top: 10px;">添加返回参数</el-button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-8">
|
||||
<el-button type="primary" @click="submit">生成</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { mcp } from '@/api/autoCode'
|
||||
|
||||
defineOptions({
|
||||
name: 'MCP'
|
||||
})
|
||||
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
type: '',
|
||||
params: [],
|
||||
response: []
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入工具名称', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '请输入工具描述', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function addParam() {
|
||||
form.params.push({
|
||||
name: '',
|
||||
description: '',
|
||||
type: '',
|
||||
required: false
|
||||
})
|
||||
}
|
||||
|
||||
function removeParam(index) {
|
||||
form.params.splice(index, 1)
|
||||
}
|
||||
|
||||
function addResponse() {
|
||||
form.response.push({
|
||||
type: ''
|
||||
})
|
||||
}
|
||||
|
||||
function removeResponse(index) {
|
||||
form.response.splice(index, 1)
|
||||
}
|
||||
|
||||
function submit() {
|
||||
formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
// 简单校验参数
|
||||
for (const p of form.params) {
|
||||
if (!p.name || !p.description || !p.type) {
|
||||
ElMessage.error('请完善所有参数信息')
|
||||
return
|
||||
}
|
||||
}
|
||||
// 校验返回参数
|
||||
for (const r of form.response) {
|
||||
if (!r.type) {
|
||||
ElMessage.error('请完善所有返回参数类型')
|
||||
return
|
||||
}
|
||||
}
|
||||
const res = await mcp(form)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
261
src/view/systemTools/autoCode/mcpTest.vue
Normal file
261
src/view/systemTools/autoCode/mcpTest.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
|
||||
<el-card class="mb-2">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center font-bold">
|
||||
<span>MCP 服务器配置示例</span>
|
||||
<el-tooltip content="复制配置" placement="top">
|
||||
<el-button :icon="DocumentCopy" circle @click="copyMcpConfig" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="font-mono whitespace-pre-wrap break-words bg-gray-100 p-2.5 rounded text-gray-700">{{ mcpServerConfig }}</pre>
|
||||
</el-card>
|
||||
|
||||
|
||||
<el-row :gutter="8">
|
||||
<el-col v-for="tool in mcpTools" :key="tool.name" :xs="24" :sm="12" :md="12" :lg="8">
|
||||
<el-card class="mb-5 min-h-[150px] flex flex-col overflow-hidden">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center font-bold">
|
||||
<span>{{ tool.name }}</span>
|
||||
<el-tooltip content="测试工具" placement="top">
|
||||
<el-button :icon="VideoPlay" circle @click="openTestDialog(tool)" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div class="text-sm mb-1">{{ tool.description }}</div>
|
||||
<div v-if="tool.inputSchema && tool.inputSchema.properties && Object.keys(tool.inputSchema.properties).length > 0" class="mt-1 text-xs overflow-y-auto max-h-[100px] p-2 border-t border-gray-200 bg-gray-50 rounded-b">
|
||||
<p class="font-semibold mb-1 text-gray-700 flex items-center">
|
||||
<span class="mr-1 my-2">参数列表</span>
|
||||
<span class="text-xs text-gray-500">({{ Object.keys(tool.inputSchema.properties).length }})</span>
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(propDetails, propName) in tool.inputSchema.properties" :key="propName" class="flex flex-col p-1.5 bg-white rounded border border-gray-100 hover:border-gray-300 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium text-gray-800">{{ propName }}</span>
|
||||
<span v-if="tool.inputSchema.required && tool.inputSchema.required.includes(propName)" class="ml-1 text-red-500 text-xs">*</span>
|
||||
</div>
|
||||
<span class="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded">{{ propDetails.type }}</span>
|
||||
</div>
|
||||
<div class="text-gray-500 mt-0.5 text-xs line-clamp-2" :title="propDetails.description || '无描述'">
|
||||
{{ propDetails.description || '无描述' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mt-1 text-xs p-2 border-t border-gray-200 bg-gray-50 rounded-b flex items-center justify-center">
|
||||
<span class="text-gray-500 italic py-3">无输入参数</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
<el-dialog
|
||||
v-model="testDialogVisible"
|
||||
:title="currentTestingTool ? `${currentTestingTool.name} - 参数测试` : '参数测试'"
|
||||
width="60%"
|
||||
:before-close="handleCloseDialog"
|
||||
>
|
||||
<el-form v-if="currentTestingTool" :model="testParamsForm" ref="testParamsFormRef" label-width="120px" label-position="top" class="max-h-[calc(60vh-120px)] overflow-y-auto">
|
||||
<el-form-item
|
||||
v-for="(propDetails, propName) in currentTestingTool.inputSchema.properties"
|
||||
:key="propName"
|
||||
:label="propDetails.description || propName"
|
||||
:prop="propName"
|
||||
:rules="currentTestingTool.inputSchema.required && currentTestingTool.inputSchema.required.includes(propName) ? [{ required: true, message: '请输入 ' + (propDetails.description || propName), trigger: 'blur' }] : []"
|
||||
>
|
||||
<el-input
|
||||
v-if="propDetails.type === 'string' && !propDetails.enum"
|
||||
v-model="testParamsForm[propName]"
|
||||
:placeholder="propDetails.description || '请输入' + propName"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="propDetails.type === 'number'"
|
||||
v-model.number="testParamsForm[propName]"
|
||||
type="number"
|
||||
:placeholder="propDetails.description || '请输入数字' + propName"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="propDetails.type === 'boolean'"
|
||||
v-model="testParamsForm[propName]"
|
||||
:placeholder="propDetails.description || '请选择'"
|
||||
>
|
||||
<el-option label="True" :value="true" />
|
||||
<el-option label="False" :value="false" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-else-if="propDetails.type === 'string' && propDetails.enum"
|
||||
v-model="testParamsForm[propName]"
|
||||
:placeholder="propDetails.description || '请选择' + propName"
|
||||
>
|
||||
<el-option v-for="enumValue in propDetails.enum" :key="enumValue" :label="enumValue" :value="enumValue" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else
|
||||
type="textarea"
|
||||
v-model="testParamsForm[propName]"
|
||||
:placeholder="(propDetails.description || propName) + ' (请输入JSON格式)'"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="apiDialogResponse" class="mt-5 p-[15px] border border-gray-200 rounded bg-gray-50">
|
||||
<h4 class="mt-0 mb-2.5 text-base">API 返回结果:</h4>
|
||||
<div v-if="typeof apiDialogResponse === 'string'">
|
||||
<pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ apiDialogResponse }}</pre>
|
||||
</div>
|
||||
<div v-else-if="apiDialogResponse.type === 'image' && apiDialogResponse.content">
|
||||
<el-image
|
||||
class="max-w-full max-h-[300px]"
|
||||
:src="apiDialogResponse.content"
|
||||
:preview-src-list="[apiDialogResponse.content]"
|
||||
fit="contain"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="apiDialogResponse.type === 'text' && apiDialogResponse.content">
|
||||
<pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ apiDialogResponse.content }}</pre>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre class="bg-gray-100 p-2.5 rounded whitespace-pre-wrap break-words overflow-y-auto">{{ JSON.stringify(apiDialogResponse, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="testDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleDialogTestTool">测试</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VideoPlay, DocumentCopy } from '@element-plus/icons-vue' // Added DocumentCopy
|
||||
import { mcpList, mcpTest } from '@/api/autoCode'
|
||||
|
||||
defineOptions({
|
||||
name: 'MCPTest'
|
||||
})
|
||||
|
||||
|
||||
const mcpTools = ref([])
|
||||
const testDialogVisible = ref(false)
|
||||
const currentTestingTool = ref(null)
|
||||
const testParamsForm = reactive({})
|
||||
const testParamsFormRef = ref(null)
|
||||
const apiDialogResponse = ref(null)
|
||||
|
||||
const mcpServerConfig = ref(JSON.stringify({
|
||||
"mcpServers": {
|
||||
"gva": {
|
||||
"url": "https://127.0.0.1/sse"
|
||||
}
|
||||
}
|
||||
}, null, 2))
|
||||
|
||||
const fetchMcpTools = async () => {
|
||||
const res = await mcpList()
|
||||
if (res.code === 0 && res.data && res.data.list.tools) {
|
||||
mcpTools.value = res.data.list.tools
|
||||
mcpServerConfig.value = JSON.stringify(res.data.mcpServerConfig, null, 2)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取工具列表失败或数据格式不正确')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchMcpTools()
|
||||
})
|
||||
|
||||
const copyMcpConfig = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(mcpServerConfig.value)
|
||||
ElMessage.success('配置已复制到剪贴板')
|
||||
} catch (err) {
|
||||
ElMessage.error('复制失败: ' + err)
|
||||
}
|
||||
}
|
||||
|
||||
const openTestDialog = (tool) => {
|
||||
currentTestingTool.value = tool
|
||||
apiDialogResponse.value = null // 清空之前的API响应
|
||||
|
||||
// 重置并初始化表单数据
|
||||
for (const key in testParamsForm) {
|
||||
delete testParamsForm[key]
|
||||
}
|
||||
if (tool.inputSchema && tool.inputSchema.properties) {
|
||||
Object.keys(tool.inputSchema.properties).forEach(propName => {
|
||||
const propDetails = tool.inputSchema.properties[propName]
|
||||
// 设置默认值,优先使用 schema 中的 default,否则根据类型给初始值
|
||||
if (propDetails.default !== undefined) {
|
||||
testParamsForm[propName] = propDetails.default
|
||||
} else if (propDetails.type === 'boolean') {
|
||||
testParamsForm[propName] = false
|
||||
} else if (propDetails.type === 'number') {
|
||||
testParamsForm[propName] = null // 或者 0
|
||||
} else if (propDetails.type === 'object' || propDetails.type === 'array') {
|
||||
testParamsForm[propName] = '' // 对象和数组类型,默认为空字符串,提示用户输入JSON
|
||||
} else {
|
||||
testParamsForm[propName] = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
testDialogVisible.value = true
|
||||
// 清除表单校验状态
|
||||
if (testParamsFormRef.value) {
|
||||
testParamsFormRef.value.clearValidate()
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseDialog = (done) => {
|
||||
apiDialogResponse.value = null
|
||||
done()
|
||||
}
|
||||
|
||||
const handleDialogTestTool = async () => {
|
||||
if (!currentTestingTool.value) {
|
||||
ElMessage.warning('没有选中的测试工具')
|
||||
return
|
||||
}
|
||||
if (testParamsFormRef.value) {
|
||||
testParamsFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
|
||||
const toolName = currentTestingTool.value.name
|
||||
const payload = { ...testParamsForm }
|
||||
|
||||
if (currentTestingTool.value.inputSchema && currentTestingTool.value.inputSchema.properties) {
|
||||
Object.keys(currentTestingTool.value.inputSchema.properties).forEach(propName => {
|
||||
const propDetails = currentTestingTool.value.inputSchema.properties[propName]
|
||||
if ((propDetails.type === 'object' || propDetails.type === 'array') && payload[propName] && typeof payload[propName] === 'string') {
|
||||
try {
|
||||
payload[propName] = JSON.parse(payload[propName])
|
||||
} catch (e) {
|
||||
ElMessage.error(`参数 ${propName} 的JSON格式无效: ${e.message}`)
|
||||
throw new Error(`参数 ${propName} JSON无效`);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const res = await mcpTest({
|
||||
name:toolName,
|
||||
arguments:payload
|
||||
})
|
||||
apiDialogResponse.value = res.data
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('API调用成功')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
426
src/view/systemTools/autoCode/picture.vue
Normal file
426
src/view/systemTools/autoCode/picture.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
href="https://www.gin-vue-admin.com/empower/"
|
||||
title="此功能只针对授权用户开放,点我【购买授权】"
|
||||
/>
|
||||
<div class="gva-search-box">
|
||||
<div class="text-xl mb-2 text-gray-600">
|
||||
AI前端工程师<a
|
||||
class="text-blue-600 text-sm ml-4"
|
||||
href="https://plugin.gin-vue-admin.com/#/layout/userInfo/center"
|
||||
target="_blank"
|
||||
>获取AiPath</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 选项模式 -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-3">
|
||||
<div class="text-base font-medium mb-2">页面用途</div>
|
||||
<el-radio-group v-model="pageType" class="mb-2" @change="handlePageTypeChange">
|
||||
<el-radio label="企业官网">企业官网</el-radio>
|
||||
<el-radio label="电商页面">电商页面</el-radio>
|
||||
<el-radio label="个人博客">个人博客</el-radio>
|
||||
<el-radio label="产品介绍">产品介绍</el-radio>
|
||||
<el-radio label="活动落地页">活动落地页</el-radio>
|
||||
<el-radio label="其他">其他</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input v-if="pageType === '其他'" v-model="pageTypeCustom" placeholder="请输入页面用途" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-base font-medium mb-2">主要内容板块</div>
|
||||
<el-checkbox-group v-model="contentBlocks" class="flex flex-wrap gap-2 mb-2">
|
||||
<el-checkbox label="Banner轮播图">Banner轮播图</el-checkbox>
|
||||
<el-checkbox label="产品/服务介绍">产品/服务介绍</el-checkbox>
|
||||
<el-checkbox label="功能特点展示">功能特点展示</el-checkbox>
|
||||
<el-checkbox label="客户案例">客户案例</el-checkbox>
|
||||
<el-checkbox label="团队介绍">团队介绍</el-checkbox>
|
||||
<el-checkbox label="联系表单">联系表单</el-checkbox>
|
||||
<el-checkbox label="新闻/博客列表">新闻/博客列表</el-checkbox>
|
||||
<el-checkbox label="价格表">价格表</el-checkbox>
|
||||
<el-checkbox label="FAQ/常见问题">FAQ/常见问题</el-checkbox>
|
||||
<el-checkbox label="用户评价">用户评价</el-checkbox>
|
||||
<el-checkbox label="数据统计">数据统计</el-checkbox>
|
||||
<el-checkbox label="商品列表">商品列表</el-checkbox>
|
||||
<el-checkbox label="商品卡片">商品卡片</el-checkbox>
|
||||
<el-checkbox label="购物车">购物车</el-checkbox>
|
||||
<el-checkbox label="结算页面">结算页面</el-checkbox>
|
||||
<el-checkbox label="订单跟踪">订单跟踪</el-checkbox>
|
||||
<el-checkbox label="商品分类">商品分类</el-checkbox>
|
||||
<el-checkbox label="热门推荐">热门推荐</el-checkbox>
|
||||
<el-checkbox label="限时特惠">限时特惠</el-checkbox>
|
||||
<el-checkbox label="其他">其他</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<el-input v-if="contentBlocks.includes('其他')" v-model="contentBlocksCustom" placeholder="请输入其他内容板块" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-base font-medium mb-2">风格偏好</div>
|
||||
<el-radio-group v-model="stylePreference" class="mb-2">
|
||||
<el-radio label="简约">简约</el-radio>
|
||||
<el-radio label="科技感">科技感</el-radio>
|
||||
<el-radio label="温馨">温馨</el-radio>
|
||||
<el-radio label="专业">专业</el-radio>
|
||||
<el-radio label="创意">创意</el-radio>
|
||||
<el-radio label="复古">复古</el-radio>
|
||||
<el-radio label="奢华">奢华</el-radio>
|
||||
<el-radio label="其他">其他</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input v-if="stylePreference === '其他'" v-model="stylePreferenceCustom" placeholder="请输入风格偏好" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-base font-medium mb-2">设计布局</div>
|
||||
<el-radio-group v-model="layoutDesign" class="mb-2">
|
||||
<el-radio label="单栏布局">单栏布局</el-radio>
|
||||
<el-radio label="双栏布局">双栏布局</el-radio>
|
||||
<el-radio label="三栏布局">三栏布局</el-radio>
|
||||
<el-radio label="网格布局">网格布局</el-radio>
|
||||
<el-radio label="画廊布局">画廊布局</el-radio>
|
||||
<el-radio label="瀑布流">瀑布流</el-radio>
|
||||
<el-radio label="卡片式">卡片式</el-radio>
|
||||
<el-radio label="侧边栏+内容布局">侧边栏+内容布局</el-radio>
|
||||
<el-radio label="分屏布局">分屏布局</el-radio>
|
||||
<el-radio label="全屏滚动布局">全屏滚动布局</el-radio>
|
||||
<el-radio label="混合布局">混合布局</el-radio>
|
||||
<el-radio label="响应式">响应式</el-radio>
|
||||
<el-radio label="其他">其他</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input v-if="layoutDesign === '其他'" v-model="layoutDesignCustom" placeholder="请输入设计布局" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-base font-medium mb-2">配色方案</div>
|
||||
<el-radio-group v-model="colorScheme" class="mb-2">
|
||||
<el-radio label="蓝色系">蓝色系</el-radio>
|
||||
<el-radio label="绿色系">绿色系</el-radio>
|
||||
<el-radio label="红色系">红色系</el-radio>
|
||||
<el-radio label="黑白灰">黑白灰</el-radio>
|
||||
<el-radio label="纯黑白">纯黑白</el-radio>
|
||||
<el-radio label="暖色调">暖色调</el-radio>
|
||||
<el-radio label="冷色调">冷色调</el-radio>
|
||||
<el-radio label="其他">其他</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input v-if="colorScheme === '其他'" v-model="colorSchemeCustom" placeholder="请输入配色方案" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细描述输入框 -->
|
||||
<div class="relative">
|
||||
<div class="text-base font-medium mb-2">详细描述(可选)</div>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:maxlength="2000"
|
||||
:placeholder="placeholder"
|
||||
:rows="5"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
@blur="handleBlur"
|
||||
@focus="handleFocus"
|
||||
/>
|
||||
<div class="flex absolute right-2 bottom-2">
|
||||
<el-tooltip effect="light">
|
||||
<template #content>
|
||||
<div>
|
||||
此功能仅针对授权用户开放,前往<a
|
||||
class="text-blue-600"
|
||||
href="https://www.gin-vue-admin.com/empower/"
|
||||
target="_blank"
|
||||
>购买授权</a
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="llmAutoFunc()"
|
||||
>
|
||||
<el-icon size="18">
|
||||
<ai-gva/>
|
||||
</el-icon>
|
||||
生成
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!outPut">
|
||||
<el-empty :image-size="200"/>
|
||||
</div>
|
||||
<div v-if="outPut && htmlFromLLM">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="页面预览">
|
||||
<div class="h-[500px] overflow-auto bg-gray-50 p-4 rounded">
|
||||
<div v-if="!loadedComponents" class="text-gray-500 text-center py-4">
|
||||
组件加载中...
|
||||
</div>
|
||||
<component
|
||||
v-else
|
||||
:is="loadedComponents"
|
||||
class="vue-component-container w-full"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="源代码">
|
||||
<div class="relative h-[500px] overflow-auto bg-gray-50 p-4 rounded">
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="DocumentCopy"
|
||||
class="absolute top-2 right-2 px-2 py-1"
|
||||
@click="copySnippet(htmlFromLLM)"
|
||||
plain
|
||||
>
|
||||
复制
|
||||
</el-button>
|
||||
<pre class="mt-10 whitespace-pre-wrap">{{ htmlFromLLM }}</pre>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createWeb } from '@/api/autoCode'
|
||||
import { ref, reactive, markRaw } from 'vue'
|
||||
import * as Vue from "vue";
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { DocumentCopy } from '@element-plus/icons-vue'
|
||||
import { loadModule } from "vue3-sfc-loader";
|
||||
|
||||
defineOptions({
|
||||
name: 'Picture'
|
||||
})
|
||||
|
||||
const handleFocus = () => {
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
llmAutoFunc()
|
||||
}
|
||||
}
|
||||
|
||||
// 复制方法:把某个字符串写进剪贴板
|
||||
const copySnippet = (vueString) => {
|
||||
navigator.clipboard.writeText(vueString)
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
message: '复制成功',
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
ElMessage({
|
||||
message: '复制失败',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 选项模式相关变量
|
||||
const pageType = ref('企业官网')
|
||||
const pageTypeCustom = ref('')
|
||||
const contentBlocks = ref(['Banner轮播图', '产品/服务介绍'])
|
||||
const contentBlocksCustom = ref('')
|
||||
const stylePreference = ref('简约')
|
||||
const stylePreferenceCustom = ref('')
|
||||
const layoutDesign = ref('响应式')
|
||||
const layoutDesignCustom = ref('')
|
||||
const colorScheme = ref('蓝色系')
|
||||
const colorSchemeCustom = ref('')
|
||||
|
||||
// 页面用途与内容板块的推荐映射关系
|
||||
const pageTypeContentMap = {
|
||||
'企业官网': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '客户案例', '联系表单'],
|
||||
'电商页面': ['Banner轮播图', '商品列表', '商品卡片', '购物车', '商品分类', '热门推荐', '限时特惠', '结算页面', '用户评价'],
|
||||
'个人博客': ['Banner轮播图', '新闻/博客列表', '用户评价', '联系表单'],
|
||||
'产品介绍': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '价格表', 'FAQ/常见问题'],
|
||||
'活动落地页': ['Banner轮播图', '功能特点展示', '联系表单', '数据统计']
|
||||
}
|
||||
|
||||
const prompt = ref('')
|
||||
|
||||
// 判断是否返回的标志
|
||||
const outPut = ref(false)
|
||||
// 容纳llm返回的vue组件代码
|
||||
const htmlFromLLM = ref("")
|
||||
|
||||
// 存储加载的组件
|
||||
const loadedComponents = ref(null)
|
||||
|
||||
const loadVueComponent = async (vueCode) => {
|
||||
try {
|
||||
// 使用内存中的虚拟路径
|
||||
const fakePath = `virtual:component-0.vue`
|
||||
|
||||
const component = defineAsyncComponent({
|
||||
loader: async () => {
|
||||
try {
|
||||
const options = {
|
||||
moduleCache: {
|
||||
vue: Vue,
|
||||
},
|
||||
getFile(url) {
|
||||
// 处理所有可能的URL格式,包括相对路径、绝对路径等
|
||||
// 提取路径的最后部分,忽略查询参数
|
||||
const fileName = url.split('/').pop().split('?')[0]
|
||||
const componentFileName = fakePath.split('/').pop()
|
||||
|
||||
// 如果文件名包含我们的组件名称,或者url完全匹配fakePath
|
||||
if (fileName === componentFileName || url === fakePath ||
|
||||
url === `./component/0.vue`) {
|
||||
return Promise.resolve({
|
||||
type: '.vue',
|
||||
getContentData: () => vueCode
|
||||
})
|
||||
}
|
||||
|
||||
console.warn('请求未知文件:', url)
|
||||
return Promise.reject(new Error(`找不到文件: ${url}`))
|
||||
},
|
||||
addStyle(textContent) {
|
||||
// 不再将样式添加到document.head,而是返回样式内容
|
||||
// 稍后会将样式添加到Shadow DOM中
|
||||
return textContent
|
||||
},
|
||||
handleModule(type, source, path, options) {
|
||||
// 默认处理器
|
||||
return undefined
|
||||
},
|
||||
log(type, ...args) {
|
||||
console.log(`[vue3-sfc-loader] [${type}]`, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试加载组件
|
||||
const comp = await loadModule(fakePath, options)
|
||||
return comp.default || comp
|
||||
} catch (error) {
|
||||
console.error('组件加载详细错误:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
loadingComponent: {
|
||||
template: '<div>加载中...</div>'
|
||||
},
|
||||
errorComponent: {
|
||||
props: ['error'],
|
||||
template: '<div>组件加载失败: {{ error && error.message }}</div>',
|
||||
setup(props) {
|
||||
console.error('错误组件收到的错误:', props.error)
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 添加超时和重试选项
|
||||
timeout: 30000,
|
||||
delay: 200,
|
||||
suspensible: false,
|
||||
onError(error, retry, fail) {
|
||||
console.error('加载错误,细节:', error)
|
||||
fail()
|
||||
}
|
||||
})
|
||||
|
||||
// 创建一个包装组件,使用Shadow DOM隔离样式
|
||||
const ShadowWrapper = {
|
||||
name: 'ShadowWrapper',
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
render() {
|
||||
return Vue.h('div', { class: 'shadow-wrapper' })
|
||||
},
|
||||
mounted() {
|
||||
// 创建Shadow DOM
|
||||
const shadowRoot = this.$el.attachShadow({ mode: 'open' })
|
||||
|
||||
// 创建一个容器元素
|
||||
const container = document.createElement('div')
|
||||
container.className = 'shadow-container'
|
||||
shadowRoot.appendChild(container)
|
||||
|
||||
// 提取组件中的样式
|
||||
const styleContent = vueCode.match(/<style[^>]*>([\s\S]*?)<\/style>/i)?.[1] || ''
|
||||
|
||||
// 创建样式元素并添加到Shadow DOM
|
||||
if (styleContent) {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = styleContent
|
||||
shadowRoot.appendChild(style)
|
||||
}
|
||||
|
||||
// 创建Vue应用并挂载到Shadow DOM容器中
|
||||
const app = Vue.createApp({
|
||||
render: () => Vue.h(component)
|
||||
})
|
||||
app.mount(container)
|
||||
}
|
||||
}
|
||||
|
||||
loadedComponents.value = markRaw(ShadowWrapper)
|
||||
return ShadowWrapper
|
||||
} catch (error) {
|
||||
console.error('组件创建总错误:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 当页面用途改变时,更新内容板块的选择
|
||||
const handlePageTypeChange = (value) => {
|
||||
if (value !== '其他' && pageTypeContentMap[value]) {
|
||||
contentBlocks.value = [...pageTypeContentMap[value]]
|
||||
}
|
||||
}
|
||||
|
||||
const llmAutoFunc = async () => {
|
||||
// 构建完整的描述,包含选项模式的选择
|
||||
let fullPrompt = ''
|
||||
|
||||
// 添加页面用途
|
||||
fullPrompt += `页面用途: ${pageType.value === '其他' ? pageTypeCustom.value : pageType.value}\n`
|
||||
|
||||
// 添加内容板块
|
||||
fullPrompt += '主要内容板块: '
|
||||
const blocks = contentBlocks.value.filter(block => block !== '其他')
|
||||
if (contentBlocksCustom.value) {
|
||||
blocks.push(contentBlocksCustom.value)
|
||||
}
|
||||
fullPrompt += blocks.join(', ') + '\n'
|
||||
|
||||
// 添加风格偏好
|
||||
fullPrompt += `风格偏好: ${stylePreference.value === '其他' ? stylePreferenceCustom.value : stylePreference.value}\n`
|
||||
|
||||
// 添加设计布局
|
||||
fullPrompt += `设计布局: ${layoutDesign.value === '其他' ? layoutDesignCustom.value : layoutDesign.value}\n`
|
||||
|
||||
// 添加配色方案
|
||||
fullPrompt += `配色方案: ${colorScheme.value === '其他' ? colorSchemeCustom.value : colorScheme.value}\n`
|
||||
|
||||
// 添加用户的详细描述
|
||||
if (prompt.value) {
|
||||
fullPrompt += `\n详细描述: ${prompt.value}`
|
||||
}
|
||||
|
||||
const res = await createWeb({web: fullPrompt, command: 'createWeb'})
|
||||
if (res.code === 0) {
|
||||
outPut.value = true
|
||||
// 添加返回的Vue组件代码到数组
|
||||
htmlFromLLM.value = res.data
|
||||
// 加载新生成的组件
|
||||
await loadVueComponent(res.data)
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = ref(`补充您对页面的其他要求或特殊需求,例如:特别强调的元素、参考网站、交互效果等。`)
|
||||
</script>
|
@@ -123,6 +123,8 @@
|
||||
callback(new Error('不能为中文'))
|
||||
} else if (/^\d+$/.test(value[0])) {
|
||||
callback(new Error('不能够以数字开头'))
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
||||
callback(new Error('只能包含英文字母、数字和下划线'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
|
@@ -509,7 +509,15 @@ JOINS模式下不支持导入
|
||||
{
|
||||
label: 'NOT BETWEEN',
|
||||
value: 'NOT BETWEEN'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'IN',
|
||||
value: 'IN'
|
||||
},
|
||||
{
|
||||
label: 'NOT IN',
|
||||
value: 'NOT IN'
|
||||
},
|
||||
])
|
||||
|
||||
const addCondition = () => {
|
||||
|
@@ -202,6 +202,9 @@
|
||||
<el-form-item label="是否为ssl">
|
||||
<el-switch v-model="config.email['is-ssl']" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否LoginAuth认证">
|
||||
<el-switch v-model="config.email['is-loginauth']" />
|
||||
</el-form-item>
|
||||
<el-form-item label="secret">
|
||||
<el-input
|
||||
v-model.trim="config.email.secret"
|
||||
@@ -938,7 +941,7 @@
|
||||
</el-form>
|
||||
<div class="mt-4">
|
||||
<el-button type="primary" @click="update">立即更新 </el-button>
|
||||
<el-button type="primary" @click="reload">重启服务 </el-button>
|
||||
<el-button type="primary" @click="reload">重载服务 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1010,7 +1013,7 @@
|
||||
}
|
||||
initForm()
|
||||
const reload = () => {
|
||||
ElMessageBox.confirm('确定要重启服务?', '警告', {
|
||||
ElMessageBox.confirm('确定要重载服务?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
|
991
src/view/systemTools/version/version.vue
Normal file
991
src/view/systemTools/version/version.vue
Normal file
@@ -0,0 +1,991 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline"
|
||||
@keyup.enter="onSubmit">
|
||||
<el-form-item label="创建日期" prop="createdAtRange">
|
||||
<template #label>
|
||||
<span>
|
||||
创建日期
|
||||
<el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)">
|
||||
<el-icon>
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<el-date-picker v-model="searchInfo.createdAtRange" class="w-[380px]" type="datetimerange" range-separator="至"
|
||||
start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版本名称" prop="versionName">
|
||||
<el-input v-model="searchInfo.versionName" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版本号" prop="versionCode">
|
||||
<el-input v-model="searchInfo.versionCode" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
|
||||
|
||||
|
||||
<template v-if="showAllQuery">
|
||||
<!-- 将需要控制显示状态的查询条件添加到此范围内 -->
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
<el-button link type="primary" icon="arrow-down" @click="showAllQuery = true"
|
||||
v-if="!showAllQuery">展开</el-button>
|
||||
<el-button link type="primary" icon="arrow-up" @click="showAllQuery = false" v-else>收起</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="success" icon="download" @click="openExportDialog">创建发版</el-button>
|
||||
<el-button type="warning" icon="upload" @click="openImportDialog">导入版本</el-button>
|
||||
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length"
|
||||
@click="onDelete">删除</el-button>
|
||||
</div>
|
||||
<el-table ref="multipleTable" style="width: 100%" tooltip-effect="dark" :data="tableData" row-key="ID"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column sortable align="left" label="日期" prop="CreatedAt" width="180">
|
||||
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="left" label="版本名称" prop="versionName" width="120" />
|
||||
|
||||
<el-table-column align="left" label="版本号" prop="versionCode" width="120" />
|
||||
|
||||
<el-table-column align="left" label="操作" fixed="right" min-width="320">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon
|
||||
style="margin-right: 5px">
|
||||
<InfoFilled />
|
||||
</el-icon>查看</el-button>
|
||||
<el-button type="success" link icon="download" class="table-button"
|
||||
@click="downloadJson(scope.row)">下载发版包</el-button>
|
||||
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination layout="total, sizes, prev, pager, next, jumper" :current-page="page" :page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]" :total="total" @current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="detailShow" :show-close="true"
|
||||
:before-close="closeDetailShow" title="查看">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="版本名称">
|
||||
{{ detailForm.versionName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="版本号">
|
||||
{{ detailForm.versionCode }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="版本描述">
|
||||
{{ detailForm.description }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 导出版本抽屉 -->
|
||||
<el-drawer v-model="exportDialogVisible" title="创建发版" direction="rtl" size="80%" :before-close="closeExportDialog" :show-close="false">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">创建发版</span>
|
||||
<div>
|
||||
<el-button @click="closeExportDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleExport" :loading="exportLoading">创建发版</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="exportForm" label-width="100px">
|
||||
<el-form-item label="版本名称" required>
|
||||
<el-input v-model="exportForm.versionName" placeholder="请输入版本名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版本号" required>
|
||||
<el-input v-model="exportForm.versionCode" placeholder="请输入版本号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版本描述">
|
||||
<el-input v-model="exportForm.description" type="textarea" placeholder="请输入版本描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发版信息">
|
||||
<div class="flex gap-3 w-full">
|
||||
<!-- 菜单选择 -->
|
||||
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
|
||||
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||
<span class="m-0 text-gray-800 text-base font-medium">选择菜单</span>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
|
||||
<el-input v-model="menuFilterText" placeholder="输入关键字进行过滤" clearable size="small" />
|
||||
</div>
|
||||
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||
<el-tree ref="menuTreeRef" :data="menuTreeData" :default-checked-keys="selectedMenuIds"
|
||||
:props="menuTreeProps" default-expand-all highlight-current node-key="ID" show-checkbox
|
||||
:filter-node-method="filterMenuNode" @check="onMenuCheck" class="menu-tree">
|
||||
<template #default="{ node }">
|
||||
<span class="flex-1 flex items-center justify-between text-sm pr-2">
|
||||
<span>{{ node.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API选择 -->
|
||||
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
|
||||
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||
<span class="m-0 text-gray-800 text-base font-medium">选择API</span>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
|
||||
<el-input v-model="apiFilterTextName" placeholder="按名称过滤" clearable size="small"
|
||||
style="margin-bottom: 8px" />
|
||||
<el-input v-model="apiFilterTextPath" placeholder="按路径过滤" clearable size="small" />
|
||||
</div>
|
||||
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||
<el-tree ref="apiTreeRef" :data="apiTreeData" :default-checked-keys="selectedApiIds"
|
||||
:props="apiTreeProps" default-expand-all highlight-current node-key="onlyId" show-checkbox
|
||||
:filter-node-method="filterApiNode" @check="onApiCheck" class="api-tree">
|
||||
<template #default="{ data }">
|
||||
<div class="flex items-center justify-between w-full pr-1">
|
||||
<span>{{ data.description }}</span>
|
||||
<el-tooltip :content="data.path">
|
||||
<span class="max-w-[240px] break-all overflow-ellipsis overflow-hidden">
|
||||
{{ data.path }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字典选择 -->
|
||||
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
|
||||
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||
<span class="m-0 text-gray-800 text-base font-medium">选择字典</span>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
|
||||
<el-input v-model="dictFilterText" placeholder="输入关键字进行过滤" clearable size="small" />
|
||||
</div>
|
||||
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||
<el-tree ref="dictTreeRef" :data="dictTreeData" :default-checked-keys="selectedDictIds"
|
||||
:props="dictTreeProps" default-expand-all highlight-current node-key="ID" show-checkbox
|
||||
:filter-node-method="filterDictNode" @check="onDictCheck" class="dict-tree">
|
||||
<template #default="{ data }">
|
||||
<div class="flex items-center justify-between w-full pr-1">
|
||||
<span>{{ data.name || data.label }}</span>
|
||||
<el-tooltip :content="data.desc || (data.value ? `值: ${data.value}` : '')">
|
||||
<span class="text-gray-500 text-xs ml-2">
|
||||
{{ data.type || (data.value ? `值: ${data.value}` : '') }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 导入版本抽屉 -->
|
||||
<el-drawer v-model="importDialogVisible" title="导入版本" direction="rtl" size="80%" :before-close="closeImportDialog" :show-close="false">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">导入版本</span>
|
||||
<div>
|
||||
<el-button @click="closeImportDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleImport" :loading="importLoading"
|
||||
:disabled="!importJsonContent.trim()">导入</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="上传文件">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将JSON文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
只能上传JSON文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本JSON">
|
||||
<el-input v-model="importJsonContent" type="textarea" :rows="20" placeholder="请粘贴版本JSON"
|
||||
@input="handleJsonContentChange" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预览内容" v-if="importPreviewData">
|
||||
<div class="flex flex-col flex-1 gap-4 border border-gray-300 rounded p-4 bg-gray-50">
|
||||
<div class="flex gap-3 w-full">
|
||||
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
|
||||
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
|
||||
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||
<h3 class="m-0 text-gray-800 text-base font-medium">菜单 ({{ getTotalMenuCount() }}项)</h3>
|
||||
</div>
|
||||
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||
<el-tree
|
||||
:data="previewMenuTreeData"
|
||||
:props="menuTreeProps"
|
||||
node-key="name"
|
||||
:expand-on-click-node="false"
|
||||
:check-on-click-node="false"
|
||||
:show-checkbox="false"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex-1 flex items-center justify-between text-sm pr-2">
|
||||
<span>{{ data.meta?.title || data.title }}</span>
|
||||
<span class="text-gray-500 text-xs ml-2">{{ data.path }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
|
||||
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
|
||||
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||
<h3 class="m-0 text-gray-800 text-base font-medium">API ({{ importPreviewData.apis?.length || 0 }}项)</h3>
|
||||
</div>
|
||||
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||
<el-tree
|
||||
:data="previewApiTreeData"
|
||||
:props="apiTreeProps"
|
||||
node-key="ID"
|
||||
:expand-on-click-node="false"
|
||||
:check-on-click-node="false"
|
||||
:show-checkbox="false"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex-1 flex items-center justify-between text-sm pr-2">
|
||||
<span>{{ data.description }}</span>
|
||||
<span class="text-gray-500 text-xs ml-2">{{ data.path }} [{{ data.method }}]</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
|
||||
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
|
||||
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
|
||||
<h3 class="m-0 text-gray-800 text-base font-medium">字典 ({{ importPreviewData.dictionaries?.length || 0 }}项)</h3>
|
||||
</div>
|
||||
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
|
||||
<el-tree
|
||||
:data="previewDictTreeData"
|
||||
:props="dictTreeProps"
|
||||
node-key="ID"
|
||||
:expand-on-click-node="false"
|
||||
:check-on-click-node="false"
|
||||
:show-checkbox="false"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex-1 flex items-center justify-between text-sm pr-2">
|
||||
<span>{{ data.name || data.label }}</span>
|
||||
<span class="text-gray-500 text-xs ml-2">
|
||||
{{ data.type || (data.value ? `值: ${data.value}` : '') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
deleteSysVersion,
|
||||
deleteSysVersionByIds,
|
||||
findSysVersion,
|
||||
getSysVersionList,
|
||||
exportVersion,
|
||||
importVersion,
|
||||
downloadVersionJson
|
||||
} from '@/api/version'
|
||||
|
||||
// 导入菜单和API相关接口
|
||||
import { getMenuList } from '@/api/menu'
|
||||
import { getApiList } from '@/api/api'
|
||||
import { getSysDictionaryList } from '@/api/sysDictionary'
|
||||
|
||||
// 全量引入格式化工具 请按需保留
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useAppStore } from "@/pinia"
|
||||
|
||||
defineOptions({
|
||||
name: 'SysVersion'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 控制更多查询条件显示/隐藏状态
|
||||
const showAllQuery = ref(false)
|
||||
|
||||
// 导出相关数据
|
||||
const exportDialogVisible = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const exportForm = ref({
|
||||
versionName: '',
|
||||
versionCode: '',
|
||||
description: '',
|
||||
menuIds: [],
|
||||
apiIds: [],
|
||||
dictIds: []
|
||||
})
|
||||
|
||||
// 树形结构相关数据
|
||||
const menuTreeData = ref([])
|
||||
const apiTreeData = ref([])
|
||||
const dictTreeData = ref([])
|
||||
const selectedMenuIds = ref([])
|
||||
const selectedApiIds = ref([])
|
||||
const selectedDictIds = ref([])
|
||||
const menuFilterText = ref('')
|
||||
const apiFilterTextName = ref('')
|
||||
const apiFilterTextPath = ref('')
|
||||
const dictFilterText = ref('')
|
||||
|
||||
// 树形组件引用
|
||||
const menuTreeRef = ref(null)
|
||||
const apiTreeRef = ref(null)
|
||||
const dictTreeRef = ref(null)
|
||||
|
||||
// 树形属性配置
|
||||
const menuTreeProps = ref({
|
||||
children: 'children',
|
||||
label: function (data) {
|
||||
return data.meta?.title || data.title
|
||||
}
|
||||
})
|
||||
|
||||
const apiTreeProps = ref({
|
||||
children: 'children',
|
||||
label: 'description'
|
||||
})
|
||||
|
||||
const dictTreeProps = ref({
|
||||
children: 'sysDictionaryDetails',
|
||||
label: function (data) {
|
||||
// 如果是字典主项,显示字典名称
|
||||
if (data.name) {
|
||||
return data.name
|
||||
}
|
||||
// 如果是字典详情项,显示标签
|
||||
if (data.label) {
|
||||
return data.label
|
||||
}
|
||||
return '未知项'
|
||||
}
|
||||
})
|
||||
|
||||
// 导入相关数据
|
||||
const importDialogVisible = ref(false)
|
||||
const importLoading = ref(false)
|
||||
const importJsonContent = ref('')
|
||||
const importPreviewData = ref(null)
|
||||
const uploadRef = ref(null)
|
||||
const previewMenuTreeData = ref([])
|
||||
const previewApiTreeData = ref([])
|
||||
const previewDictTreeData = ref([])
|
||||
|
||||
|
||||
|
||||
const elSearchFormRef = ref()
|
||||
|
||||
// =========== 表格控制部分 ===========
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const onSubmit = () => {
|
||||
elSearchFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
page.value = 1
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 修改页面容量
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getSysVersionList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
// ============== 表格控制部分结束 ===============
|
||||
|
||||
// 多选数据
|
||||
const multipleSelection = ref([])
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteSysVersionFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
// 多选删除
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const IDs = []
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要删除的数据'
|
||||
})
|
||||
return
|
||||
}
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.map(item => {
|
||||
IDs.push(item.ID)
|
||||
})
|
||||
const res = await deleteSysVersionByIds({ IDs })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === IDs.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteSysVersionFunc = async (row) => {
|
||||
const res = await deleteSysVersion({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const detailForm = ref({})
|
||||
|
||||
// 查看详情控制标记
|
||||
const detailShow = ref(false)
|
||||
|
||||
|
||||
// 打开详情弹窗
|
||||
const openDetailShow = () => {
|
||||
detailShow.value = true
|
||||
}
|
||||
|
||||
|
||||
// 打开详情
|
||||
const getDetails = async (row) => {
|
||||
// 打开弹窗
|
||||
const res = await findSysVersion({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
detailForm.value = res.data
|
||||
openDetailShow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 关闭详情弹窗
|
||||
const closeDetailShow = () => {
|
||||
detailShow.value = false
|
||||
detailForm.value = {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 获取菜单和API列表
|
||||
const getMenuAndApiList = async () => {
|
||||
try {
|
||||
// 获取菜单列表
|
||||
const menuRes = await getMenuList()
|
||||
if (menuRes.code === 0) {
|
||||
menuTreeData.value = menuRes.data || []
|
||||
}
|
||||
|
||||
// 获取API列表
|
||||
const apiRes = await getApiList({ page: 1, pageSize: 9999 })
|
||||
if (apiRes.code === 0) {
|
||||
console.log('原始API数据:', apiRes.data)
|
||||
const apis = apiRes.data.list || []
|
||||
apiTreeData.value = buildApiTree(apis)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
ElMessage.error('获取菜单或API数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典列表
|
||||
const getDictList = async () => {
|
||||
try {
|
||||
const dictRes = await getSysDictionaryList({ page: 1, pageSize: 9999 })
|
||||
if (dictRes.code === 0) {
|
||||
dictTreeData.value = dictRes.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取字典数据失败:', error)
|
||||
ElMessage.error('获取字典数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 构建API树形结构
|
||||
const buildApiTree = (apis) => {
|
||||
const apiObj = {}
|
||||
apis.forEach((item) => {
|
||||
item.onlyId = 'p:' + item.path + 'm:' + item.method
|
||||
if (Object.prototype.hasOwnProperty.call(apiObj, item.apiGroup)) {
|
||||
apiObj[item.apiGroup].push(item)
|
||||
} else {
|
||||
Object.assign(apiObj, { [item.apiGroup]: [item] })
|
||||
}
|
||||
})
|
||||
const apiTree = []
|
||||
for (const key in apiObj) {
|
||||
const treeNode = {
|
||||
ID: key,
|
||||
description: key + '组',
|
||||
children: apiObj[key]
|
||||
}
|
||||
apiTree.push(treeNode)
|
||||
}
|
||||
return apiTree
|
||||
}
|
||||
|
||||
// 树形组件事件处理方法
|
||||
const filterMenuNode = (value, data) => {
|
||||
if (!value) return true
|
||||
const title = data.meta?.title || data.title || ''
|
||||
return title.indexOf(value) !== -1
|
||||
}
|
||||
|
||||
const filterApiNode = (value, data) => {
|
||||
if (!apiFilterTextName.value && !apiFilterTextPath.value) return true
|
||||
let matchesName, matchesPath
|
||||
if (!apiFilterTextName.value) {
|
||||
matchesName = true
|
||||
} else {
|
||||
matchesName = data.description && data.description.includes(apiFilterTextName.value)
|
||||
}
|
||||
if (!apiFilterTextPath.value) {
|
||||
matchesPath = true
|
||||
} else {
|
||||
matchesPath = data.path && data.path.includes(apiFilterTextPath.value)
|
||||
}
|
||||
return matchesName && matchesPath
|
||||
}
|
||||
|
||||
const filterDictNode = (value, data) => {
|
||||
if (!value) return true
|
||||
const name = data.name || ''
|
||||
const type = data.type || ''
|
||||
const desc = data.desc || ''
|
||||
const label = data.label || ''
|
||||
const dataValue = data.value || ''
|
||||
return name.indexOf(value) !== -1 ||
|
||||
type.indexOf(value) !== -1 ||
|
||||
desc.indexOf(value) !== -1 ||
|
||||
label.indexOf(value) !== -1 ||
|
||||
dataValue.indexOf(value) !== -1
|
||||
}
|
||||
|
||||
const onMenuCheck = (data, checked) => {
|
||||
if (checked.checkedKeys) {
|
||||
selectedMenuIds.value = checked.checkedKeys
|
||||
}
|
||||
}
|
||||
|
||||
const onApiCheck = (data, checked) => {
|
||||
if (checked.checkedKeys) {
|
||||
selectedApiIds.value = checked.checkedKeys
|
||||
}
|
||||
}
|
||||
|
||||
const onDictCheck = (data, checked) => {
|
||||
if (checked.checkedKeys) {
|
||||
selectedDictIds.value = checked.checkedKeys
|
||||
}
|
||||
}
|
||||
|
||||
// 监听过滤文本变化
|
||||
watch(menuFilterText, (val) => {
|
||||
if (menuTreeRef.value) {
|
||||
menuTreeRef.value.filter(val)
|
||||
}
|
||||
})
|
||||
|
||||
watch([apiFilterTextName, apiFilterTextPath], () => {
|
||||
if (apiTreeRef.value) {
|
||||
apiTreeRef.value.filter('')
|
||||
}
|
||||
})
|
||||
|
||||
watch(dictFilterText, (val) => {
|
||||
if (dictTreeRef.value) {
|
||||
dictTreeRef.value.filter(val)
|
||||
}
|
||||
})
|
||||
|
||||
// 导出相关方法
|
||||
const openExportDialog = async () => {
|
||||
exportDialogVisible.value = true
|
||||
await getMenuAndApiList()
|
||||
await getDictList()
|
||||
}
|
||||
|
||||
const closeExportDialog = () => {
|
||||
exportDialogVisible.value = false
|
||||
exportForm.value = {
|
||||
versionName: '',
|
||||
versionCode: '',
|
||||
description: '',
|
||||
menuIds: [],
|
||||
apiIds: [],
|
||||
dictIds: []
|
||||
}
|
||||
selectedMenuIds.value = []
|
||||
selectedApiIds.value = []
|
||||
selectedDictIds.value = []
|
||||
menuFilterText.value = ''
|
||||
apiFilterTextName.value = ''
|
||||
apiFilterTextPath.value = ''
|
||||
dictFilterText.value = ''
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!exportForm.value.versionName || !exportForm.value.versionCode) {
|
||||
ElMessage.warning('请填写版本名称和版本号')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
try {
|
||||
// 获取选中的菜单、API和字典
|
||||
const checkedMenus = menuTreeRef.value ? menuTreeRef.value.getCheckedNodes(false, true) : []
|
||||
const checkedApis = apiTreeRef.value ? apiTreeRef.value.getCheckedNodes(true) : []
|
||||
const checkedDicts = dictTreeRef.value ? dictTreeRef.value.getCheckedNodes(true) : []
|
||||
|
||||
const menuIds = checkedMenus.map(menu => menu.ID)
|
||||
const apiIds = checkedApis.map(api => api.ID)
|
||||
const dictIds = checkedDicts.map(dict => dict.ID)
|
||||
|
||||
exportForm.value.menuIds = menuIds
|
||||
exportForm.value.apiIds = apiIds
|
||||
exportForm.value.dictIds = dictIds
|
||||
|
||||
const res = await exportVersion(exportForm.value)
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '创建发版失败')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success('创建发版成功')
|
||||
closeExportDialog()
|
||||
getTableData() // 刷新表格数据
|
||||
} catch (error) {
|
||||
console.error('创建发版失败:', error)
|
||||
ElMessage.error('创建发版失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导入相关方法
|
||||
const openImportDialog = () => {
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
|
||||
const closeImportDialog = () => {
|
||||
importDialogVisible.value = false
|
||||
importJsonContent.value = ''
|
||||
importPreviewData.value = null
|
||||
previewMenuTreeData.value = []
|
||||
previewApiTreeData.value = []
|
||||
// 清理上传文件
|
||||
if (uploadRef.value) {
|
||||
uploadRef.value.clearFiles()
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传处理函数
|
||||
const handleFileChange = (file) => {
|
||||
if (!file.raw) return
|
||||
|
||||
// 验证文件类型
|
||||
if (!file.name.toLowerCase().endsWith('.json')) {
|
||||
ElMessage.error('只能上传JSON文件')
|
||||
uploadRef.value.clearFiles()
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const content = e.target.result
|
||||
// 验证JSON格式
|
||||
JSON.parse(content)
|
||||
importJsonContent.value = content
|
||||
handleJsonContentChange()
|
||||
ElMessage.success('文件上传成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('JSON文件格式错误')
|
||||
uploadRef.value.clearFiles()
|
||||
}
|
||||
}
|
||||
reader.readAsText(file.raw)
|
||||
}
|
||||
|
||||
const handleFileRemove = () => {
|
||||
importJsonContent.value = ''
|
||||
importPreviewData.value = null
|
||||
previewMenuTreeData.value = []
|
||||
previewApiTreeData.value = []
|
||||
}
|
||||
|
||||
// 计算菜单总数(递归计算所有菜单项)
|
||||
const getTotalMenuCount = () => {
|
||||
if (!importPreviewData.value?.menus) return 0
|
||||
|
||||
const countMenus = (menus) => {
|
||||
let count = 0
|
||||
menus.forEach(menu => {
|
||||
count += 1 // 当前菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
count += countMenus(menu.children) // 递归计算子菜单
|
||||
}
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
return countMenus(importPreviewData.value.menus)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleJsonContentChange = () => {
|
||||
if (!importJsonContent.value.trim()) {
|
||||
importPreviewData.value = null
|
||||
previewMenuTreeData.value = []
|
||||
previewApiTreeData.value = []
|
||||
previewDictTreeData.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(importJsonContent.value)
|
||||
|
||||
// 构建预览数据
|
||||
importPreviewData.value = {
|
||||
menus: data.menus || [],
|
||||
apis: data.apis || [],
|
||||
dictionaries: data.dictionaries || []
|
||||
}
|
||||
|
||||
// 直接使用菜单数据,因为它已经是树形结构(包含children字段)
|
||||
if (data.menus && data.menus.length > 0) {
|
||||
previewMenuTreeData.value = data.menus
|
||||
} else {
|
||||
previewMenuTreeData.value = []
|
||||
}
|
||||
|
||||
// 构建API树形数据(按分组组织)
|
||||
if (data.apis && data.apis.length > 0) {
|
||||
const apiGroups = {}
|
||||
data.apis.forEach(api => {
|
||||
const group = api.apiGroup || '未分组'
|
||||
if (!apiGroups[group]) {
|
||||
apiGroups[group] = {
|
||||
ID: `group_${group}`,
|
||||
description: group,
|
||||
path: '',
|
||||
method: '',
|
||||
children: []
|
||||
}
|
||||
}
|
||||
apiGroups[group].children.push(api)
|
||||
})
|
||||
previewApiTreeData.value = Object.values(apiGroups)
|
||||
} else {
|
||||
previewApiTreeData.value = []
|
||||
}
|
||||
|
||||
// 处理字典数据
|
||||
if (data.dictionaries && data.dictionaries.length > 0) {
|
||||
previewDictTreeData.value = data.dictionaries
|
||||
} else {
|
||||
previewDictTreeData.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('JSON解析失败:', error)
|
||||
importPreviewData.value = null
|
||||
previewMenuTreeData.value = []
|
||||
previewApiTreeData.value = []
|
||||
previewDictTreeData.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importJsonContent.value.trim()) {
|
||||
ElMessage.warning('请输入版本JSON')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(importJsonContent.value)
|
||||
} catch (error) {
|
||||
ElMessage.error('JSON格式错误,请检查输入内容')
|
||||
return
|
||||
}
|
||||
|
||||
importLoading.value = true
|
||||
try {
|
||||
const data = JSON.parse(importJsonContent.value)
|
||||
const res = await importVersion(data)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('导入成功')
|
||||
closeImportDialog()
|
||||
getTableData() // 刷新表格数据
|
||||
} else {
|
||||
ElMessage.error(res.msg || '导入失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
ElMessage.error('导入失败')
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载版本JSON
|
||||
const downloadJson = async (row) => {
|
||||
try {
|
||||
const res = await downloadVersionJson({ ID: row.ID })
|
||||
// 处理axios响应,获取实际的blob数据
|
||||
// 当responseType为blob时,axios拦截器会返回完整的response对象
|
||||
let blob
|
||||
if (res instanceof Blob) {
|
||||
blob = res
|
||||
} else if (res.data instanceof Blob) {
|
||||
blob = res.data
|
||||
} else {
|
||||
// 如果不是blob,可能是错误响应,尝试从response中获取
|
||||
blob = res
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${row.versionName}_${row.versionCode}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Element Plus 树形组件样式优化 */
|
||||
:deep(.el-tree) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__label) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__view) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user