Files
ServiceMethodView/ServiceMethodView.html
T
2026-08-27 11:45:35 +08:00

809 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务方法管理 - DLL 扫描浏览</title>
<!-- Vue 2 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
<!-- Element UI CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.13/lib/theme-chalk/index.css">
<!-- Element UI JS -->
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.13/lib/index.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js"></script>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
background: #f0f2f5;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
color: #303133;
}
.page-container {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
}
.top-bar {
background: #fff;
border-radius: 10px;
padding: 20px 24px;
box-shadow: 0 2px 12px rgba(0,0,0,.06);
margin-bottom: 16px;
}
.page-title {
font-size: 20px;
font-weight: 600;
margin: 0 0 16px 0;
color: #303133;
}
.scan-form {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.scan-form .el-input {
flex: 1;
min-width: 280px;
}
.scan-form .el-dropdown {
flex-shrink: 0;
}
.el-dropdown-menu__item {
max-width: 500px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.stat-card {
background: #fff;
border-radius: 10px;
padding: 16px 20px;
box-shadow: 0 2px 12px rgba(0,0,0,.04);
display: flex;
flex-direction: column;
}
.stat-label {
color: #909399;
font-size: 13px;
margin-bottom: 6px;
}
.stat-value {
color: #409eff;
font-size: 24px;
font-weight: 700;
word-break: break-all;
}
.filter-bar {
margin-bottom: 16px;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.main-layout {
width: 100%;
}
.left-panel {
width: 100%;
}
.module-title {
font-size: 14px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.module-title i {
color: #e6a23c;
font-size: 18px;
}
.type-badge {
cursor: pointer;
color: #409eff;
font-family: Consolas, monospace;
text-decoration: underline;
text-decoration-style: dashed;
font-size: 13px;
}
.type-badge:hover {
color: #66b1ff;
background: #ecf5ff;
border-radius: 3px;
}
.type-badge.system {
color: #606266;
cursor: default;
text-decoration: none;
}
.type-badge.system:hover {
background: transparent;
}
.type-badge.active {
background: #409eff;
color: #fff;
border-radius: 3px;
padding: 0 4px;
text-decoration: none;
}
.param-desc {
color: #909399;
font-size: 12px;
}
.empty-tip {
text-align: center;
padding: 60px 0;
color: #909399;
}
.type-dialog .el-dialog__header {
background: #409eff;
padding: 16px 20px;
}
.type-dialog .el-dialog__title {
color: #fff;
font-size: 16px;
font-weight: 600;
word-break: break-all;
line-height: 1.5;
}
.type-dialog .el-dialog__headerbtn .el-dialog__close {
color: #fff;
}
.type-dialog .el-dialog__body {
padding: 20px;
max-height: 70vh;
overflow-y: auto;
}
.dialog-namespace {
color: #909399;
font-size: 13px;
margin-bottom: 12px;
}
.dialog-type-desc {
color: #606266;
font-size: 14px;
line-height: 1.8;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.dialog-section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.dialog-prop-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.dialog-prop-item {
padding: 14px;
background: #f5f7fa;
border-radius: 6px;
}
.dialog-prop-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 6px;
flex-wrap: wrap;
}
.dialog-prop-name {
font-weight: 600;
color: #303133;
font-family: Consolas, monospace;
font-size: 14px;
word-break: break-all;
line-height: 1.5;
}
.dialog-prop-type {
color: #409eff;
font-family: Consolas, monospace;
font-size: 14px;
cursor: pointer;
word-break: break-all;
line-height: 1.5;
text-align: right;
}
.dialog-prop-type:hover {
color: #66b1ff;
}
.dialog-prop-type.system {
color: #909399;
cursor: default;
}
.dialog-prop-desc {
color: #909399;
font-size: 13px;
line-height: 1.6;
}
.dialog-empty {
text-align: center;
padding: 40px 0;
color: #909399;
}
@media (max-width: 768px) {
.type-dialog {
width: 92% !important;
}
}
.module-empty {
text-align: center;
padding: 30px 0;
color: #909399;
}
.type-table-card {
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,.04);
}
.modules-panel {
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,.04);
}
.modules-panel .el-card__header {
padding: 16px 20px;
background: #fafbfc;
}
.module-collapse {
border: none;
}
.module-collapse .el-collapse-item__header {
font-size: 14px;
font-weight: 600;
color: #303133;
padding-left: 4px;
padding-right: 4px;
}
.module-collapse .el-collapse-item__content {
padding: 0 0 16px 0;
}
.module-collapse-title {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.module-collapse-title span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.method-table {
width: 100%;
}
.method-table .el-table__expanded-cell {
padding: 16px 20px;
background: #fafbfc;
}
.expand-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.expand-title {
font-weight: 600;
color: #303133;
font-size: 13px;
}
.expand-params {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px 16px;
align-items: baseline;
}
.expand-params .param-name {
color: #303133;
font-family: Consolas, monospace;
font-weight: 500;
white-space: nowrap;
}
.expand-params .param-type-desc {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.expand-params .param-desc {
color: #909399;
font-size: 12px;
}
.left-panel {
min-width: 0;
}
.type-badge {
word-break: break-all;
line-height: 1.5;
}
</style>
</head>
<body>
<div id="app" v-cloak>
<div class="page-container">
<div class="top-bar">
<h1 class="page-title">服务方法管理 — DLL 扫描浏览</h1>
<div class="scan-form">
<el-input
v-model="filePath"
placeholder="请输入 DLL 文件绝对路径,例如 D:\\GTHINKING\\Bin\\S_XLXSBJA.dll"
clearable
@keyup.enter.native="scanDll">
</el-input>
<el-dropdown v-if="recentPaths.length" size="small" split-button type="info" @command="selectRecentPath" @click="filePath = recentPaths[0]">
<i class="el-icon-time"></i> 最近路径
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(p, idx) in recentPaths" :key="idx" :command="p" :title="p">{{ p }}</el-dropdown-item>
<el-dropdown-item divided command="__clear__">清空记录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button type="primary" icon="el-icon-search" :loading="scanning" @click="scanDll">
扫描 DLL
</el-button>
<el-button icon="el-icon-refresh" :loading="loading" @click="loadData" :disabled="!macId">
刷新数据
</el-button>
</div>
</div>
<div v-if="macId" class="stats-bar">
<div class="stat-card">
<span class="stat-label">服务部件</span>
<span class="stat-value" style="font-size: 16px;">{{ macId }}</span>
</div>
<div class="stat-card">
<span class="stat-label">模块数</span>
<span class="stat-value">{{ srcmd.length }}</span>
</div>
<div class="stat-card">
<span class="stat-label">方法数</span>
<span class="stat-value">{{ srcmethod.length }}</span>
</div>
<div class="stat-card">
<span class="stat-label">参数数</span>
<span class="stat-value">{{ srcmethodparams.length }}</span>
</div>
<div class="stat-card">
<span class="stat-label">自定义类型</span>
<span class="stat-value">{{ srctype.length }}</span>
</div>
</div>
<div v-if="macId" class="filter-bar">
<el-input
v-model="filterText"
placeholder="搜索方法名 / 参数名 / 类型名"
prefix-icon="el-icon-search"
clearable
style="width: 260px;">
</el-input>
<el-select
v-if="viewMode === 'module'"
v-model="jumpModule"
placeholder="快速跳转模块"
size="small"
clearable
style="width: 220px;"
@change="jumpToModule">
<el-option
v-for="md in srcmd"
:key="md.MDID"
:label="md.MDNAME"
:value="md.MDID">
</el-option>
</el-select>
<template v-if="viewMode === 'module'">
<el-button size="small" icon="el-icon-arrow-down" @click="expandAllModules">全部展开</el-button>
<el-button size="small" icon="el-icon-arrow-up" @click="collapseAllModules">全部收起</el-button>
</template>
<el-button size="small" icon="el-icon-close" @click="clearFilter">清空筛选</el-button>
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="module">按模块浏览</el-radio-button>
<el-radio-button label="type">按类型浏览</el-radio-button>
</el-radio-group>
<el-tag v-if="viewMode === 'module' && filterText" type="info" size="small">{{ filteredMethodCount }} 个方法</el-tag>
</div>
<div v-if="macId" class="main-layout">
<!-- 左侧:模块/方法列表 -->
<div class="left-panel">
<!-- 按模块视图 -->
<template v-if="viewMode === 'module'">
<el-card class="modules-panel" shadow="never">
<div slot="header" class="module-title">
<i class="el-icon-s-operation" style="color: #409eff;"></i>
<span>模块与方法</span>
</div>
<div v-if="filteredModules.length === 0" class="module-empty">没有匹配的数据</div>
<el-collapse v-else v-model="expandedModules" class="module-collapse">
<el-collapse-item v-for="md in filteredModules" :key="md.MDID" :name="md.MDID" :data-module-id="md.MDID">
<template slot="title">
<div class="module-collapse-title">
<span :title="md.MDNAME">{{ md.MDNAME }}</span>
<el-tag size="mini" type="info">{{ md.methods.length }} 个方法</el-tag>
</div>
</template>
<div v-if="md.methods.length === 0" class="module-empty">该模块下没有 AjaxMethod 方法</div>
<el-table
v-else
:data="md.methods"
class="method-table"
size="small"
stripe
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 600 }">
<el-table-column type="expand" width="40">
<template slot-scope="props">
<div class="expand-header">
<span class="expand-title">参数明细</span>
<el-button type="text" size="mini" icon="el-icon-document-copy" @click="copyMethodSignature(props.row)">复制签名</el-button>
</div>
<div class="expand-params">
<template v-for="p in props.row.params">
<span class="param-name">{{ p.PARAMNAME }}</span>
<div class="param-type-desc">
<span
:class="['type-badge', { system: isSystemType(p.PARAMTYPE), active: isActiveType(p.PARAMTYPE) }]"
@click.stop="showType(p.PARAMTYPE)">
{{ p.PARAMTYPE }}
</span>
<span v-if="p.PARAMDESC" class="param-desc">{{ p.PARAMDESC }}</span>
</div>
</template>
<template v-if="props.row.params.length === 0">
<span class="param-name"></span>
<span class="param-desc">无参数</span>
</template>
</div>
</template>
</el-table-column>
<el-table-column prop="METHODNAME" label="方法名" min-width="140"></el-table-column>
<el-table-column prop="MODIFIERS" label="修饰符" min-width="100"></el-table-column>
<el-table-column prop="AUTHREQUIRED" label="需登录" width="70" align="center"></el-table-column>
<el-table-column prop="USEHTTPGET" label="支持GET" width="80" align="center"></el-table-column>
<el-table-column label="返回类型" min-width="220">
<template slot-scope="scope">
<span
:class="['type-badge', { system: isSystemType(scope.row.RETTYPE), active: isActiveType(scope.row.RETTYPE) }]"
@click.stop="showType(scope.row.RETTYPE)">
{{ scope.row.RETTYPE || 'void' }}
</span>
</template>
</el-table-column>
<el-table-column prop="RETDESC" label="返回值描述" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column prop="METHODDESC" label="方法描述" min-width="200" show-overflow-tooltip></el-table-column>
</el-table>
</el-collapse-item>
</el-collapse>
</el-card>
</template>
<!-- 按类型视图 -->
<template v-if="viewMode === 'type'">
<el-card class="type-table-card" shadow="never">
<div slot="header" class="module-title">
<i class="el-icon-coin" style="color: #409eff;"></i>
<span>自定义类型一览</span>
</div>
<el-table :data="filteredTypes" style="width: 100%" size="small" stripe>
<el-table-column label="类型全称" min-width="260">
<template slot-scope="scope">
<el-link type="primary" @click="showType(scope.row.TYPEFULLNAME)">{{ scope.row.TYPEFULLNAME }}</el-link>
</template>
</el-table-column>
<el-table-column prop="NAMESPACE" label="命名空间" min-width="180"></el-table-column>
<el-table-column prop="TYPEDESC" label="描述" show-overflow-tooltip></el-table-column>
</el-table>
</el-card>
</template>
</div>
</div>
<!-- 类型定义弹窗 -->
<el-dialog
:visible.sync="dialogVisible"
:title="currentTypeFullName || '类型定义'"
width="70%"
:close-on-click-modal="true"
custom-class="type-dialog">
<div v-if="currentTypeNamespace" class="dialog-namespace">{{ currentTypeNamespace }}</div>
<div v-if="currentTypeDesc" class="dialog-type-desc">{{ currentTypeDesc }}</div>
<div class="dialog-section-title">公共属性 / 字段({{ currentTypeProps.length }})</div>
<div v-if="currentTypeProps.length === 0" class="dialog-empty">该类型没有公共属性/字段</div>
<div v-else class="dialog-prop-list">
<div v-for="prop in currentTypeProps" :key="prop.PROPNAME + '_' + prop.PROPTYPEFULLNAME" class="dialog-prop-item">
<div class="dialog-prop-header">
<span class="dialog-prop-name">{{ prop.PROPNAME }}</span>
<span
:class="['dialog-prop-type', { system: isSystemType(prop.PROPTYPEFULLNAME) }]"
@click="showType(prop.PROPTYPEFULLNAME)">
{{ prop.PROPTYPE || prop.PROPTYPEFULLNAME }}
</span>
</div>
<div v-if="prop.PROPDESC" class="dialog-prop-desc">{{ prop.PROPDESC }}</div>
</div>
</div>
</el-dialog>
<div v-if="!macId && !scanning && !loading" class="empty-tip">
<i class="el-icon-upload" style="font-size: 56px; color: #dcdfe6; display: block; margin-bottom: 16px;"></i>
<div>输入 DLL 文件路径并点击“扫描 DLL”开始解析服务方法</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
// 服务调用基地址,按项目实际部署调整
baseUrl: 'http://127.0.0.1/GTHINKING/AjaxService/S_XLXSBJ/102373019.ashx',
filePath: '',
scanning: false,
loading: false,
macId: '',
srcmd: [],
srcmethod: [],
srcmethodparams: [],
srctype: [],
srcprop: [],
filterText: '',
viewMode: 'module',
expandedModules: [],
currentTypeFullName: '',
dialogVisible: false,
recentPaths: [],
jumpModule: '',
},
computed: {
currentTypeNamespace() {
const t = this.srctype.find(x => x.TYPEFULLNAME === this.currentTypeFullName);
return t && t.NAMESPACE ? t.NAMESPACE : '';
},
currentTypeDesc() {
const t = this.srctype.find(x => x.TYPEFULLNAME === this.currentTypeFullName);
return t && t.TYPEDESC ? t.TYPEDESC : '';
},
currentTypeProps() {
return this.srcprop.filter(x => x.TYPEFULLNAME === this.currentTypeFullName);
},
filteredModules() {
const text = (this.filterText || '').toLowerCase();
return this.srcmd.map(md => {
const methods = this.srcmethod
.filter(m => m.MACID === md.MACID && m.MDID === md.MDID)
.map(m => {
const params = this.srcmethodparams.filter(p =>
p.MACID === m.MACID && p.MDID === m.MDID && p.METHODNAME === m.METHODNAME
);
return { ...m, params };
});
return { ...md, methods };
}).filter(md => {
if (!text) return true;
return md.methods.some(m =>
(m.METHODNAME || '').toLowerCase().includes(text) ||
m.params.some(p =>
(p.PARAMNAME || '').toLowerCase().includes(text) ||
(p.PARAMTYPE || '').toLowerCase().includes(text)
) ||
(m.RETTYPE || '').toLowerCase().includes(text)
);
});
},
filteredTypes() {
const text = (this.filterText || '').toLowerCase();
if (!text) return this.srctype;
return this.srctype.filter(t =>
(t.TYPEFULLNAME || '').toLowerCase().includes(text) ||
(t.TYPENAME || '').toLowerCase().includes(text) ||
(t.NAMESPACE || '').toLowerCase().includes(text)
);
},
filteredMethodCount() {
return this.filteredModules.reduce((sum, md) => sum + md.methods.length, 0);
}
},
methods: {
isSystemType(typeName) {
if (!typeName) return true;
const lower = typeName.toLowerCase();
if (lower.startsWith('system.')) return true;
if (lower.startsWith('microsoft.')) return true;
if (['string', 'int', 'bool', 'long', 'double', 'float', 'decimal', 'datetime', 'object', 'void', 'byte', 'char', 'short', 'uint', 'ulong', 'ushort'].includes(lower)) return true;
if (lower.endsWith('[]')) return this.isSystemType(lower.replace('[]', ''));
if (lower.endsWith('?')) return this.isSystemType(lower.replace('?', ''));
if (lower.includes('<')) {
const base = lower.split('<')[0];
const generic = ['list', 'ienumerable', 'icollection', 'dictionary', 'nullable'];
if (generic.some(g => base.includes(g))) return true;
}
return false;
},
isActiveType(typeName) {
if (!typeName || this.isSystemType(typeName)) return false;
let base = typeName;
if (base.endsWith('[]')) base = base.replace('[]', '');
if (base.endsWith('?')) base = base.replace('?', '');
if (base.indexOf('<') > 0) base = base.substring(0, base.indexOf('<'));
return base.trim() === this.currentTypeFullName;
},
callMethod(methodName, params) {
const url = this.baseUrl + '/' + methodName;
return axios.post(url, params, {
headers: { 'Content-Type': 'application/json' }
}).then(res => {
const data = res.data;
if (data && data.success === false) {
throw new Error(data.msg || '服务调用失败');
}
return data;
});
},
scanDll() {
if (!this.filePath) {
this.$message.warning('请输入 DLL 文件路径');
return;
}
this.scanning = true;
this.callMethod('SaveServiceMethodInfo', { filePath: this.filePath })
.then(data => {
this.$message.success(data.msg || '扫描完成');
this.macId = data.macId || '';
this.currentTypeFullName = '';
this.saveRecentPath(this.filePath);
return this.loadData();
})
.catch(err => {
this.$message.error(err.message || '扫描失败');
})
.finally(() => {
this.scanning = false;
});
},
loadData() {
if (!this.macId) {
this.$message.warning('请先扫描 DLL');
return Promise.resolve();
}
this.loading = true;
return this.callMethod('GetServiceMethodData', { macId: this.macId })
.then(data => {
this.srcmd = data.SRCMD || [];
this.srcmethod = data.SRCMETHOD || [];
this.srcmethodparams = data.SRCMETHODPARAMS || [];
this.srctype = data.SRCTYPE || [];
this.srcprop = data.SRCPROP || [];
this.expandedModules = (data.SRCMD || []).map(md => md.MDID);
this.currentTypeFullName = '';
this.$message.success('数据加载完成');
})
.catch(err => {
this.$message.error(err.message || '加载数据失败');
})
.finally(() => {
this.loading = false;
});
},
showType(typeName) {
if (!typeName) return;
if (this.isSystemType(typeName)) return;
let base = typeName;
if (base.endsWith('[]')) base = base.replace('[]', '');
if (base.endsWith('?')) base = base.replace('?', '');
if (base.indexOf('<') > 0) base = base.substring(0, base.indexOf('<'));
base = base.trim();
if (!this.srctype.some(t => t.TYPEFULLNAME === base)) {
this.$message.info('该类型未在扫描结果中找到,可能为系统类型或未被引用');
return;
}
this.currentTypeFullName = base;
this.dialogVisible = true;
},
loadRecentPaths() {
try {
const list = JSON.parse(localStorage.getItem('svcMethodRecentPaths') || '[]');
this.recentPaths = Array.isArray(list) ? list : [];
} catch (e) {
this.recentPaths = [];
}
},
saveRecentPath(path) {
if (!path) return;
let list = this.recentPaths.filter(p => p !== path);
list.unshift(path);
if (list.length > 10) list = list.slice(0, 10);
this.recentPaths = list;
try {
localStorage.setItem('svcMethodRecentPaths', JSON.stringify(list));
} catch (e) {}
},
selectRecentPath(path) {
if (path === '__clear__') {
this.clearRecentPaths();
this.$message.success('已清空最近路径');
} else {
this.filePath = path;
}
},
clearRecentPaths() {
this.recentPaths = [];
try {
localStorage.removeItem('svcMethodRecentPaths');
} catch (e) {}
},
expandAllModules() {
this.expandedModules = this.srcmd.map(md => md.MDID);
},
collapseAllModules() {
this.expandedModules = [];
},
jumpToModule() {
if (!this.jumpModule) return;
const mdId = this.jumpModule;
if (!this.expandedModules.includes(mdId)) {
this.expandedModules.push(mdId);
}
this.$nextTick(() => {
const el = document.querySelector('[data-module-id="' + mdId + '"]');
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
},
clearFilter() {
this.filterText = '';
this.jumpModule = '';
},
copyMethodSignature(method) {
const params = method.params.map(p => p.PARAMTYPE + ' ' + p.PARAMNAME).join(', ');
const sig = (method.MODIFIERS || 'public') + ' ' + (method.RETTYPE || 'void') + ' ' + method.METHODNAME + '(' + params + ')';
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(sig).then(() => {
this.$message.success('方法签名已复制');
}).catch(() => {
this.$message.warning('复制失败,请手动复制');
});
} else {
const ta = document.createElement('textarea');
ta.value = sig;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
this.$message.success('方法签名已复制');
} catch (e) {
this.$message.warning('复制失败,请手动复制');
}
document.body.removeChild(ta);
}
}
},
mounted() {
this.loadRecentPaths();
const urlParams = new URLSearchParams(window.location.search);
const path = urlParams.get('path');
if (path) {
this.filePath = path;
} else if (this.recentPaths.length) {
this.filePath = this.recentPaths[0];
}
}
});
</script>
</body>
</html>