diff --git a/GVSDServiceServer/apps/venom/migrations/0002_venompackage_dependencies_venompackage_readme.py b/GVSDServiceServer/apps/venom/migrations/0002_venompackage_dependencies_venompackage_readme.py new file mode 100644 index 00000000..b5e174d4 --- /dev/null +++ b/GVSDServiceServer/apps/venom/migrations/0002_venompackage_dependencies_venompackage_readme.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.5 on 2026-10-07 18:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('venom', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='venompackage', + name='Dependencies', + field=models.JSONField(blank=True, default=list, verbose_name='依赖列表'), + ), + migrations.AddField( + model_name='venompackage', + name='Readme', + field=models.TextField(default='', verbose_name='README(Markdown 原文)'), + ), + ] diff --git a/GVSDServiceServer/apps/venom/models.py b/GVSDServiceServer/apps/venom/models.py index d412b1d8..362aaf62 100644 --- a/GVSDServiceServer/apps/venom/models.py +++ b/GVSDServiceServer/apps/venom/models.py @@ -17,6 +17,10 @@ class VenomPackage(models.Model): 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='包配置内容') + #: 依赖列表:[{'Name': 'xxx', 'Version': '^1.0.0'}];Version 可为空(无版本约束) + Dependencies = models.JSONField(default=list, blank=True, verbose_name='依赖列表') + #: README(Markdown 原文,由官网前端渲染) + Readme = models.TextField(default='', verbose_name='README(Markdown 原文)') 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='更新时间') diff --git a/GVSDServiceServer/apps/venom/urls.py b/GVSDServiceServer/apps/venom/urls.py index 9ba255c2..c23d1fef 100644 --- a/GVSDServiceServer/apps/venom/urls.py +++ b/GVSDServiceServer/apps/venom/urls.py @@ -11,4 +11,6 @@ urlpatterns = [ path('packages//', views.PackageDetailView.as_view(), name='package_detail'), path('packages//config/', views.PackageConfigView.as_view(), name='package_config'), path('packages//releases/', views.PackageReleaseView.as_view(), name='package_releases'), + # 官网按包名取详情(venom.gvsds.com/project/<包名>/) + path('project//', views.PackageByNameView.as_view(), name='package_by_name'), ] diff --git a/GVSDServiceServer/apps/venom/views.py b/GVSDServiceServer/apps/venom/views.py index 15c11aff..6aa5a1db 100644 --- a/GVSDServiceServer/apps/venom/views.py +++ b/GVSDServiceServer/apps/venom/views.py @@ -7,6 +7,7 @@ POST /api/venom/packages//config/ 保存包配置 GET /api/venom/packages//releases/ 发布历史 POST /api/venom/packages//releases/ 发布新版本(写历史) + GET /api/venom/project/<包名>/ 按包名取详情(官网调用:含 README 与依赖列表) """ import logging @@ -60,6 +61,42 @@ def _GetPackage(PackageUUID): PackageUUID=PackageUUID).first() +def _NormalizeDependencies(value): + """依赖列表标准化为 [{'Name', 'Version'}];非法项丢弃、同名去重、限 200 条 + + 兼容两种入参:字符串(仅包名)与对象 {'Name','Version'}(Version 可为空 = 无版本约束)。 + """ + if not isinstance(value, list): + return [] + rows, seen = [], set() + for item in value[:200]: + if isinstance(item, str): + name, version = item.strip(), '' + elif isinstance(item, dict): + name = str(item.get('Name') or '').strip() + version = str(item.get('Version') or '').strip() + else: + continue + if not name or name in seen: + continue + seen.add(name) + rows.append({'Name': name[:128], 'Version': version[:64]}) + return rows + + +def _DetailRow(package): + """包详情行(含 README/依赖/发布历史;官网与配置页共用)""" + releases = list(package.releases.all()) + return { + **_PackageRow(package), + 'ConfigText': package.ConfigText or '', + 'Readme': package.Readme or '', + 'Dependencies': package.Dependencies or [], + 'LatestVersion': releases[0].Version if releases else '', + 'Releases': [_ReleaseRow(r) for r in releases], + } + + class PackageListView(APIView): """包列表 / 新建包""" @@ -83,26 +120,26 @@ class PackageListView(APIView): PackageName=name, PackageDesc=(request.data.get('PackageDesc') or '').strip()[:512], ConfigText=request.data.get('ConfigText') or '', + Dependencies=_NormalizeDependencies(request.data.get('Dependencies')), + Readme=request.data.get('Readme') or '', CreatorName=(request.data.get('CreatorName') or '').strip()[:128], ) package = _GetPackage(package.PackageUUID) - return _ok(_PackageRow(package), msg='软件包已创建') + return _ok(_DetailRow(package), msg='软件包已创建') class PackageDetailView(APIView): - """包详情(配置页读取)""" + """包详情(配置页读取;含 README / 依赖列表 / 发布历史)""" 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) + return _ok(_DetailRow(package)) class PackageConfigView(APIView): - """保存包配置""" + """保存包配置(说明 / 配置内容 / 依赖列表 / README)""" def post(self, request, package_uuid): package = VenomPackage.objects.filter(PackageUUID=package_uuid).first() @@ -112,10 +149,13 @@ class PackageConfigView(APIView): 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='配置已保存') + if 'Dependencies' in request.data: + package.Dependencies = _NormalizeDependencies(request.data.get('Dependencies')) + if 'Readme' in request.data: + package.Readme = request.data.get('Readme') or '' + package.save(update_fields=[ + 'PackageDesc', 'ConfigText', 'Dependencies', 'Readme', 'UpdateTime']) + return _ok(_DetailRow(_GetPackage(package.PackageUUID)), msg='配置已保存') class PackageReleaseView(APIView): @@ -144,3 +184,18 @@ class PackageReleaseView(APIView): PublisherName=(request.data.get('PublisherName') or '').strip()[:128], ) return _ok(_ReleaseRow(release), msg='已发布') + + +class PackageByNameView(APIView): + """按包名取包详情——官网 `venom.gvsds.com/project/<包名>/` 的数据源 + + GET /api/venom/project// + """ + + def get(self, request, package_name): + name = str(package_name or '').strip().strip('/') + package = VenomPackage.objects.annotate(release_count=Count('releases')).filter( + PackageName=name).first() + if package is None: + return _err(f'包「{name}」不存在', 404, status=404) + return _ok(_DetailRow(package)) diff --git a/GVSDServiceServer/venom_ui/src/App.vue b/GVSDServiceServer/venom_ui/src/App.vue index c8576925..bb535aed 100644 --- a/GVSDServiceServer/venom_ui/src/App.vue +++ b/GVSDServiceServer/venom_ui/src/App.vue @@ -15,6 +15,7 @@ import { Box, Clock, Collection, + Delete, Finished, Plus, Promotion, @@ -25,6 +26,7 @@ import { import { venomApi, type RemoteContext, + type VenomDependency, type VenomPackageItem, type VenomReleaseItem, } from './api' @@ -45,11 +47,40 @@ const detail = ref(null) const releases = ref([]) const dialogVisible = ref(false) -const createForm = reactive({ PackageName: '', PackageDesc: '', ConfigText: '' }) +const createForm = reactive({ + PackageName: '', + PackageDesc: '', + ConfigText: '', + Readme: '', + Dependencies: [] as VenomDependency[], +}) -const configForm = reactive({ PackageDesc: '', ConfigText: '' }) +const configForm = reactive({ + PackageDesc: '', + ConfigText: '', + Readme: '', + Dependencies: [] as VenomDependency[], +}) const releaseForm = reactive({ Version: '', Notes: '' }) +/** 空依赖行(依赖编辑器用) */ +function EmptyDependency(): VenomDependency { + return { Name: '', Version: '' } +} + +/** 提交前清洗依赖:丢弃空包名、去空白、保序去重 */ +function CleanDependencies(rows: VenomDependency[]): VenomDependency[] { + const seen = new Set() + const result: VenomDependency[] = [] + for (const row of rows) { + const name = (row.Name || '').trim() + if (!name || seen.has(name)) continue + seen.add(name) + result.push({ Name: name, Version: (row.Version || '').trim() }) + } + return result +} + /** 全部软件包的发布次数合计(列表概览用) */ const totalReleases = computed(() => packages.value.reduce((sum, p) => sum + (p.ReleaseCount || 0), 0), @@ -71,6 +102,8 @@ function openCreate(): void { createForm.PackageName = '' createForm.PackageDesc = '' createForm.ConfigText = '' + createForm.Readme = '' + createForm.Dependencies = [EmptyDependency()] dialogVisible.value = true } @@ -85,6 +118,8 @@ async function submitCreate(): Promise { PackageName: createForm.PackageName.trim(), PackageDesc: createForm.PackageDesc, ConfigText: createForm.ConfigText, + Readme: createForm.Readme, + Dependencies: CleanDependencies(createForm.Dependencies), CreatorName: operatorName.value, }) ElMessage.success('软件包已创建') @@ -109,6 +144,11 @@ async function openConfig(row: VenomPackageItem): Promise { detail.value = d configForm.PackageDesc = d.PackageDesc || '' configForm.ConfigText = d.ConfigText || '' + configForm.Readme = d.Readme || '' + configForm.Dependencies = (d.Dependencies || []).map((dep) => ({ + Name: dep.Name || '', + Version: dep.Version || '', + })) releases.value = r.list || [] releaseForm.Version = '' releaseForm.Notes = '' @@ -126,6 +166,17 @@ function backToList(): void { releases.value = [] } +/** 依赖编辑器:追加空行 */ +function addDependency(rows: VenomDependency[]): void { + rows.push(EmptyDependency()) +} + +/** 依赖编辑器:删除指定行(至少保留一行占位) */ +function removeDependency(rows: VenomDependency[], index: number): void { + rows.splice(index, 1) + if (!rows.length) rows.push(EmptyDependency()) +} + async function saveConfig(): Promise { if (!detail.value) return saving.value = true @@ -133,6 +184,8 @@ async function saveConfig(): Promise { detail.value = await venomApi.saveConfig(detail.value.PackageUUID, { PackageDesc: configForm.PackageDesc, ConfigText: configForm.ConfigText, + Readme: configForm.Readme, + Dependencies: CleanDependencies(configForm.Dependencies), }) ElMessage.success('配置已保存') await loadPackages() @@ -299,6 +352,32 @@ onMounted(loadPackages) placeholder="包配置内容(如依赖、构建参数等)" /> + +
+
+ + + +
+ + 添加依赖 + +
+
+ + + 保存配置 @@ -359,7 +438,13 @@ onMounted(loadPackages) - + @@ -370,6 +455,27 @@ onMounted(loadPackages) + +
+
+ + + +
+ + 添加依赖 + +
+
+ + +