添加动态页功能

This commit is contained in:
2025-06-05 15:39:57 +08:00
parent c45e65433b
commit 377913b5d4
26 changed files with 1333 additions and 4 deletions

View File

@ -174,7 +174,6 @@ const { data: messages } = useRequest(fetchMessage, {
.content {
height: calc(100vh - 64px - 20px);
margin: 10px;
padding: 24px;
background-color: white;
overflow-y: auto;
}

View File

@ -42,6 +42,11 @@ const router = createRouter({
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/TenantView.vue'),
},
{
path: '/system/page',
meta: {needLogin: true, needMenu: true},
component: () => import('@/views/PageView.vue'),
},
{
path: '/system/logger',
meta: { needLogin: true, needMenu: true },
@ -74,6 +79,14 @@ const router = createRouter({
},
],
},
{
path: '/page/designer',
component: () => import('@/views/lowcode/AmisDesignerView.vue'),
},
{
path: '/page/render',
component: () => import('@/views/lowcode/AmisRenderView.vue'),
},
],
})

View File

@ -552,6 +552,8 @@ const roleOpt = computed({
<style lang="scss" scoped>
.account {
padding: 24px;
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;

View File

@ -522,6 +522,8 @@ const pagination = computed(() => ({
<style lang="scss" scoped>
.authority {
padding: 24px;
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;

View File

@ -242,6 +242,8 @@ const pagination = computed(() => ({
<style lang="scss" scoped>
.tenant {
padding: 24px;
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;

View File

@ -345,6 +345,8 @@ const onSelectRecipient = (option) => {
<style lang="scss" scoped>
.tenant {
padding: 24px;
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;

View File

@ -0,0 +1,391 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import {computed, reactive, ref} from 'vue'
import {message} from 'ant-design-vue'
import {usePagination} from 'vue-request'
import router from '@/router/index.js'
const searchRef = ref()
const editRef = ref()
const state = reactive({
search: {},
searchData: {},
edit: {},
modal: {
title: '',
show: false,
},
data: {
title: '页面数据',
show: false,
},
})
const columns = [
{
title: 'ID',
dataIndex: 'id',
sorter: true,
width: 200,
ellipsis: true,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建人',
dataIndex: 'createBy',
maxWidth: 100,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({text}) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({text}) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 230,
fixed: 'right',
},
]
const rules = {}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const onDataChange = (record) => {
state.searchData = {pageId: record.id}
onTableDataChange()
state.data.show = true
}
const fetchPage = (params) => {
return axios
.get('/db/tPage?' + qs.stringify(params, {allowDots: true}))
.then(([, res]) => res.data)
}
const fetchData = (params) => {
return axios
.get('/db/tPageData?' + qs.stringify(params, {allowDots: true}))
.then(([, res]) => res.data)
}
const submitPage = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
state.edit.content = null;
axios.put('/db/tPage', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tPage', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deletePage = (record) => {
let params = {idList: record.id}
axios.delete('/db/tPage', {params}).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const deleteData = (record) => {
let params = {idList: record.id}
axios.delete('/db/tPageData', {params}).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableDataChange()
return message.success('删除数据成功')
}
})
}
const {
data: page,
run,
loading,
current,
pageSize,
total,
} = usePagination(fetchPage, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const {
data: data,
run: runData,
_,
current: currentData,
pageSize: pageSizeData,
total: totalData,
} = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
manual: true,
})
const onTableChange = (pagination, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{column: sorter?.field, asc: sorter?.order === 'ascend'}]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const onTableDataChange = (paginationData, filters, sorter) => {
// 排序条件
let s = {}
if (sorter) {
s.orders = [{column: sorter?.field, asc: sorter?.order === 'ascend'}]
}
// 筛选条件
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
runData({
current: paginationData?.current || current,
size: paginationData?.pageSize || pageSize,
...state.searchData,
...s,
...f,
})
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const paginationData = computed(() => ({
current: currentData.value,
pageSize: pageSizeData.value,
total: totalData.value,
}))
const designCustom = (record) => {
if (record?.id) {
router.push({path: '/page/designer', query: {qid: record.id}})
} else {
router.push({path: '/page/designer'})
}
}
const renderCustom = (record) => {
if (record?.pageId && record?.id) {
router.push({
path: '/page/render',
query: {qid: record.pageId, aid: record.id},
})
} else if (record.id) {
router.push({path: '/page/render', query: {qid: record.id}})
} else {
router.push({path: '/page/render'})
}
}
</script>
<template>
<div class="custom">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="描述" name="description">
<a-input v-model:value="state.search.description" placeholder="请输入描述"/>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:account:post']" type="primary" @click="designCustom()"
>新增
</a-button>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="page?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" @click="renderCustom(record)">查看</a-button>
<a-button type="link" @click="designCustom(record)">设计</a-button>
<a-button type="link" @click="onDataChange(record)">数据</a-button>
<a-button v-hasAuth="['system:account:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deletePage(record)">
<a-button v-hasAuth="['system:logger:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitPage">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述"/>
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:open="state.data.show" :title="state.data.title" width="80%">
<a-table
:columns="columns"
:data-source="data?.records"
:pagination="paginationData"
:scroll="{ x: '100%' }"
@change="onTableDataChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button type="link" @click="renderCustom(record)">查看</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:logger:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.custom {
padding: 24px;
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -501,6 +501,8 @@ const treeAuth = computed(() => fromArray(authorityOpts.value, { parentKey: 'par
<style lang="scss" scoped>
.role {
padding: 24px;
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;

View File

@ -406,6 +406,8 @@ const tenantOpt = computed({
<style lang="scss" scoped>
.tenant {
padding: 24px;
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;

View File

@ -0,0 +1,101 @@
<script setup>
import { DesktopOutlined, MobileOutlined } from '@ant-design/icons-vue'
// 引入一些样式依赖
import 'amis-ui/lib/themes/default.css'
import 'amis-ui/lib/themes/antd.css'
import 'amis-editor-core/lib/style.css'
import { applyReactInVue } from 'veaury'
import { Editor } from 'amis-editor' //引入编辑器
import { ref, shallowRef } from 'vue'
import axios from '@/http/api.js'
import { useRequest } from 'vue-request'
import { message } from 'ant-design-vue'
import { useRoute } from 'vue-router'
const qid = useRoute().query.qid
const AmisEditor = applyReactInVue(Editor) //使用编辑器
const previewModel = ref(false) //是否预览,实际开发中如果需要编辑和预览可以写一个change事件来改变这个值的状态
const mobileModel = ref(false) //是否是手机模式
const schema = shallowRef({}) //渲染表单的内容
const editorChanged = (value) => {
schema.value = value
}
// 加载页面结构
const fetchSchema = async (params) => {
if (qid) {
let [err, res] = await axios.get('/db/tPage/' + params.id)
if (err || res.code !== 0) {
message.error(err.msg || '获取页面结构失败')
} else {
schema.value = res.data?.content || {}
}
}
}
useRequest(fetchSchema, { defaultParams: [{ id: qid }] })
// 保存或新增页面结构
const submitSchema = async () => {
if (qid) {
let [err, res] = await axios.put('/db/tPage', { id: qid, content: schema.value })
if (err || res.code !== 0) {
message.error(err.msg || '更新页面结构失败')
} else {
message.success('更新页面结构成功')
}
} else {
let [err, res] = await axios.post('/db/tPage', { content: schema.value })
if (err || res.code !== 0) {
message.error(err.msg || '保存页面结构失败')
} else {
message.success('保存页面结构成功')
}
}
}
</script>
<template>
<a-page-header class="header" title="设计器" @back="() => $router.go(-1)">
<template #extra>
<a-radio-group v-model:value="mobileModel">
<a-radio-button :value="false">
<DesktopOutlined />
</a-radio-button>
<a-radio-button :value="true">
<MobileOutlined />
</a-radio-button>
</a-radio-group>
<a-button v-if="previewModel" @click="previewModel = false">编辑</a-button>
<a-space v-else>
<a-button @click="previewModel = true">预览</a-button>
<a-button type="primary" @click="submitSchema">保存</a-button>
</a-space>
</template>
</a-page-header>
<AmisEditor
theme="antd"
className="amis-editor"
:preview="previewModel"
:isMobile="mobileModel"
:value="schema"
:onChange="editorChanged"
/>
</template>
<style lang="scss" scoped>
:deep(.amis-editor) {
height: calc(100vh - 64px - 20px - 51px);
}
.header {
border-bottom: 1px solid #e8e9eb;
padding: 5px 10px;
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<a-page-header class="header" title="渲染器" @back="() => $router.go(-1)">
<template #extra>
<a-popover>
<template #content>
<a-qrcode ref="qrcodeCanvasRef" :value="qrcode" />
</template>
<a-button @click="dowloadChange">二维码</a-button>
</a-popover>
</template>
</a-page-header>
<a-result v-if="loading" status="warning" :title="tips" />
<div id="root" />
</template>
<script setup>
import 'amis/sdk/sdk.js'
import 'amis-ui/lib/themes/default.css'
import 'amis-ui/lib/themes/antd.css'
import { computed, ref, shallowRef } from 'vue'
import axios from '@/http/api.js'
import { useRoute } from 'vue-router'
const qid = useRoute().query.qid
const aid = useRoute().query.aid
const loading = ref(true)
const tips = ref('加载中,请稍后...')
const qrcode = computed(() => {
return window.location.href
})
const qrcodeCanvasRef = ref()
const dowloadChange = async () => {
const url = await qrcodeCanvasRef.value.toDataURL()
const a = document.createElement('a')
a.download = 'QRCode.png'
a.href = url
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
// eslint-disable-next-line no-undef
const amis = amisRequire('amis/embed')
const schema = shallowRef({}) //渲染页面的结构
// 加载页面结构
const fetchSchema = qid ? axios.get('/db/tPage/' + qid) : Promise.resolve(null)
const data = shallowRef({}) //渲染页面的数据
// 加载页面数据
const fetchData = aid ? axios.get('/db/tPageData/' + aid) : Promise.resolve(null)
Promise.all([fetchSchema, fetchData]).then((res) => {
if (res[0] && res[0][1].code === 0) {
schema.value = res[0][1].data?.content || {}
}
if (res[1] && res[1][1].code === 0) {
data.value = res[1][1].data?.content || {}
}
amis.embed('#root', schema.value, data.value, {
theme: 'antd',
tracker: (eventTrack) => {
if (eventTrack.eventType === 'pageLoaded') {
if (Object.keys(schema.value).length === 0) {
tips.value = '空页面'
} else {
loading.value = false
}
}
},
})
})
</script>
<style lang="scss" scoped>
.header {
border-bottom: 1px solid #e8e9eb;
padding: 5px 10px;
}
</style>