feat(订阅式软件提供): GVSDServiceServer 新增 Venom 包管理器与远端页面子工程
- apps/venom: VenomPackage/VenomPackageRelease 模型 + 0001 迁移,提供包列表/新建(包名全局唯一)/详情/配置保存/发布历史接口,并放行跨源 CORS - venom_ui: Vite+Vue SFC 子工程,单文件 ES module 产物,支持 mount/unmount 微应用契约,页面含包列表、新建、配置页、发布历史 - sub_server: VenomRemoteEntryView 改由 venom_ui 构建产物托管;remote_pages 占位模块改为 mount/unmount
This commit is contained in:
1 parent
b078f8e9fe
commit
c50f3f0a55
19 files changed
+2087
-45
No files matched your search
@@ -2,3 +2,7 @@ __pycache__/
|
||||
*.pyc
|
||||
# 本地开发库(本服务无本地模型,仅占位)
|
||||
db.sqlite3
|
||||
|
||||
# venom_ui 前端子工程:依赖与构建产物不入库(构建见 venom_ui/README 或 vite build)
|
||||
venom_ui/node_modules/
|
||||
venom_ui/dist/
|
||||
@@ -27,11 +27,14 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'apps.sub_server',
|
||||
'apps.venom',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
# Venom 远端页面在 GERP 宿主内渲染,需跨源调用 /api/venom/* 与加载 /venom/*
|
||||
'apps.venom.middleware.VenomCorsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'GVSDServiceServer.urls'
|
||||
|
||||
@@ -7,7 +7,9 @@ from apps.sub_server import views
|
||||
urlpatterns = [
|
||||
path('', views.HomeView.as_view(), name='home'),
|
||||
path('health/', views.HealthView.as_view(), name='health'),
|
||||
# 产品定制页的远端组件入口(宿主 GERP 运行时 import 的 ES module)
|
||||
# 产品定制页的远端组件入口(宿主 GERP 运行时 import 的 ES module,构建产物托管)
|
||||
path('venom/remote-entry.js', views.VenomRemoteEntryView.as_view(), name='venom_remote_entry'),
|
||||
path('api/sub/', include('apps.sub_server.urls')),
|
||||
# Venom 包管理器业务接口(远端页面跨源调用)
|
||||
path('api/venom/', include('apps.venom.urls')),
|
||||
]
|
||||
@@ -1,51 +1,29 @@
|
||||
"""模块:产品定制页的远端组件(ES module)
|
||||
"""模块:Venom 定制页远端入口的**占位实现**(仅在 UI 未构建时使用)
|
||||
|
||||
宿主 GERP 前端通过 `import(<RemoteEntry>)` 加载本模块,并用其中的
|
||||
`createComponent(Host)` 工厂取得 Vue 组件,然后**原生渲染**(非 iframe)。
|
||||
真实产物由 `venom_ui`(Vite + Vue SFC)构建得到 `venom_ui/dist/venom-entry.js`,
|
||||
由 `apps.sub_server.views.VenomRemoteEntryView` 直接托管。
|
||||
|
||||
契约(与 GERP `src/apps/remoteLoader.ts` 一致):
|
||||
export function createComponent(Host) -> Vue 组件
|
||||
Host = { Vue, VueRouter, Pinia, ElementPlus }(宿主注入 → 共享同一份运行时)
|
||||
|
||||
真实项目里这份产物可由 Vite 打包 Vue SFC 得到;此处用渲染函数写成等价实现,
|
||||
以便子服务器无需 Node 构建链即可交付。
|
||||
远端模块契约(与 GERP `src/apps/remoteLoader.ts` 一致,二者满足其一即可):
|
||||
1) 微应用(推荐,页面自带运行时):
|
||||
export function mount(Element, Context) -> void
|
||||
export function unmount(Element) -> void
|
||||
2) 组件工厂(宿主渲染):
|
||||
export function createComponent(Host) -> Vue 组件
|
||||
"""
|
||||
|
||||
VENOM_REMOTE_ENTRY_JS = r"""/**
|
||||
* Venom 定制页 —— 远端组件入口(由 GVSDServiceServer 托管)
|
||||
* 契约:export function createComponent(Host) -> Vue 组件
|
||||
* 占位模块:Venom 定制页 UI 尚未构建
|
||||
* 构建方式:cd venom_ui && npm install && npm run build
|
||||
*/
|
||||
export function createComponent(Host) {
|
||||
const { h, ref } = Host.Vue
|
||||
const { ElCard, ElButton, ElTag, ElMessage } = Host.ElementPlus
|
||||
export function mount(Element) {
|
||||
Element.innerHTML =
|
||||
'<div style="padding:24px;font-size:14px;color:#909399;line-height:1.9">' +
|
||||
'<b>Venom 定制页 UI 尚未构建</b><br/>' +
|
||||
'请在 GVSDServiceServer/venom_ui 下执行 <code>npm install</code> 与 <code>npm run build</code> 后刷新本页。' +
|
||||
'</div>'
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'VenomCustomPage',
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
return () =>
|
||||
h('div', { style: 'padding: 4px' }, [
|
||||
h(ElCard, { shadow: 'never' }, {
|
||||
header: () =>
|
||||
h('div', { style: 'display: flex; align-items: center; gap: 8px' }, [
|
||||
h('b', '这是Venom'),
|
||||
h(ElTag, { type: 'danger', effect: 'plain', size: 'small' }, () => 'Venom 定制页 · 由 GVSDServiceServer 托管'),
|
||||
]),
|
||||
default: () =>
|
||||
h('div', [
|
||||
h('div', { style: 'font-size: 20px; font-weight: 700; padding: 8px 0' }, '这是Venom'),
|
||||
h('div', { style: 'font-size: 13px; line-height: 2; color: var(--el-text-color-secondary)' }, [
|
||||
h('div', '该页面组件由提供者子服务器 GVSDServiceServer 托管,GERP 宿主运行时加载后原生渲染(非 iframe)。'),
|
||||
h('div', ['点击次数:', h('b', String(count.value))]),
|
||||
]),
|
||||
h('div', { style: 'margin-top: 12px; display: flex; gap: 8px' }, [
|
||||
h(ElButton, { type: 'primary', onClick: () => { count.value += 1 } }, () => '点我 +1'),
|
||||
h(ElButton, { onClick: () => { ElMessage.success('来自 Venom 定制页') } }, () => '弹提示'),
|
||||
]),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
},
|
||||
}
|
||||
export function unmount(Element) {
|
||||
Element.innerHTML = ''
|
||||
}
|
||||
"""
|
||||
@@ -4,6 +4,7 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
@@ -75,14 +76,22 @@ class MSyncStatusView(APIView):
|
||||
|
||||
|
||||
class VenomRemoteEntryView(APIView):
|
||||
"""Venom 定制页的远端组件入口(ES module,宿主 GERP 运行时 import 后原生渲染)
|
||||
"""Venom 定制页的远端入口(ES module,宿主 GERP 运行时 import 后原生渲染)
|
||||
|
||||
内容来自 venom_ui 的 Vite 构建产物(venom_ui/dist/venom-entry.js);
|
||||
未构建时回退到占位提示模块,避免 404/500。
|
||||
|
||||
宿主页面多为 https 源、本服务在 http://127.0.0.1:25090,故 ES module 加载属**跨源**,
|
||||
必须放行 CORS(`Access-Control-Allow-Origin`),否则浏览器会拒绝该模块。
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
response = HttpResponse(VENOM_REMOTE_ENTRY_JS, content_type='text/javascript; charset=utf-8')
|
||||
dist_file = Path(settings.BASE_DIR) / 'venom_ui' / 'dist' / 'venom-entry.js'
|
||||
if dist_file.is_file():
|
||||
content = dist_file.read_text(encoding='utf-8')
|
||||
else:
|
||||
content = VENOM_REMOTE_ENTRY_JS
|
||||
response = HttpResponse(content, content_type='text/javascript; charset=utf-8')
|
||||
response['Access-Control-Allow-Origin'] = '*'
|
||||
response['Cache-Control'] = 'no-store'
|
||||
return response
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Venom 包管理器(提供者子服务器的产品业务)
|
||||
"""
|
||||
@@ -0,0 +1,10 @@
|
||||
"""模块:Venom 包管理器 AppConfig"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class VenomConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.venom'
|
||||
verbose_name = 'Venom 包管理器'
|
||||
verbose_name_plural = 'Venom 包管理器'
|
||||
@@ -0,0 +1,23 @@
|
||||
"""模块:Venom 相关路径的 CORS 中间件
|
||||
|
||||
远端页面在 GERP 宿主内渲染(宿主多为 https 源),页面需跨源调用本服务的
|
||||
`/api/venom/*` 与加载 `/venom/remote-entry.js`,故统一放行 CORS。
|
||||
本服务为演示子服务器,未启用 CSRF/Session 鉴权。
|
||||
"""
|
||||
|
||||
|
||||
class VenomCorsMiddleware:
|
||||
SERVICE_ORIGIN = '*'
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
path = request.path or ''
|
||||
if path.startswith('/api/venom/') or path.startswith('/venom/'):
|
||||
response['Access-Control-Allow-Origin'] = self.SERVICE_ORIGIN
|
||||
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
||||
response['Access-Control-Allow-Headers'] = 'Content-Type, X-Operator'
|
||||
response['Access-Control-Max-Age'] = '600'
|
||||
return response
|
||||
@@ -0,0 +1,51 @@
|
||||
# Generated by Django 6.0.5 on 2026-10-05 17:41
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VenomPackage',
|
||||
fields=[
|
||||
('PackageUUID', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='软件包UUID')),
|
||||
('PackageName', models.CharField(max_length=128, unique=True, verbose_name='包名(全局唯一)')),
|
||||
('PackageDesc', models.CharField(default='', max_length=512, verbose_name='包说明')),
|
||||
('ConfigText', models.TextField(default='', verbose_name='包配置内容')),
|
||||
('CreatorName', models.CharField(default='', max_length=128, verbose_name='创建人')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Venom 软件包',
|
||||
'verbose_name_plural': 'Venom 软件包',
|
||||
'db_table': 'VenomPackage',
|
||||
'ordering': ['-CreateTime'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VenomPackageRelease',
|
||||
fields=[
|
||||
('ReleaseUUID', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='发布UUID')),
|
||||
('Version', models.CharField(max_length=64, verbose_name='版本号')),
|
||||
('Notes', models.TextField(default='', verbose_name='发布说明')),
|
||||
('PublisherName', models.CharField(default='', max_length=128, verbose_name='发布人')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='发布时间')),
|
||||
('Package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='venom.venompackage', verbose_name='所属软件包')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Venom 发布记录',
|
||||
'verbose_name_plural': 'Venom 发布记录',
|
||||
'db_table': 'VenomPackageRelease',
|
||||
'ordering': ['-CreateTime'],
|
||||
},
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -0,0 +1,53 @@
|
||||
"""模块:Venom 包管理器数据模型
|
||||
|
||||
VenomPackage —— 软件包(包名全局唯一)
|
||||
VenomPackageRelease—— 包的发布历史(每次发布一条,含版本与说明)
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class VenomPackage(models.Model):
|
||||
"""软件包——包名全局唯一"""
|
||||
|
||||
PackageUUID = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False,
|
||||
verbose_name='软件包UUID')
|
||||
PackageName = models.CharField(max_length=128, unique=True, verbose_name='包名(全局唯一)')
|
||||
PackageDesc = models.CharField(max_length=512, default='', verbose_name='包说明')
|
||||
ConfigText = models.TextField(default='', verbose_name='包配置内容')
|
||||
CreatorName = models.CharField(max_length=128, default='', verbose_name='创建人')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'VenomPackage'
|
||||
ordering = ['-CreateTime']
|
||||
verbose_name = 'Venom 软件包'
|
||||
verbose_name_plural = 'Venom 软件包'
|
||||
|
||||
def __str__(self):
|
||||
return self.PackageName
|
||||
|
||||
|
||||
class VenomPackageRelease(models.Model):
|
||||
"""包的发布历史"""
|
||||
|
||||
ReleaseUUID = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False,
|
||||
verbose_name='发布UUID')
|
||||
Package = models.ForeignKey(VenomPackage, on_delete=models.CASCADE, related_name='releases',
|
||||
verbose_name='所属软件包')
|
||||
Version = models.CharField(max_length=64, verbose_name='版本号')
|
||||
Notes = models.TextField(default='', verbose_name='发布说明')
|
||||
PublisherName = models.CharField(max_length=128, default='', verbose_name='发布人')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='发布时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'VenomPackageRelease'
|
||||
ordering = ['-CreateTime']
|
||||
verbose_name = 'Venom 发布记录'
|
||||
verbose_name_plural = 'Venom 发布记录'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.Package.PackageName}@{self.Version}'
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Venom 包管理器 API 路由(挂载于 /api/venom/)"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'venom'
|
||||
|
||||
urlpatterns = [
|
||||
path('packages/', views.PackageListView.as_view(), name='package_list'),
|
||||
path('packages/<uuid:package_uuid>/', views.PackageDetailView.as_view(), name='package_detail'),
|
||||
path('packages/<uuid:package_uuid>/config/', views.PackageConfigView.as_view(), name='package_config'),
|
||||
path('packages/<uuid:package_uuid>/releases/', views.PackageReleaseView.as_view(), name='package_releases'),
|
||||
]
|
||||
@@ -0,0 +1,146 @@
|
||||
"""模块:Venom 包管理器 API
|
||||
|
||||
远端页面(由本服务托管、在 GERP 宿主内渲染)跨源调用以下接口:
|
||||
GET /api/venom/packages/ 包列表
|
||||
POST /api/venom/packages/ 新建包(包名全局唯一)
|
||||
GET /api/venom/packages/<uuid>/ 包详情(配置页读取)
|
||||
POST /api/venom/packages/<uuid>/config/ 保存包配置
|
||||
GET /api/venom/packages/<uuid>/releases/ 发布历史
|
||||
POST /api/venom/packages/<uuid>/releases/ 发布新版本(写历史)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.db.models import Count
|
||||
from django.utils.dateformat import format as date_format
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import VenomPackage, VenomPackageRelease
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ok(data=None, msg='success'):
|
||||
return Response({'code': 200, 'msg': msg, 'data': data})
|
||||
|
||||
|
||||
def _err(msg, code=400, status=None):
|
||||
return Response({'code': code, 'msg': msg, 'data': None}, status=status or 200)
|
||||
|
||||
|
||||
def _Time(value):
|
||||
return date_format(value, 'Y-m-d H:i:s') if value else ''
|
||||
|
||||
|
||||
def _PackageRow(package):
|
||||
return {
|
||||
'PackageUUID': str(package.PackageUUID),
|
||||
'PackageName': package.PackageName,
|
||||
'PackageDesc': package.PackageDesc or '',
|
||||
'CreatorName': package.CreatorName or '',
|
||||
'CreateTime': _Time(package.CreateTime),
|
||||
'UpdateTime': _Time(package.UpdateTime),
|
||||
'ReleaseCount': getattr(package, 'release_count', 0),
|
||||
}
|
||||
|
||||
|
||||
def _ReleaseRow(release):
|
||||
return {
|
||||
'ReleaseUUID': str(release.ReleaseUUID),
|
||||
'Version': release.Version,
|
||||
'Notes': release.Notes or '',
|
||||
'PublisherName': release.PublisherName or '',
|
||||
'CreateTime': _Time(release.CreateTime),
|
||||
}
|
||||
|
||||
|
||||
def _GetPackage(PackageUUID):
|
||||
return VenomPackage.objects.annotate(release_count=Count('releases')).filter(
|
||||
PackageUUID=PackageUUID).first()
|
||||
|
||||
|
||||
class PackageListView(APIView):
|
||||
"""包列表 / 新建包"""
|
||||
|
||||
def get(self, request):
|
||||
keyword = (request.query_params.get('Keyword') or '').strip()
|
||||
qs = VenomPackage.objects.annotate(release_count=Count('releases'))
|
||||
if keyword:
|
||||
qs = qs.filter(PackageName__icontains=keyword)
|
||||
rows = [_PackageRow(p) for p in qs]
|
||||
return _ok({'list': rows, 'total': len(rows)})
|
||||
|
||||
def post(self, request):
|
||||
name = (request.data.get('PackageName') or '').strip()
|
||||
if not name:
|
||||
return _err('请填写包名')
|
||||
if len(name) > 128:
|
||||
return _err('包名过长(最多 128 字)')
|
||||
if VenomPackage.objects.filter(PackageName=name).exists():
|
||||
return _err(f'包名「{name}」已存在(包名必须全局唯一)')
|
||||
package = VenomPackage.objects.create(
|
||||
PackageName=name,
|
||||
PackageDesc=(request.data.get('PackageDesc') or '').strip()[:512],
|
||||
ConfigText=request.data.get('ConfigText') or '',
|
||||
CreatorName=(request.data.get('CreatorName') or '').strip()[:128],
|
||||
)
|
||||
package = _GetPackage(package.PackageUUID)
|
||||
return _ok(_PackageRow(package), msg='软件包已创建')
|
||||
|
||||
|
||||
class PackageDetailView(APIView):
|
||||
"""包详情(配置页读取)"""
|
||||
|
||||
def get(self, request, package_uuid):
|
||||
package = _GetPackage(package_uuid)
|
||||
if package is None:
|
||||
return _err('软件包不存在', 404)
|
||||
row = _PackageRow(package)
|
||||
row['ConfigText'] = package.ConfigText or ''
|
||||
return _ok(row)
|
||||
|
||||
|
||||
class PackageConfigView(APIView):
|
||||
"""保存包配置"""
|
||||
|
||||
def post(self, request, package_uuid):
|
||||
package = VenomPackage.objects.filter(PackageUUID=package_uuid).first()
|
||||
if package is None:
|
||||
return _err('软件包不存在', 404)
|
||||
if 'PackageDesc' in request.data:
|
||||
package.PackageDesc = (request.data.get('PackageDesc') or '').strip()[:512]
|
||||
if 'ConfigText' in request.data:
|
||||
package.ConfigText = request.data.get('ConfigText') or ''
|
||||
package.save(update_fields=['PackageDesc', 'ConfigText', 'UpdateTime'])
|
||||
row = _PackageRow(_GetPackage(package.PackageUUID))
|
||||
row['ConfigText'] = package.ConfigText or ''
|
||||
return _ok(row, msg='配置已保存')
|
||||
|
||||
|
||||
class PackageReleaseView(APIView):
|
||||
"""发布历史 / 发布新版本"""
|
||||
|
||||
def get(self, request, package_uuid):
|
||||
package = VenomPackage.objects.filter(PackageUUID=package_uuid).first()
|
||||
if package is None:
|
||||
return _err('软件包不存在', 404)
|
||||
rows = [_ReleaseRow(r) for r in package.releases.all()]
|
||||
return _ok({'PackageName': package.PackageName, 'list': rows, 'total': len(rows)})
|
||||
|
||||
def post(self, request, package_uuid):
|
||||
package = VenomPackage.objects.filter(PackageUUID=package_uuid).first()
|
||||
if package is None:
|
||||
return _err('软件包不存在', 404)
|
||||
version = (request.data.get('Version') or '').strip()
|
||||
if not version:
|
||||
return _err('请填写版本号')
|
||||
if len(version) > 64:
|
||||
return _err('版本号过长(最多 64 字)')
|
||||
release = VenomPackageRelease.objects.create(
|
||||
Package=package,
|
||||
Version=version,
|
||||
Notes=request.data.get('Notes') or '',
|
||||
PublisherName=(request.data.get('PublisherName') or '').strip()[:128],
|
||||
)
|
||||
return _ok(_ReleaseRow(release), msg='已发布')
|
||||
+1251
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "venom-ui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Venom 包管理器定制页(远端微前端产物,由 GVSDServiceServer 托管)",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-plus": "^2.14.3",
|
||||
"vue": "^3.5.40"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"vite": "^8.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Venom 包管理器(产品定制页)
|
||||
*
|
||||
* 列表视图:展示全部软件包(包名全局唯一),可新建、可打开包配置页
|
||||
* 配置视图:编辑包说明/配置内容、发布新版本、查看发布历史
|
||||
*
|
||||
* 视图切换在组件内部完成(不占用宿主路由),符合「远端页面自理」的微前端约定。
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
venomApi,
|
||||
type RemoteContext,
|
||||
type VenomPackageItem,
|
||||
type VenomReleaseItem,
|
||||
} from './api'
|
||||
|
||||
const props = defineProps<{ Context?: RemoteContext }>()
|
||||
|
||||
const operatorName = computed(() => props.Context?.UserName || '')
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const publishing = ref(false)
|
||||
const keyword = ref('')
|
||||
const packages = ref<VenomPackageItem[]>([])
|
||||
|
||||
/** '' = 列表视图;否则为当前打开配置页的包 UUID */
|
||||
const openedUUID = ref('')
|
||||
const detail = ref<VenomPackageItem | null>(null)
|
||||
const releases = ref<VenomReleaseItem[]>([])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const createForm = reactive({ PackageName: '', PackageDesc: '', ConfigText: '' })
|
||||
|
||||
const configForm = reactive({ PackageDesc: '', ConfigText: '' })
|
||||
const releaseForm = reactive({ Version: '', Notes: '' })
|
||||
|
||||
async function loadPackages(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await venomApi.listPackages(keyword.value.trim())
|
||||
packages.value = r.list || []
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate(): void {
|
||||
createForm.PackageName = ''
|
||||
createForm.PackageDesc = ''
|
||||
createForm.ConfigText = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitCreate(): Promise<void> {
|
||||
if (!createForm.PackageName.trim()) {
|
||||
ElMessage.warning('请填写包名')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
await venomApi.createPackage({
|
||||
PackageName: createForm.PackageName.trim(),
|
||||
PackageDesc: createForm.PackageDesc,
|
||||
ConfigText: createForm.ConfigText,
|
||||
CreatorName: operatorName.value,
|
||||
})
|
||||
ElMessage.success('软件包已创建')
|
||||
dialogVisible.value = false
|
||||
await loadPackages()
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开包的配置页 */
|
||||
async function openConfig(row: VenomPackageItem): Promise<void> {
|
||||
openedUUID.value = row.PackageUUID
|
||||
loading.value = true
|
||||
try {
|
||||
const [d, r] = await Promise.all([
|
||||
venomApi.getPackage(row.PackageUUID),
|
||||
venomApi.listReleases(row.PackageUUID),
|
||||
])
|
||||
detail.value = d
|
||||
configForm.PackageDesc = d.PackageDesc || ''
|
||||
configForm.ConfigText = d.ConfigText || ''
|
||||
releases.value = r.list || []
|
||||
releaseForm.Version = ''
|
||||
releaseForm.Notes = ''
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message)
|
||||
openedUUID.value = ''
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function backToList(): void {
|
||||
openedUUID.value = ''
|
||||
detail.value = null
|
||||
releases.value = []
|
||||
}
|
||||
|
||||
async function saveConfig(): Promise<void> {
|
||||
if (!detail.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
detail.value = await venomApi.saveConfig(detail.value.PackageUUID, {
|
||||
PackageDesc: configForm.PackageDesc,
|
||||
ConfigText: configForm.ConfigText,
|
||||
})
|
||||
ElMessage.success('配置已保存')
|
||||
await loadPackages()
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function publishRelease(): Promise<void> {
|
||||
if (!detail.value) return
|
||||
if (!releaseForm.Version.trim()) {
|
||||
ElMessage.warning('请填写版本号')
|
||||
return
|
||||
}
|
||||
publishing.value = true
|
||||
try {
|
||||
await venomApi.publishRelease(detail.value.PackageUUID, {
|
||||
Version: releaseForm.Version.trim(),
|
||||
Notes: releaseForm.Notes,
|
||||
PublisherName: operatorName.value,
|
||||
})
|
||||
ElMessage.success('已发布')
|
||||
releaseForm.Version = ''
|
||||
releaseForm.Notes = ''
|
||||
const r = await venomApi.listReleases(detail.value.PackageUUID)
|
||||
releases.value = r.list || []
|
||||
await loadPackages()
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message)
|
||||
} finally {
|
||||
publishing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadPackages)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="venom-page">
|
||||
<!-- ==================== 列表视图 ==================== -->
|
||||
<template v-if="!openedUUID">
|
||||
<div class="head">
|
||||
<div class="brand">
|
||||
<span class="brand-name">这是Venom</span>
|
||||
<span class="brand-sub">Venom 包管理器 · 软件包列表</span>
|
||||
</div>
|
||||
<div class="spacer" />
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="按包名搜索"
|
||||
clearable
|
||||
class="search"
|
||||
@keyup.enter="loadPackages"
|
||||
@clear="loadPackages"
|
||||
/>
|
||||
<el-button @click="loadPackages">刷新</el-button>
|
||||
<el-button type="primary" @click="openCreate">新建软件包</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="packages" border size="default" class="table">
|
||||
<el-table-column prop="PackageName" label="包名" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="pkg-name">{{ row.PackageName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="PackageDesc" label="说明" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="CreatorName" label="创建人" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="CreateTime" label="创建时间" width="180" />
|
||||
<el-table-column prop="ReleaseCount" label="发布数" width="90" align="center" />
|
||||
<el-table-column label="操作" width="130" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openConfig(row)">打开配置</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="还没有软件包,点右上角「新建软件包」开始" :image-size="80" />
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<!-- ==================== 配置视图 ==================== -->
|
||||
<template v-else>
|
||||
<div class="head">
|
||||
<el-button link @click="backToList">← 返回列表</el-button>
|
||||
<div class="brand">
|
||||
<span class="brand-name">{{ detail?.PackageName || '' }}</span>
|
||||
<span class="brand-sub">包配置</span>
|
||||
</div>
|
||||
<div class="spacer" />
|
||||
<el-tag size="small" effect="plain">发布 {{ detail?.ReleaseCount || 0 }} 次</el-tag>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">包配置</el-divider>
|
||||
<el-form label-width="96px" class="form">
|
||||
<el-form-item label="包名">
|
||||
<el-input :model-value="detail?.PackageName || ''" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="包说明">
|
||||
<el-input v-model="configForm.PackageDesc" maxlength="512" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置内容">
|
||||
<el-input
|
||||
v-model="configForm.ConfigText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="包配置内容(如依赖、构建参数等)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveConfig">保存配置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider content-position="left">发布新版本</el-divider>
|
||||
<el-form label-width="96px" class="form">
|
||||
<el-form-item label="版本号">
|
||||
<el-input v-model="releaseForm.Version" maxlength="64" placeholder="如 1.0.0" class="version" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发布说明">
|
||||
<el-input v-model="releaseForm.Notes" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="success" :loading="publishing" @click="publishRelease">发布</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider content-position="left">发布历史</el-divider>
|
||||
<el-table :data="releases" border size="small" class="table">
|
||||
<el-table-column prop="Version" label="版本" width="140" />
|
||||
<el-table-column prop="PublisherName" label="发布人" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="CreateTime" label="发布时间" width="180" />
|
||||
<el-table-column prop="Notes" label="发布说明" min-width="200" show-overflow-tooltip />
|
||||
<template #empty>
|
||||
<el-empty description="暂无发布记录" :image-size="60" />
|
||||
</template>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<!-- ==================== 新建软件包 ==================== -->
|
||||
<el-dialog v-model="dialogVisible" title="新建软件包" width="620px" :close-on-click-modal="false">
|
||||
<el-form label-width="96px">
|
||||
<el-form-item label="包名" required>
|
||||
<el-input v-model="createForm.PackageName" maxlength="128" placeholder="包名必须全局唯一" />
|
||||
</el-form-item>
|
||||
<el-form-item label="包说明">
|
||||
<el-input v-model="createForm.PackageDesc" maxlength="512" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置内容">
|
||||
<el-input v-model="createForm.ConfigText" type="textarea" :rows="5" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitCreate">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.venom-page {
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
.brand-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.brand-sub {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.search {
|
||||
width: 220px;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
}
|
||||
.form {
|
||||
max-width: 720px;
|
||||
}
|
||||
.version {
|
||||
width: 220px;
|
||||
}
|
||||
.pkg-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
/** Venom 包管理器 API 客户端
|
||||
*
|
||||
* 远端模块由 GVSDServiceServer 自身托管,故用 `import.meta.url` 推断服务地址,
|
||||
* 无需宿主额外注入(跨源由子服务器的 CORS 中间件放行)。
|
||||
*/
|
||||
|
||||
const API_BASE = (() => {
|
||||
try {
|
||||
return new URL('/api/venom', import.meta.url).toString()
|
||||
} catch {
|
||||
return '/api/venom'
|
||||
}
|
||||
})()
|
||||
|
||||
export interface VenomPackageItem {
|
||||
PackageUUID: string
|
||||
PackageName: string
|
||||
PackageDesc: string
|
||||
CreatorName: string
|
||||
CreateTime: string
|
||||
UpdateTime: string
|
||||
ReleaseCount: number
|
||||
ConfigText?: string
|
||||
}
|
||||
|
||||
/** 宿主注入的上下文(当前登录人/公司,用于「创建人」「发布人」) */
|
||||
export interface RemoteContext {
|
||||
UserUUID?: string
|
||||
UserName?: string
|
||||
CompanyUUID?: string
|
||||
CompanyName?: string
|
||||
}
|
||||
|
||||
export interface VenomReleaseItem {
|
||||
ReleaseUUID: string
|
||||
Version: string
|
||||
Notes: string
|
||||
PublisherName: string
|
||||
CreateTime: string
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
let payload: { code?: number; msg?: string; data?: unknown } | null = null
|
||||
try {
|
||||
payload = await response.json()
|
||||
} catch {
|
||||
throw new Error(`服务响应异常(HTTP ${response.status})`)
|
||||
}
|
||||
if (!payload || payload.code !== 200) {
|
||||
throw new Error(payload?.msg || `请求失败(HTTP ${response.status})`)
|
||||
}
|
||||
return payload.data as T
|
||||
}
|
||||
|
||||
export const venomApi = {
|
||||
listPackages: (keyword = '') =>
|
||||
request<{ list: VenomPackageItem[]; total: number }>(
|
||||
'GET',
|
||||
`/packages/${keyword ? `?Keyword=${encodeURIComponent(keyword)}` : ''}`,
|
||||
),
|
||||
createPackage: (data: { PackageName: string; PackageDesc: string; ConfigText: string; CreatorName: string }) =>
|
||||
request<VenomPackageItem>('POST', '/packages/', data),
|
||||
getPackage: (uuid: string) => request<VenomPackageItem>('GET', `/packages/${uuid}/`),
|
||||
saveConfig: (uuid: string, data: { PackageDesc: string; ConfigText: string }) =>
|
||||
request<VenomPackageItem>('POST', `/packages/${uuid}/config/`, data),
|
||||
listReleases: (uuid: string) =>
|
||||
request<{ PackageName: string; list: VenomReleaseItem[]; total: number }>(
|
||||
'GET',
|
||||
`/packages/${uuid}/releases/`,
|
||||
),
|
||||
publishRelease: (uuid: string, data: { Version: string; Notes: string; PublisherName: string }) =>
|
||||
request<VenomReleaseItem>('POST', `/packages/${uuid}/releases/`, data),
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Venom 定制页远端入口(微应用契约)
|
||||
*
|
||||
* 宿主 GERP 通过 `import(<RemoteEntry>)` 加载本模块,调用:
|
||||
* mount(Element, Context) —— 在给定容器内挂载本页面(自带运行时)
|
||||
* unmount(Element) —— 卸载并清空容器
|
||||
*
|
||||
* 与宿主组件工厂契约 createComponent(Host) 并列,二者满足其一即可(见 GERP remoteLoader.ts)。
|
||||
* 本页面自带 Vue/Element Plus,运行时可与会话完全自洽。
|
||||
*/
|
||||
import { createApp, type App as VueApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import type { RemoteContext } from './api'
|
||||
|
||||
let app: VueApp | null = null
|
||||
|
||||
export function mount(Element: HTMLElement, Context: RemoteContext = {}): void {
|
||||
app = createApp(App, { Context: Context || {} })
|
||||
app.use(ElementPlus)
|
||||
app.mount(Element)
|
||||
}
|
||||
|
||||
export function unmount(Element: HTMLElement): void {
|
||||
if (app) {
|
||||
app.unmount()
|
||||
app = null
|
||||
}
|
||||
Element.innerHTML = ''
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { defineConfig, type Plugin } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
/**
|
||||
* 把构建出的 CSS 内联进 JS(远端入口要求单文件:宿主只 import 这一个 ES module)。
|
||||
* 产物在页面加载时自动注入 <style>,避免宿主再去加载额外的 .css。
|
||||
*/
|
||||
function InlineCssPlugin(): Plugin {
|
||||
return {
|
||||
name: 'venom-inline-css',
|
||||
enforce: 'post',
|
||||
generateBundle(_options, bundle) {
|
||||
const cssAsset = Object.values(bundle).find(
|
||||
(f) => f.type === 'asset' && String(f.fileName).endsWith('.css'),
|
||||
)
|
||||
if (!cssAsset || cssAsset.type !== 'asset') return
|
||||
const css = String(cssAsset.source)
|
||||
delete bundle[cssAsset.fileName]
|
||||
const entry = Object.values(bundle).find((f) => f.type === 'chunk' && f.isEntry)
|
||||
if (!entry || entry.type !== 'chunk') return
|
||||
entry.code +=
|
||||
'\n;(function(){try{var s=document.createElement("style");' +
|
||||
's.setAttribute("data-venom-ui","");s.textContent=' + JSON.stringify(css) +
|
||||
';document.head.appendChild(s)}catch(e){}})();\n'
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), InlineCssPlugin()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
cssCodeSplit: false,
|
||||
// 单文件 ES module 产物:venom-entry.js(由 GVSDServiceServer 托管)
|
||||
lib: {
|
||||
entry: 'src/main.ts',
|
||||
formats: ['es'],
|
||||
fileName: () => 'venom-entry.js',
|
||||
},
|
||||
rollupOptions: {
|
||||
output: { inlineDynamicImports: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user