Merge remote-tracking branch 'origin/master'

master
chentaisen 2024-08-28 08:20:02 +08:00
commit 73f3431dba
8 changed files with 480 additions and 149 deletions

View File

@ -0,0 +1,53 @@
import request from '@/utils/request'
// 查询节点参数列表
export function listParam(query) {
return request({
url: '/quest/param/list',
method: 'get',
params: query
})
}
// 查询节点参数详细
export function getParam(id) {
return request({
url: '/quest/param/' + id,
method: 'get'
})
}
// 新增节点参数
export function addParam(data) {
return request({
url: '/quest/param',
method: 'post',
data: data
})
}
// 批量新增
export function batchAdd(data) {
return request({
url: '/quest/param/batchAdd',
method: 'post',
data: data
})
}
// 修改节点参数
export function updateParam(data) {
return request({
url: '/quest/param',
method: 'put',
data: data
})
}
// 删除节点参数
export function delParam(id) {
return request({
url: '/quest/param/' + id,
method: 'delete'
})
}

View File

@ -18,6 +18,14 @@ export function listDbTable(query) {
}) })
} }
// 查询db所有数据库列表
export function listDbTableAll() {
return request({
url: '/code/gen/db/listAll',
method: 'get'
})
}
// 查询表详细信息 // 查询表详细信息
export function getGenTable(tableId) { export function getGenTable(tableId) {
return request({ return request({
@ -80,9 +88,21 @@ export function synchDb(tableName,dbName) {
} }
// 查询所有数据库名称 // 查询所有数据库名称
export function selDbNameAll(tableName) { export function selDbNameAll() {
return request({ return request({
url: '/code/gen/selDbNameAll', url: '/code/gen/selDbNameAll',
method: 'get' method: 'get'
}) })
} }
// 根据数据库名称与表名称查询表字段
export function selectDbTableColumnsByName(dbName,table) {
return request({
url: '/code/gen/selectDbTableColumnsByName',
method: 'get',
params: {
dbName: dbName,
table: table
}
})
}

View File

@ -39,7 +39,7 @@
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<el-drawer title="通知公告" :visible.sync="drawer" direction="rtl" :size="'60%'"> <el-drawer title="通知公告" :visible.sync="drawer" direction="rtl" :size="'60%'" :modal="false">
<el-radio-group v-model="readingStatu" size="small"> <el-radio-group v-model="readingStatu" size="small">
<el-radio-button label="">全部</el-radio-button> <el-radio-button label="">全部</el-radio-button>
<el-radio-button label="1">已读</el-radio-button> <el-radio-button label="1">已读</el-radio-button>
@ -104,6 +104,7 @@
</el-descriptions> </el-descriptions>
</div> </div>
<el-dialog <el-dialog
:modal="false"
title="提示" title="提示"
:visible.sync="findNoticeFilg" :visible.sync="findNoticeFilg"
width="50%"> width="50%">

View File

@ -74,7 +74,7 @@ export default {
}; };
}, },
mounted() { mounted() {
this.getList(); this.getList(); //
this.jsPlumb = jsPlumb.getInstance(); this.jsPlumb = jsPlumb.getInstance();
this.fixNodesPosition() this.fixNodesPosition()
this.$nextTick(() => { this.$nextTick(() => {
@ -209,12 +209,12 @@ export default {
.auxiliary-line-x { .auxiliary-line-x {
position: absolute; position: absolute;
border: .5px dashed #2ab1e8; border: .5px dashed #2ab1e8;
z-index: 9999; z-index: 1008;
} }
.auxiliary-line-y { .auxiliary-line-y {
position: absolute; position: absolute;
border: .5px dashed #2ab1e8; border: .5px dashed #2ab1e8;
z-index: 9999; z-index: 1008;
} }
} }
} }
@ -223,7 +223,7 @@ export default {
<style lang="less"> <style lang="less">
.jtk-connector.active{ .jtk-connector.active{
z-index: 9999; z-index: 1008;
path { path {
stroke: #150042; stroke: #150042;
stroke-width: 1.5; stroke-width: 1.5;

View File

@ -6,7 +6,7 @@
@click="setActive" @click="setActive"
@mouseenter="showAnchor" @mouseenter="showAnchor"
@mouseleave="hideAnchor" @mouseleave="hideAnchor"
@dblclick.prevent="editNode" @dblclick.prevent="findDisposition()"
@contextmenu.prevent="onContextmenu"> @contextmenu.prevent="onContextmenu">
<div class="log-wrap"> <div class="log-wrap">
<img :src="node.logImg" alt=""> <img :src="node.logImg" alt="">
@ -17,21 +17,83 @@
<div class="node-anchor anchor-right" v-show="mouseEnter"></div> <div class="node-anchor anchor-right" v-show="mouseEnter"></div>
<div class="node-anchor anchor-bottom" v-show="mouseEnter"></div> <div class="node-anchor anchor-bottom" v-show="mouseEnter"></div>
<div class="node-anchor anchor-left" v-show="mouseEnter"></div> <div class="node-anchor anchor-left" v-show="mouseEnter"></div>
<!-- <el-dialog :visible.sync="updNameFlag" width="30%">--> <!-- 节点配置-->
<!-- <el-form v-model="node">--> <div align="center">
<!-- <el-form-item label="节点名称">--> <el-dialog :modal="false" title="配置" :visible.sync="disposition.findFlag" width="50%" >
<!-- <el-input v-model="newNodeName" placeholder="请输入内容" />--> <span>数据库:&nbsp;&nbsp;{{ disposition.formData.dbName }}</span>
<!-- </el-form-item>--> <span>数据表:&nbsp;&nbsp;{{ disposition.formData.table }}</span>
<!-- <el-form-item>--> <span>
<!-- <el-button type="primary" @click="doUpdName"> </el-button>--> <el-table ref="table" :data="disposition.formData.fields" height="300px">
<!-- </el-form-item>--> <el-table-column :show-overflow-tooltip="true" label="字段名称" prop="columnName" />
<!-- </el-form>--> <el-table-column :show-overflow-tooltip="true" label="字段描述" prop="columnComment" />
<!-- </el-dialog>--> <el-table-column :show-overflow-tooltip="true" label="字段类型" prop="columnType" />
</el-table>
</span>
<el-button @click="disposition.findFlag = false">确认</el-button>
<el-button type="primary" @click="editDisposition"></el-button>
</el-dialog>
<!-- 自定义表单对话框 -->
<el-drawer
style="padding-left: 20px; padding-right: 20px;"
:modal="false"
:visible.sync="disposition.updFlag"
direction="rtl"
title="节点配置"
append-to-body
size="60%">
<el-form ref="queryForm" :inline="true" :model="queryParams" size="small">
<el-form-item label="数据库名称" prop="dbName">
<el-select v-model="queryParams.dbName" placeholder="请选择数据库" clearable>
<el-option v-for="item in dbNameOptions" :key="item" :label="item" :value="item"/>
</el-select>
</el-form-item>
<el-form-item label="表名称" prop="tableName">
<el-input
v-model="queryParams.tableName"
clearable
placeholder="请输入表名称" />
</el-form-item>
<el-form-item label="表描述" prop="tableComment">
<el-input
v-model="queryParams.tableComment"
clearable
placeholder="请输入表描述" />
</el-form-item>
<el-form-item>
<el-button icon="el-icon-search" size="mini" type="primary" @click="handleQuery"></el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"></el-button>
</el-form-item>
</el-form>
<el-table ref="table" :data="tableOptions" height="300px">
<el-table-column :show-overflow-tooltip="true" label="表名称" prop="tableName" />
<el-table-column :show-overflow-tooltip="true" label="表描述" prop="tableComment" />
<el-table-column label="操作">
<template slot-scope="scope">
<el-button type="text" @click="selectTable(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-divider />
<el-table ref="table" v-show="fieldsOptions" :data="fieldsOptions" height="300px"
@row-click="clickRow" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column :show-overflow-tooltip="true" label="字段名称" prop="columnName" />
<el-table-column :show-overflow-tooltip="true" label="字段描述" prop="columnComment" />
<el-table-column :show-overflow-tooltip="true" label="字段类型" prop="columnType" />
</el-table>
<el-divider />
<div align="center">
<el-button type="primary" @click="saveDisposition"> </el-button>
<el-button @click="disposition.updFlag = false"> </el-button>
</div>
</el-drawer>
</div>
</div> </div>
</template> </template>
<script> <script>
import ClickOutside from 'vue-click-outside' import ClickOutside from 'vue-click-outside'
import {listDbTableAll, selDbNameAll, selectDbTableColumnsByName} from "../../../../api/tool/gen";
export default { export default {
name: "nodeItem", name: "nodeItem",
props: { props: {
@ -53,11 +115,35 @@ export default {
}, },
data() { data() {
return { return {
// updNameFlag: false,
// newNodeName: '',
mouseEnter: false, mouseEnter: false,
isActive: false, isActive: false,
isSelected: false isSelected: false,
disposition: {
findFlag: false,
updFlag: false,
formData: {
nodeId: undefined,
dbName: undefined,
table: {},
fields: []
}
},
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
dbName: undefined,
tableName: undefined,
tableComment: undefined
},
//
dbNameOptions: [],
//
tableOptions: [],
//
tableList: [],
//
fieldsOptions: []
}; };
}, },
methods: { methods: {
@ -69,18 +155,20 @@ export default {
}, },
onContextmenu() { onContextmenu() {
this.$contextmenu({ this.$contextmenu({
items: [{ items: [
{
label: '修改名称', label: '修改名称',
disabled: false, disabled: false,
icon: "", icon: "",
onClick: () => { onClick: () => {
// this.toUpdName(); this.toUpdName();
this.newNodeName = prompt("请输入新名称:");
if (this.newNodeName !== null) {
this.$emit('setNodeName', this.node.id, this.newNodeName)
} else {
alert("操作取消!");
} }
},{
label: '修改配置',
disabled: false,
icon: "",
onClick: () => {
this.disposition.updFlag = true;
} }
},{ },{
label: '删除节点', label: '删除节点',
@ -89,7 +177,8 @@ export default {
onClick: () => { onClick: () => {
this.deleteNode() this.deleteNode()
} }
}], }
],
event, event,
customClass: 'custom-class', customClass: 'custom-class',
zIndex: 9999, zIndex: 9999,
@ -117,24 +206,43 @@ export default {
this.$emit("changeLineState", this.node.id, false) this.$emit("changeLineState", this.node.id, false)
this.isActive = false this.isActive = false
}, },
editNode() { //
this.newNodeName = prompt("请输入新名称:"); toUpdName() {
if (this.newNodeName !== null) { this.$prompt('请输入新名称', '更改名称', {
this.$emit('setNodeName', this.node.id, this.newNodeName) confirmButtonText: '确定',
} else { cancelButtonText: '取消'
alert("操作取消!"); }).then(({ value }) => {
} this.$emit('setNodeName', this.node.id, value)
this.$message({
type: 'success',
message: '修改成功'
});
}).catch(() => {
this.$message({
type: 'info',
message: '操作取消'
});
});
}, },
//
deleteNode() { deleteNode() {
this.$confirm('确认删除该节点吗?', '删除提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$emit("deleteNode", this.node) this.$emit("deleteNode", this.node)
this.$message({
type: 'success',
message: '删除成功'
});
}).catch(() => {
this.$message({
type: 'info',
message: '操作取消'
});
});
}, },
// toUpdName() {
// this.updNameFlag = true
// },
// doUpdName() {
// this.$emit('setNodeName', this.node.id, this.newNodeName)
// this.updNameFlag = false
// },
/** jsPlumb节点类型 --> 数据库节点类型 */ /** jsPlumb节点类型 --> 数据库节点类型 */
toMysqlNode(node,preLine,nextLine){ toMysqlNode(node,preLine,nextLine){
return { return {
@ -177,7 +285,116 @@ export default {
node: node, node: node,
line: line line: line
} }
},
// -
findDisposition() {
//
this.disposition.formData.nodeId = this.node.id
this.disposition.findFlag = true
},
//
editDisposition() {
this.selDbNameList()
this.getListAll()
this.disposition.updFlag = true
},
//
selDbNameList() {
selDbNameAll().then(res => {
this.dbNameOptions = res.data
})
},
//
getFields(dbName, tableName) {
selectDbTableColumnsByName(dbName, tableName).then(res => {
this.fieldsOptions = res.data
})
},
//
saveDisposition() {
console.log(this.disposition.formData)
const dispositionData = this.disposition.formData
const addData = []
/**
* {
* node_info_code: '',
* node_type: '',
* node_value: ''
* }
*/
//
addData.push({
nodeInfoCode: dispositionData.nodeId,
nodeType: 'dbName',
nodeValue: dispositionData.dbName
})
//
addData.push({
nodeInfoCode: dispositionData.nodeId,
nodeType: 'table',
nodeValue: dispositionData.table
})
//
addData.push({
nodeInfoCode: dispositionData.nodeId,
nodeType: 'fields',
nodeValue: dispositionData.fields.toString()
})
console.log(addData)
this.disposition.updFlag = false
this.disposition.findFlag = false
},
//
resetDisposition() {
this.disposition.formData = {
dbName: undefined,
table: {},
fields: []
} }
},
getListAll() {
listDbTableAll().then(res => {
this.tableList = res.data;
this.tableOptions = res.data;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.tableOptions = []
this.tableList.forEach(table => {
const params = this.queryParams
if ((table.dbName === params.dbName || !params.dbName) &&
(table.tableName.includes(params.tableName) || !params.tableName) &&
(table.tableComment.includes(params.tableComment) || !params.tableComment)){
this.tableOptions.push(table)
}
})
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.resetDisposition()
this.handleQuery();
},
/** 选择表信息*/
selectTable(row) {
this.disposition.formData.table = row.tableName
this.disposition.formData.dbName = row.dbName
this.getFields(row.dbName, row.tableName)
},
clickRow(row) {
this.$refs.table.toggleRowSelection(row);
},
//
handleSelectionChange(selection) {
this.disposition.formData.fields = selection.map(item => {
return {
fieldName: item.columnName,
fieldType: item.columnType,
fieldComment: item.columnComment
};
});
},
} }
}; };
</script> </script>
@ -186,6 +403,10 @@ export default {
@labelColor: #409eff; @labelColor: #409eff;
@nodeSize: 20px; @nodeSize: 20px;
@viewSize: 10px; @viewSize: 10px;
.v-modal {
width: 50%;
display: none !important;
}
.node-item { .node-item {
position: absolute; position: absolute;
display: flex; display: flex;
@ -197,9 +418,9 @@ export default {
border-radius: 4px; border-radius: 4px;
cursor: move; cursor: move;
box-sizing: content-box; box-sizing: content-box;
z-index: 9995; z-index: 1006;
&:hover { &:hover {
z-index: 9998; z-index: 1007;
.delete-btn{ .delete-btn{
display: block; display: block;
} }
@ -225,7 +446,7 @@ export default {
justify-content: center; justify-content: center;
border-radius: 10px; border-radius: 10px;
cursor: crosshair; cursor: crosshair;
z-index: 9999; z-index: 1008;
background: -webkit-radial-gradient(sandybrown 10%, white 30%, #9a54ff 60%); background: -webkit-radial-gradient(sandybrown 10%, white 30%, #9a54ff 60%);
} }
.anchor-top{ .anchor-top{

View File

@ -1,4 +1,5 @@
const nodeTypeList = [{ const nodeTypeList = [
{
type: 'start', type: 'start',
typeName: '开始', typeName: '开始',
nodeName: '开始', nodeName: '开始',
@ -34,7 +35,29 @@ const nodeTypeList = [{
nodeName: '清洗', nodeName: '清洗',
logImg: require('@/assets/svg/15清洗.svg'), logImg: require('@/assets/svg/15清洗.svg'),
log_bg_color: 'rgba(250, 205, 81, 0.2)' log_bg_color: 'rgba(250, 205, 81, 0.2)'
}] },
{
type: 'table',
typeName: '表结构',
nodeName: '表结构',
logImg: require('@/assets/svg/5文件数据.svg'),
log_bg_color: 'rgba(0, 128, 0, 0.2)'
},
{
type: 'unite',
typeName: '联合查询',
nodeName: '联合查询',
logImg: require('@/assets/svg/侧边栏测试任务.svg'),
log_bg_color: 'rgba(0, 128, 0, 0.2)'
},
{
type: 'exportation',
typeName: '数据输出',
nodeName: '数据输出',
logImg: require('@/assets/svg/19导出.svg'),
log_bg_color: 'rgba(0, 128, 0, 0.2)'
}
]
console.log(nodeTypeList) console.log(nodeTypeList)

View File

@ -146,19 +146,22 @@ const methods = {
}, },
// 确认删除连线 // 确认删除连线
confirmDelLine(line) { confirmDelLine(line) {
if (confirm("确认删除该连线?")) { this.$confirm('确认删除该连线吗?', '删除提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.jsPlumb.deleteConnection(line) this.jsPlumb.deleteConnection(line)
} else { this.$message({
// 用户点击了取消 type: 'success',
alert("操作已取消!"); message: '删除成功'
} });
// this.$Modal.confirm({ }).catch(() => {
// title: '删除连线', this.$message({
// content: "<p>确认删除该连线?</p>", type: 'info',
// onOk: () => { message: '操作取消'
// this.jsPlumb.deleteConnection(line) });
// } });
// })
}, },
// 删除连线 // 删除连线
deleLine(line) { deleLine(line) {

View File

@ -9,21 +9,15 @@
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="任务触发器" prop="taskWebhook">
<el-input
v-model="queryParams.taskWebhook"
placeholder="请输入任务触发器"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="启用状态" prop="state"> <el-form-item label="启用状态" prop="state">
<el-input <el-select v-model="queryParams.state" placeholder="请选择启用状态" clearable>
v-model="queryParams.state" <el-option
placeholder="请输入启用状态" v-for="dict in dict.type.sys_enable_status"
clearable :key="dict.value"
@keyup.enter.native="handleQuery" :label="dict.label"
:value="dict.value"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery"></el-button>
@ -79,36 +73,40 @@
<el-table v-loading="loading" :data="questList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="questList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="编号" align="center" prop="id" />
<el-table-column label="任务编码" align="center" prop="taskCode" /> <el-table-column label="任务编码" align="center" prop="taskCode" />
<el-table-column label="任务名称" align="center" prop="taskName" /> <el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="任务触发器" align="center" prop="taskWebhook" /> <el-table-column label="任务触发器" align="center" prop="taskWebhook" />
<el-table-column label="规则编码" align="center" prop="ruleCode" /> <el-table-column label="规则编码" align="center" prop="ruleCode" />
<el-table-column label="任务类型" align="center" prop="taskType" /> <el-table-column label="任务类型" align="center" prop="taskType" />
<el-table-column label="启用状态" align="center" prop="state" /> <el-table-column label="任务内容" align="center" prop="taskData" />
<el-table-column label="启用状态" align="center" prop="state">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_enable_status" :value="scope.row.state"/>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
icon="el-icon-edit" icon="el-icon-setting"
@click="toUpdateTaskNode(scope.row.taskCode)" @click="toUpdateTaskNode(scope.row.taskCode)"
v-hasPermi="['quest:quest:edit']" v-hasPermi="['quest:quest:edit']"
>管理任务</el-button> >管理</el-button>
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
icon="el-icon-edit" icon="el-icon-edit"
@click="handleUpdate(scope.row)" @click="handleUpdate(scope.row)"
v-hasPermi="['quest:quest:edit']" v-hasPermi="['quest:quest:edit']"
>修改任务</el-button> >修改</el-button>
<el-button <el-button
size="mini" size="mini"
type="text" type="text"
icon="el-icon-delete" icon="el-icon-delete"
@click="handleDelete(scope.row)" @click="handleDelete(scope.row)"
v-hasPermi="['quest:quest:remove']" v-hasPermi="['quest:quest:remove']"
>删除任务</el-button> >删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -133,11 +131,12 @@
<el-form-item label="规则编码" prop="ruleCode"> <el-form-item label="规则编码" prop="ruleCode">
<el-input v-model="form.ruleCode" placeholder="请输入规则编码" /> <el-input v-model="form.ruleCode" placeholder="请输入规则编码" />
</el-form-item> </el-form-item>
<el-form-item label="任务类型" prop="task_type"> <el-form-item label="任务内容">
<el-input v-model="form.taskType" placeholder="请输入任务类型" /> <editor v-model="form.taskData" :min-height="192"/>
</el-form-item> </el-form-item>
<el-form-item label="启用状态" prop="state"> <el-form-item label="启用状态" prop="state">
<el-input v-model="form.state" placeholder="请输入启用状态" /> <el-radio v-model="form.state" label="Y"></el-radio>
<el-radio v-model="form.state" label="N"></el-radio>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
@ -149,10 +148,12 @@
</template> </template>
<script> <script>
import {listQuest, getQuest, delQuest, addQuest, updateQuest} from "@/api/quest/quest"; import { listQuest, getQuest, delQuest, addQuest, updateQuest } from "/src/api/quest/quest";
import dict from "../../../utils/dict";
export default { export default {
name: "Quest", name: "Quest",
dicts: ['sys_enable_status'],
data() { data() {
return { return {
// //
@ -196,11 +197,11 @@ export default {
this.getList(); this.getList();
}, },
methods: { methods: {
dict,
/** 查询任务列表 */ /** 查询任务列表 */
getList() { getList() {
this.loading = true; this.loading = true;
listQuest(this.queryParams).then(response => { listQuest(this.queryParams).then(response => {
console.log(response)
this.questList = response.data.rows; this.questList = response.data.rows;
this.total = response.data.total; this.total = response.data.total;
this.loading = false; this.loading = false;
@ -220,6 +221,7 @@ export default {
taskWebhook: null, taskWebhook: null,
ruleCode: null, ruleCode: null,
taskType: null, taskType: null,
taskData: null,
state: null, state: null,
createBy: null, createBy: null,
createTime: null, createTime: null,
@ -289,7 +291,8 @@ export default {
}).then(() => { }).then(() => {
this.getList(); this.getList();
this.$modal.msgSuccess("删除成功"); this.$modal.msgSuccess("删除成功");
}).catch(() => {}); }).catch(() => {
});
}, },
/** 导出按钮操作 */ /** 导出按钮操作 */
handleExport() { handleExport() {
@ -300,6 +303,13 @@ export default {
// //
toUpdateTaskNode(taskCode){ toUpdateTaskNode(taskCode){
this.$router.push({path: '/quest/node', query: {taskCode: taskCode}}) this.$router.push({path: '/quest/node', query: {taskCode: taskCode}})
},
updState(row){
updateQuest(row).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} }
} }
}; };