整合前端

This commit is contained in:
砂糖
2026-04-13 17:04:38 +08:00
parent 69609a2cb1
commit 5d4794c9bd
915 changed files with 144259 additions and 0 deletions

View File

@@ -0,0 +1,425 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab" style="margin-bottom: 15px;">
<el-tab-pane label="Web应用" name="web">
<div class="header-bar" style="display: flex; justify-content: flex-end; align-items: center; margin-bottom: 20px;">
<el-button
type="primary"
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>创建应用</el-button>
</div>
<div class="search-section" style="margin-bottom: 30px; display: flex; justify-content: center;">
<el-input
v-model="queryParams.applicationName"
placeholder="搜索应用"
clearable
prefix-icon="el-icon-search"
style="width: 50%; max-width: 600px;"
@keyup.enter.native="handleQuery"
/>
</div>
<div class="all-applications-grid">
<div v-for="item in applicationList" :key="item.applicationId" class="custom-application-card"
@click="goToApplicationDetail(item.applicationId)"
>
<div class="card-actions" @click.stop>
<el-dropdown trigger="hover" @command="handleCardCommand">
<span class="el-dropdown-link" @click.stop @mousedown.stop>
<i class="el-icon-more" style="font-size: 16px; color: #909399;"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{ action: 'edit', item: item }">编辑</el-dropdown-item>
<el-dropdown-item :command="{ action: 'delete', item: item }">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="open-type-icon">
<el-tooltip :content="getOpenTypeText(item.routeType)" placement="right" effect="light">
<i :class="getOpenTypeIcon(item.routeType)"></i>
</el-tooltip>
</div>
<svg-icon :icon-class="item.icon" class="card-icon" />
<span style="font-size: 14px; font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;">{{ item.applicationName }}</span>
</div>
</div>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</el-tab-pane>
<el-tab-pane label="小工具" name="tools">
<ToolsEntry />
</el-tab-pane>
</el-tabs>
<!-- Add/Modify Application Dialog (keep as is) -->
<el-dialog :title="title" :visible.sync="open" width="50%" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="应用图标" prop="icon">
<el-popover
placement="bottom-start"
width="460"
trigger="click"
@show="$refs['iconSelect'].reset()"
>
<IconSelect ref="iconSelect" @selected="selected" :active-icon="form.icon" />
<el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
<svg-icon
v-if="form.icon"
slot="prefix"
:icon-class="form.icon"
style="width: 25px;"
/>
<i v-else slot="prefix" class="el-icon-search el-input__icon" />
</el-input>
</el-popover>
</el-form-item>
<el-form-item label="IP 地址" prop="ip">
<el-input v-model="form.ip" placeholder="请输入服务 IP 地址" />
</el-form-item>
<el-form-item label="端口号" prop="port">
<el-input v-model="form.port" placeholder="请输入服务端口号" />
</el-form-item>
<el-form-item label="应用名称" prop="applicationName">
<el-input v-model="form.applicationName" placeholder="请输入应用名称" />
</el-form-item>
<el-form-item label="应用描述" prop="description">
<el-input v-model="form.description" type="textarea" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
<el-form-item label="路由类型" prop="routeType">
<el-radio-group v-model="form.routeType">
<el-radio label="0">当前页面打开</el-radio>
<el-radio label="1">新标签页打开</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { addApplication, delApplication, getApplication, listApplication, updateApplication } from "@/api/oa/application";
import IconSelect from "@/components/IconSelect";
import { handleApplicationRoute } from "@/utils/application";
import ToolsEntry from './tools/index.vue';
export default {
name: "Application",
components: { IconSelect, ToolsEntry },
data() {
return {
// 按钮loading
buttonLoading: false,
// 遮罩层
loading: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 应用集成表格数据
applicationList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 30,
applicationName: undefined,
sortField: undefined,
sortOrder: undefined,
},
// 表单参数
form: {
applicationId: undefined,
icon: undefined,
ip: undefined,
port: undefined,
applicationName: undefined,
description: undefined,
delFlag: undefined,
remark: undefined,
routeType: 0
},
// 表单校验
rules: {
applicationId: [
{ required: true, message: "主键ID不能为空", trigger: "blur" }
],
ip: [
{ required: true, message: "服务 IP 地址不能为空", trigger: "blur" }
],
port: [
{ required: true, message: "服务端口号不能为空", trigger: "blur" }
],
applicationName: [
{ required: true, message: "应用名称不能为空", trigger: "blur" }
],
description: [
{ required: true, message: "应用描述不能为空", trigger: "blur" }
],
remark: [
{ required: true, message: "备注不能为空", trigger: "blur" }
],
routeType: [
{ required: true, message: "路由类型不能为空", trigger: "change" }
]
},
activeTab: 'web', // 默认显示Web应用Tab
};
},
created() {
this.getList();
},
methods: {
// 选择图标
selected(name) {
this.form.icon = name;
},
// 处理卡片操作命令
handleCardCommand(command) {
if (command.action === 'edit') {
this.handleUpdate(command.item);
} else if (command.action === 'delete') {
this.handleDelete(command.item);
}
},
// 跳转到应用详情页面
goToApplicationDetail(id) {
const application = this.applicationList.find(item => item.applicationId === id);
if (application) {
handleApplicationRoute(application);
}
},
/** 查询应用集成列表 */
getList() {
this.loading = true;
const params = { ...this.queryParams };
if (params.sortField) {
const [field, order] = params.sortField.split(',');
params.sortField = field;
params.sortOrder = order;
}
listApplication(params).then(response => {
this.applicationList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
applicationId: undefined,
icon: undefined,
ip: undefined,
port: undefined,
applicationName: undefined,
description: undefined,
delFlag: undefined,
remark: undefined,
routeType: 0
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams.applicationName = undefined;
this.queryParams.sortField = undefined;
this.queryParams.sortOrder = undefined;
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加应用集成";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const applicationId = row.applicationId;
getApplication(applicationId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改应用集成";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.applicationId != null) {
updateApplication(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addApplication(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const applicationIds = row.applicationId;
this.$modal.confirm('是否确认删除应用名称为"' + row.applicationName + '"的数据项?').then(() => {
this.loading = true;
return delApplication(applicationIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
getOpenTypeIcon(type) {
const icons = {
'0': 'el-icon-monitor',
'1': 'el-icon-position'
};
return icons[type] || icons['0'];
},
getOpenTypeText(type) {
const texts = {
'0': '在页面中打开',
'1': '在新标签页打开'
};
return texts[type] || texts['0'];
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
}
.header-bar {
margin-bottom: 20px;
}
.search-section {
margin-bottom: 30px;
display: flex;
justify-content: center;
}
.section-header {
margin-bottom: 15px;
font-size: 18px;
font-weight: bold;
}
.all-applications-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
}
.custom-application-card {
position: relative;
width: 180px;
height: 100px;
padding: 15px;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-application-card:hover {
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1);
}
.card-actions {
position: absolute;
top: 10px;
right: 10px;
opacity: 0; /* Hidden by default */
transition: opacity 0.3s ease; /* Smooth transition for hover */
}
.custom-application-card:hover .card-actions {
opacity: 1; /* Show on hover */
}
.card-icon {
font-size: 40px;
margin-bottom: 8px;
}
.open-type-icon {
position: absolute;
top: 10px;
left: 10px;
padding: 4px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s;
z-index: 1;
opacity: 0;
transform: scale(0.8);
pointer-events: none;
i {
font-size: 16px;
color: #409EFF;
}
}
.custom-application-card:hover {
.open-type-icon {
opacity: 1;
transform: scale(1);
pointer-events: auto;
&:hover {
background: rgba(255, 255, 255, 1);
transform: scale(1.1);
}
}
}
</style>

View File

@@ -0,0 +1,166 @@
<template>
<div class="app-container">
<div class="app-wrapper">
<iframe
v-if="appUrl"
:src="appUrl"
:name="appName"
frameborder="0"
class="app-iframe"
@load="handleIframeLoad"
></iframe>
<div v-else class="loading-container">
<fad-loading></fad-loading>
</div>
<div v-if="loading" class="loading-overlay">
<fad-loading></fad-loading>
</div>
<div class="app-actions">
<el-tooltip content="刷新" placement="bottom" effect="light">
<el-button
type="text"
:icon="loading ? 'el-icon-loading' : 'el-icon-refresh'"
@click="refreshApp"
:loading="loading"
circle
></el-button>
</el-tooltip>
<el-tooltip content="在新标签页打开" placement="bottom" effect="light">
<el-button
type="text"
icon="el-icon-position"
@click="openInNewTab"
circle
></el-button>
</el-tooltip>
</div>
</div>
</div>
</template>
<script>
import { getApplication } from "@/api/oa/application";
import FadLoading from "@/components/fad-ui/fad-loading";
export default {
name: "ApplicationRender",
components: {
FadLoading
},
data() {
return {
appUrl: "",
appName: "",
applicationId: null,
loading: false
};
},
created() {
this.applicationId = this.$route.params.applicationId;
this.appName = `app-${this.applicationId}`;
this.loading = true;
this.getApplicationData();
},
methods: {
async getApplicationData() {
try {
const response = await getApplication(this.applicationId);
const { ip, port } = response.data;
let url;
if (ip.startsWith('http://') || ip.startsWith('https://')) {
url = ip;
} else {
url = `http://${ip}`;
if (port) {
url += `:${port}`;
}
}
this.appUrl = url;
} catch (error) {
console.error("获取应用信息出错:", error);
this.$message.error("获取应用信息出错");
this.loading = false;
}
},
handleIframeLoad() {
this.loading = false;
},
refreshApp() {
this.loading = true;
const iframe = document.querySelector('.app-iframe');
if (iframe) {
iframe.src = this.appUrl;
}
},
openInNewTab() {
if (this.appUrl) {
window.open(this.appUrl, '_blank');
}
}
}
};
</script>
<style lang="scss" scoped>
.app-container {
width: 100%;
height: calc(100vh - 84px);
position: relative;
overflow: hidden;
.app-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.app-iframe {
width: 100%;
height: 100%;
border: none;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.app-actions {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
display: flex;
gap: 8px;
background: rgba(255, 255, 255, 0.9);
padding: 4px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.el-button {
padding: 6px;
font-size: 16px;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
}
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<div class="tools-container">
<div class="tools-grid">
<div
v-for="tool in tools"
:key="tool.key"
class="tool-card"
@click="openTool(tool)"
>
<svg-icon v-if="tool.iconType === 'svg'" :icon-class="tool.icon" class="tool-icon" />
<i v-else-if="tool.iconType === 'el'" :class="tool.icon" class="tool-icon"></i>
<div class="tool-title">{{ tool.title }}</div>
</div>
</div>
<el-drawer
v-show="currentTool && currentTool.openType === 'drawer'"
:visible.sync="drawerVisible"
:title="currentTool ? currentTool.title : ''"
size="40%"
direction="rtl"
@close="closeDrawer"
@closed="afterDrawerClosed"
>
<component
v-if="currentTool && currentTool.component"
:is="currentTool.component"
/>
<div v-else style="text-align:center; color:#999; padding:40px 0;">敬请期待</div>
</el-drawer>
<el-dialog
v-if="currentTool && currentTool.openType === 'dialog'"
:visible.sync="dialogVisible"
:title="currentTool ? currentTool.title : ''"
width="40%"
@close="closeDialog"
>
<component
v-if="currentTool && currentTool.component"
:is="currentTool.component"
/>
<div v-else style="text-align:center; color:#999; padding:40px 0;">敬请期待</div>
</el-dialog>
</div>
</template>
<script>
import SvgIcon from '@/components/SvgIcon/index.vue';
import QrGenerator from './qr.vue';
export default {
name: 'ToolsEntry',
components: { QrGenerator, SvgIcon },
data() {
return {
tools: [
{
key: 'qr',
title: '二维码生成器',
icon: 'qr', // 只写图标名
iconType: 'svg', // 新增
component: 'QrGenerator',
openType: 'drawer' // 新增
},
// {
// key: 'demo',
// title: '演示工具',
// icon: 'el-icon-s-tools',
// iconType: 'el', // 新增
// component: null, // 先占位
// openType: 'dialog' // 新增
// },
// 可继续添加更多工具
],
drawerVisible: false,
dialogVisible: false, // 新增
currentTool: null
};
},
methods: {
openTool(tool) {
this.currentTool = tool;
if (tool.openType === 'drawer') {
this.drawerVisible = true;
} else if (tool.openType === 'dialog') {
this.dialogVisible = true;
}
},
closeDrawer() {
this.drawerVisible = false;
// 不要立即清空 currentTool
},
afterDrawerClosed() {
this.currentTool = null;
},
closeDialog() {
this.dialogVisible = false;
this.currentTool = null;
}
}
};
</script>
<style scoped>
.tools-container {
padding: 20px;
}
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px;
}
.tool-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 120px;
cursor: pointer;
transition: box-shadow 0.2s;
}
.tool-card:hover {
box-shadow: 0 4px 16px 0 rgba(0,0,0,0.10);
}
.tool-icon {
font-size: 36px;
color: #409EFF;
margin-bottom: 10px;
}
.tool-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="qr-generator">
<el-form :inline="false" @submit.native.prevent class="qr-form">
<el-form-item label="二维码内容" class="qr-form-item">
<el-input
v-model="text"
type="textarea"
:rows="4"
placeholder="请输入要生成二维码的内容,可多行"
style="width: 100%;"
/>
</el-form-item>
<el-form-item class="qr-form-item-btns">
<el-button type="primary" @click="generate">生成二维码</el-button>
<el-button type="success" :disabled="!qrDataUrl" @click="saveQr">保存二维码</el-button>
<el-button type="info" :disabled="!qrDataUrl" @click="printQr">打印二维码</el-button>
</el-form-item>
</el-form>
<transition name="fade">
<div v-if="qrDataUrl" class="qr-result">
<img :src="qrDataUrl" alt="二维码" ref="qrImg" />
<div style="margin-top: 10px; color: #666; font-size: 13px;">可右键保存或点击保存二维码按钮</div>
</div>
</transition>
</div>
</template>
<script>
import QRCode from 'qrcode';
export default {
name: 'QrGenerator',
data() {
return {
text: '',
qrDataUrl: ''
};
},
methods: {
async generate() {
if (!this.text) {
this.$message.warning('请输入内容');
return;
}
try {
this.qrDataUrl = await QRCode.toDataURL(this.text, { width: 240 });
} catch (e) {
this.$message.error('二维码生成失败');
}
},
saveQr() {
if (!this.qrDataUrl) return;
const a = document.createElement('a');
a.href = this.qrDataUrl;
a.download = 'qrcode.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
printQr() {
if (!this.qrDataUrl) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head>
<title>打印二维码</title>
<style>
body { text-align: center; margin: 0; padding: 40px 0; }
img { width: 240px; height: 240px; }
</style>
</head>
<body>
<img src="${this.qrDataUrl}" alt="二维码" />
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
}
}
};
</script>
<style scoped>
.qr-generator {
padding: 20px 0;
max-width: 420px;
margin: 0 auto;
}
.qr-form {
background: #fafbfc;
border-radius: 8px;
padding: 20px 20px 10px 20px;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04);
}
.qr-form-item {
width: 100%;
}
.qr-form-item-btns {
display: flex;
gap: 12px;
margin-top: 8px;
}
.qr-result {
margin-top: 32px;
text-align: center;
}
.qr-result img {
width: 240px;
height: 240px;
border-radius: 8px;
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.08);
background: #fff;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>