This commit is contained in:
2026-06-24 10:04:03 +08:00
parent b103192fac
commit 0f961789dc
538 changed files with 128210 additions and 128008 deletions
+7
View File
@@ -42,10 +42,17 @@ func (c *ApiSoftwareUpgradeController) Check() {
scheme, host := services.PublicRequestBaseURL(&c.Controller) scheme, host := services.PublicRequestBaseURL(&c.Controller)
dl := services.ResolveSoftwareDownloadURL(scheme, host, row.DownloadURL, row.FileID) dl := services.ResolveSoftwareDownloadURL(scheme, host, row.DownloadURL, row.FileID)
dls := services.ResolveSoftwareDownloadURLs(scheme, host, row.DownloadURLs)
if platform := strings.ToLower(strings.TrimSpace(c.GetString("platform"))); platform != "" {
if platformURL := strings.TrimSpace(dls[platform]); platformURL != "" {
dl = platformURL
}
}
data := map[string]interface{}{ data := map[string]interface{}{
"latestVersion": latest, "latestVersion": latest,
"downloadUrl": dl, "downloadUrl": dl,
"downloadUrls": dls,
"forceUpdate": row.ForceUpdate == 1, "forceUpdate": row.ForceUpdate == 1,
"releaseNotes": "", "releaseNotes": "",
} }
+48 -22
View File
@@ -78,20 +78,23 @@ func (c *PlatformSoftwareUpgradeController) backfillDownloadURL(productID uint64
func (c *PlatformSoftwareUpgradeController) rowToMap(row *models.SystemSoftwareUpgrade) map[string]interface{} { func (c *PlatformSoftwareUpgradeController) rowToMap(row *models.SystemSoftwareUpgrade) map[string]interface{} {
scheme, host := services.PublicRequestBaseURL(&c.Controller) scheme, host := services.PublicRequestBaseURL(&c.Controller)
resolved := services.ResolveSoftwareDownloadURL(scheme, host, row.DownloadURL, row.FileID) resolved := services.ResolveSoftwareDownloadURL(scheme, host, row.DownloadURL, row.FileID)
resolvedMulti := services.ResolveSoftwareDownloadURLs(scheme, host, row.DownloadURLs)
return map[string]interface{}{ return map[string]interface{}{
"id": row.ID, "id": row.ID,
"name": row.Name, "name": row.Name,
"code": row.Code, "code": row.Code,
"latestVersion": row.LatestVersion, "latestVersion": row.LatestVersion,
"fileId": row.FileID, "fileId": row.FileID,
"downloadUrl": row.DownloadURL, "downloadUrl": row.DownloadURL,
"resolvedDownloadUrl": resolved, "downloadUrls": row.DownloadURLs,
"forceUpdate": row.ForceUpdate, "resolvedDownloadUrl": resolved,
"releaseNotes": row.ReleaseNotes, "resolvedDownloadUrls": resolvedMulti,
"status": row.Status, "forceUpdate": row.ForceUpdate,
"sort": row.Sort, "releaseNotes": row.ReleaseNotes,
"createTime": row.CreateTime, "status": row.Status,
"updateTime": row.UpdateTime, "sort": row.Sort,
"createTime": row.CreateTime,
"updateTime": row.UpdateTime,
} }
} }
@@ -158,15 +161,16 @@ func (c *PlatformSoftwareUpgradeController) Detail() {
} }
type softwareUpgradePayload struct { type softwareUpgradePayload struct {
Name *string `json:"name"` Name *string `json:"name"`
Code *string `json:"code"` Code *string `json:"code"`
LatestVersion *string `json:"latestVersion"` LatestVersion *string `json:"latestVersion"`
FileID *uint64 `json:"fileId"` FileID *uint64 `json:"fileId"`
DownloadURL *string `json:"downloadUrl"` DownloadURL *string `json:"downloadUrl"`
ForceUpdate *int8 `json:"forceUpdate"` DownloadURLs map[string]string `json:"downloadUrls"`
ReleaseNotes *string `json:"releaseNotes"` ForceUpdate *int8 `json:"forceUpdate"`
Status *int8 `json:"status"` ReleaseNotes *string `json:"releaseNotes"`
Sort *int `json:"sort"` Status *int8 `json:"status"`
Sort *int `json:"sort"`
} }
// Create POST /platform/softwareupgrade // Create POST /platform/softwareupgrade
@@ -198,6 +202,12 @@ func (c *PlatformSoftwareUpgradeController) Create() {
Status: 1, Status: 1,
Sort: 0, Sort: 0,
} }
if len(p.DownloadURLs) > 0 {
if b, err := json.Marshal(p.DownloadURLs); err == nil {
s := string(b)
row.DownloadURLs = &s
}
}
if p.ForceUpdate != nil { if p.ForceUpdate != nil {
row.ForceUpdate = *p.ForceUpdate row.ForceUpdate = *p.ForceUpdate
} }
@@ -263,6 +273,22 @@ func (c *PlatformSoftwareUpgradeController) Update() {
if p.DownloadURL != nil { if p.DownloadURL != nil {
up["download_url"] = strings.TrimSpace(*p.DownloadURL) up["download_url"] = strings.TrimSpace(*p.DownloadURL)
} }
if p.DownloadURLs != nil {
clean := map[string]string{}
for k, v := range p.DownloadURLs {
platform := strings.ToLower(strings.TrimSpace(k))
url := strings.TrimSpace(v)
if platform == "" || url == "" {
continue
}
clean[platform] = url
}
if len(clean) == 0 {
up["download_urls"] = nil
} else if b, err := json.Marshal(clean); err == nil {
up["download_urls"] = string(b)
}
}
if p.ForceUpdate != nil { if p.ForceUpdate != nil {
up["force_update"] = *p.ForceUpdate up["force_update"] = *p.ForceUpdate
} }
+6 -1
View File
@@ -7,7 +7,8 @@ CREATE TABLE IF NOT EXISTS `yz_system_software_upgrade` (
`code` varchar(64) NOT NULL COMMENT '客户端唯一标识,与 check 接口 code 一致', `code` varchar(64) NOT NULL COMMENT '客户端唯一标识,与 check 接口 code 一致',
`latest_version` varchar(32) NOT NULL DEFAULT '0.0.0' COMMENT '当前发布的最新版本号', `latest_version` varchar(32) NOT NULL DEFAULT '0.0.0' COMMENT '当前发布的最新版本号',
`file_id` bigint unsigned DEFAULT NULL COMMENT '关联 yz_system_files.id,安装包', `file_id` bigint unsigned DEFAULT NULL COMMENT '关联 yz_system_files.id,安装包',
`download_url` varchar(512) DEFAULT NULL COMMENT '完整下载地址;为空则用 file_id 对应 src 拼公开 URL', `download_url` varchar(512) DEFAULT NULL COMMENT '兼容旧客户端的单安装包地址;为空则用 file_id 对应 src 拼公开 URL',
`download_urls` text DEFAULT NULL COMMENT '多运行环境安装包地址 JSON,如 {"windows":"...","mac":"...","ubuntu":"...","linux":"..."}',
`force_update` tinyint NOT NULL DEFAULT 0 COMMENT '1 建议强制更新', `force_update` tinyint NOT NULL DEFAULT 0 COMMENT '1 建议强制更新',
`release_notes` varchar(2000) DEFAULT NULL COMMENT '更新说明', `release_notes` varchar(2000) DEFAULT NULL COMMENT '更新说明',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1 启用 0 停用', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 启用 0 停用',
@@ -19,3 +20,7 @@ CREATE TABLE IF NOT EXISTS `yz_system_software_upgrade` (
UNIQUE KEY `uk_code` (`code`), UNIQUE KEY `uk_code` (`code`),
KEY `idx_status_delete` (`status`,`delete_time`) KEY `idx_status_delete` (`status`,`delete_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='软件升级产品'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='软件升级产品';
-- 已有表升级:
-- ALTER TABLE `yz_system_software_upgrade`
-- ADD COLUMN `download_urls` text DEFAULT NULL COMMENT '多运行环境安装包地址 JSON,如 {"windows":"...","mac":"...","ubuntu":"...","linux":"..."}' AFTER `download_url`;
+14 -13
View File
@@ -4,19 +4,20 @@ import "time"
// SystemSoftwareUpgrade 软件升级产品 yz_system_software_upgrade // SystemSoftwareUpgrade 软件升级产品 yz_system_software_upgrade
type SystemSoftwareUpgrade struct { type SystemSoftwareUpgrade struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"` ID uint64 `orm:"column(id);pk;auto" json:"id"`
Name string `orm:"column(name);size(128)" json:"name"` Name string `orm:"column(name);size(128)" json:"name"`
Code string `orm:"column(code);size(64);unique" json:"code"` Code string `orm:"column(code);size(64);unique" json:"code"`
LatestVersion string `orm:"column(latest_version);size(32)" json:"latestVersion"` LatestVersion string `orm:"column(latest_version);size(32)" json:"latestVersion"`
FileID *uint64 `orm:"column(file_id);null" json:"fileId"` FileID *uint64 `orm:"column(file_id);null" json:"fileId"`
DownloadURL *string `orm:"column(download_url);size(512);null" json:"downloadUrl"` DownloadURL *string `orm:"column(download_url);size(512);null" json:"downloadUrl"`
ForceUpdate int8 `orm:"column(force_update);default(0)" json:"forceUpdate"` DownloadURLs *string `orm:"column(download_urls);type(text);null" json:"downloadUrls"`
ReleaseNotes *string `orm:"column(release_notes);size(2000);null" json:"releaseNotes"` ForceUpdate int8 `orm:"column(force_update);default(0)" json:"forceUpdate"`
Status int8 `orm:"column(status);default(1)" json:"status"` ReleaseNotes *string `orm:"column(release_notes);size(2000);null" json:"releaseNotes"`
Sort int `orm:"column(sort);default(0)" json:"sort"` Status int8 `orm:"column(status);default(1)" json:"status"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"` Sort int `orm:"column(sort);default(0)" json:"sort"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"updateTime"` CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"createTime"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"deleteTime"` UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"updateTime"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"deleteTime"`
} }
func (m *SystemSoftwareUpgrade) TableName() string { func (m *SystemSoftwareUpgrade) TableName() string {
+45 -17
View File
@@ -1,6 +1,7 @@
package services package services
import ( import (
"encoding/json"
"strings" "strings"
"server/models" "server/models"
@@ -21,6 +22,49 @@ func PublicRequestBaseURL(c *beego.Controller) (scheme, host string) {
return scheme, host return scheme, host
} }
// ResolveSoftwareDownloadURL 优先使用自定义 download_url;否则根据 file_id 读附件 src 拼完整 URL
func ResolveSoftwareDownloadURLs(scheme, host string, downloadURLs *string) map[string]string {
result := map[string]string{}
if downloadURLs == nil || strings.TrimSpace(*downloadURLs) == "" {
return result
}
raw := strings.TrimSpace(*downloadURLs)
var values map[string]string
if err := json.Unmarshal([]byte(raw), &values); err != nil {
return result
}
for k, v := range values {
platform := strings.ToLower(strings.TrimSpace(k))
url := strings.TrimSpace(v)
if platform == "" || url == "" {
continue
}
result[platform] = ResolvePublicURL(scheme, host, url)
}
return result
}
// ResolvePublicURL 把相对路径拼成公开 URL;http(s) 地址原样返回。
func ResolvePublicURL(scheme, host, src string) string {
src = strings.TrimSpace(src)
if src == "" {
return ""
}
if strings.HasPrefix(strings.ToLower(src), "http://") || strings.HasPrefix(strings.ToLower(src), "https://") {
return src
}
if host == "" {
return src
}
if !strings.HasPrefix(src, "/") {
src = "/" + src
}
if scheme == "" {
scheme = "http"
}
return scheme + "://" + host + src
}
// ResolveSoftwareDownloadURL 优先使用自定义 download_url;否则根据 file_id 读附件 src 拼完整 URL // ResolveSoftwareDownloadURL 优先使用自定义 download_url;否则根据 file_id 读附件 src 拼完整 URL
func ResolveSoftwareDownloadURL(scheme, host string, downloadURL *string, fileID *uint64) string { func ResolveSoftwareDownloadURL(scheme, host string, downloadURL *string, fileID *uint64) string {
if downloadURL != nil { if downloadURL != nil {
@@ -40,21 +84,5 @@ func ResolveSoftwareDownloadURL(scheme, host string, downloadURL *string, fileID
if err != nil { if err != nil {
return "" return ""
} }
src := strings.TrimSpace(f.Src) return ResolvePublicURL(scheme, host, f.Src)
if src == "" {
return ""
}
if strings.HasPrefix(strings.ToLower(src), "http://") || strings.HasPrefix(strings.ToLower(src), "https://") {
return src
}
if host == "" {
return src
}
if !strings.HasPrefix(src, "/") {
src = "/" + src
}
if scheme == "" {
scheme = "http"
}
return scheme + "://" + host + src
} }
+86 -58
View File
@@ -597,13 +597,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@intlify/core-base": { "node_modules/@intlify/core-base": {
"version": "9.14.4", "version": "9.14.5",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.14.4.tgz", "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.5.tgz",
"integrity": "sha512-vtZCt7NqWhKEtHa3SD/322DlgP5uR9MqWxnE0y8Q0tjDs9H5Lxhss+b5wv8rmuXRoHKLESNgw9d+EN9ybBbj9g==", "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/message-compiler": "9.14.4", "@intlify/message-compiler": "9.14.5",
"@intlify/shared": "9.14.4" "@intlify/shared": "9.14.5"
}, },
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@@ -613,12 +613,12 @@
} }
}, },
"node_modules/@intlify/message-compiler": { "node_modules/@intlify/message-compiler": {
"version": "9.14.4", "version": "9.14.5",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.4.tgz", "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.5.tgz",
"integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==", "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/shared": "9.14.4", "@intlify/shared": "9.14.5",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
"engines": { "engines": {
@@ -629,9 +629,9 @@
} }
}, },
"node_modules/@intlify/shared": { "node_modules/@intlify/shared": {
"version": "9.14.4", "version": "9.14.5",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.4.tgz", "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.5.tgz",
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==", "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 16" "node": ">= 16"
@@ -1589,6 +1589,18 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/array-buffer-byte-length": { "node_modules/array-buffer-byte-length": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
@@ -1663,14 +1675,15 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.5", "version": "1.18.1",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.16.0",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
} }
}, },
"node_modules/birpc": { "node_modules/birpc": {
@@ -1943,7 +1956,6 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@@ -2387,9 +2399,9 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.16.0",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -2422,16 +2434,16 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.5", "version": "4.0.6",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2", "hasown": "^2.0.4",
"mime-types": "^2.1.12" "mime-types": "^2.1.35"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
@@ -2676,9 +2688,9 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.4",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -2712,6 +2724,19 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/i18next": { "node_modules/i18next": {
"version": "20.6.1", "version": "20.6.1",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz", "resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
@@ -3212,15 +3237,15 @@
} }
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.23", "version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
"version": "4.17.23", "version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash-unified": { "node_modules/lodash-unified": {
@@ -3421,7 +3446,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/namespace-emitter": { "node_modules/namespace-emitter": {
@@ -3431,9 +3455,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.15",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -3576,9 +3600,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3641,9 +3665,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.15",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -3660,7 +3684,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.12",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -3694,10 +3718,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "2.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT" "license": "MIT",
"engines": {
"node": ">=10"
}
}, },
"node_modules/prr": { "node_modules/prr": {
"version": "1.0.1", "version": "1.0.1",
@@ -4646,9 +4673,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.1", "version": "7.3.5",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4742,13 +4769,14 @@
} }
}, },
"node_modules/vue-i18n": { "node_modules/vue-i18n": {
"version": "9.14.4", "version": "9.14.5",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.4.tgz", "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.5.tgz",
"integrity": "sha512-B934C8yUyWLT0EMud3DySrwSUJI7ZNiWYsEEz2gknTthqKiG4dzWE/WSa8AzCuSQzwBEv4HtG1jZDhgzPfWSKQ==", "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==",
"deprecated": "v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@intlify/core-base": "9.14.4", "@intlify/core-base": "9.14.5",
"@intlify/shared": "9.14.4", "@intlify/shared": "9.14.5",
"@vue/devtools-api": "^6.5.0" "@vue/devtools-api": "^6.5.0"
}, },
"engines": { "engines": {
@@ -21,63 +21,75 @@
<el-input v-model="form.latestVersion" placeholder="如 1.2.0" /> <el-input v-model="form.latestVersion" placeholder="如 1.2.0" />
</el-form-item> </el-form-item>
<el-form-item label="安装包"> <el-form-item label="多平台安装包">
<el-upload <div class="platform-package-list">
ref="uploadRef" <p class="hint">
class="pkg-upload" 同一个产品 code 可配置多个运行环境安装包客户端按自身系统读取响应中的
:limit="1" <code>downloadUrls[platform]</code>
:file-list="displayFileList" </p>
:http-request="handlePackageUpload" <div v-for="item in platformOptions" :key="item.value" class="platform-package-row">
:on-remove="onRemovePackage" <div class="platform-label">
@exceed="onExceedReplace" <el-tag effect="plain">{{ item.label }}</el-tag>
accept=".zip,.exe,.dmg,.msi,.msix,.apk,.deb,.rpm,.7z,.pkg,.tar,.gz" </div>
> <el-input
<el-button type="primary" :loading="uploading"> v-model="form.downloadUrls[item.value]"
{{ form.fileId ? "重新上传安装包" : "上传安装包" }} clearable
</el-button> :placeholder="`填写 ${item.label} 下载地址,或上传后自动填写`"
</el-upload> />
<div v-if="uploadXHRActive" class="upload-progress-wrap"> <el-upload
<el-progress :show-file-list="false"
:percentage="uploadPercent" :http-request="(options) => handlePackageUpload(options, item.value)"
:indeterminate="uploadIndeterminate" accept=".zip,.exe,.dmg,.msi,.msix,.apk,.deb,.rpm,.7z,.pkg,.tar,.gz,.appimage,.AppImage"
:stroke-width="10" >
striped <el-button
striped-flow type="primary"
/> plain
<div class="upload-progress-text"> :loading="uploading && uploadPlatform === item.value"
<template v-if="!uploadIndeterminate"> >
<span>{{ uploadPercent }}%</span> 上传
<span class="upload-sep">·</span> </el-button>
<span>{{ uploadSpeedText }}</span> </el-upload>
<template v-if="uploadSizeText"> </div>
<span class="upload-sep">·</span> <div v-if="uploadXHRActive" class="upload-progress-wrap">
<span>{{ uploadSizeText }}</span> <el-progress
</template> :percentage="uploadPercent"
</template> :indeterminate="uploadIndeterminate"
<template v-else> :stroke-width="10"
<span>上传中</span> striped
<template v-if="uploadSpeedText && uploadSpeedText !== '—'"> striped-flow
/>
<div class="upload-progress-text">
<template v-if="!uploadIndeterminate">
<span>{{ uploadPercent }}%</span>
<span class="upload-sep">·</span> <span class="upload-sep">·</span>
<span>{{ uploadSpeedText }}</span> <span>{{ uploadSpeedText }}</span>
<template v-if="uploadSizeText">
<span class="upload-sep">·</span>
<span>{{ uploadSizeText }}</span>
</template>
</template> </template>
<template v-if="uploadSizeText"> <template v-else>
<span class="upload-sep">·</span> <span>上传中</span>
<span>{{ uploadSizeText }}</span> <template v-if="uploadSpeedText && uploadSpeedText !== '—'">
<span class="upload-sep">·</span>
<span>{{ uploadSpeedText }}</span>
</template>
<template v-if="uploadSizeText">
<span class="upload-sep">·</span>
<span>{{ uploadSizeText }}</span>
</template>
</template> </template>
</template> </div>
</div> </div>
</div> </div>
<div v-if="form.fileId" class="pkg-meta">
<span>文件 ID<code>{{ form.fileId }}</code></span>
</div>
</el-form-item> </el-form-item>
<el-form-item label="下载/更新地址"> <el-form-item label="兼容下载地址">
<el-input <el-input
v-model="form.downloadUrl" v-model="form.downloadUrl"
type="textarea" type="textarea"
:rows="2" :rows="2"
placeholder="上传后自动填写;可改为 CDN 等完整 URL" placeholder="旧版客户端兼容字段 downloadUrl;新客户端建议使用 downloadUrls"
/> />
</el-form-item> </el-form-item>
@@ -96,7 +108,7 @@
<el-form-item label="排序"> <el-form-item label="排序">
<el-input-number v-model="form.sort" :min="0" style="width: 100%" /> <el-input-number v-model="form.sort" :min="0" style="width: 100%" />
</el-form-item> </el-form-item>
<el-form-item v-if="!isAdd && resolvedUrl" label="当前解析地址"> <el-form-item v-if="!isAdd && resolvedUrl" label="兼容解析地址">
<el-link :href="resolvedUrl" type="primary" target="_blank">{{ resolvedUrl }}</el-link> <el-link :href="resolvedUrl" type="primary" target="_blank">{{ resolvedUrl }}</el-link>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -125,6 +137,7 @@ const loading = ref(false);
const saving = ref(false); const saving = ref(false);
const uploading = ref(false); const uploading = ref(false);
const uploadXHRActive = ref(false); const uploadXHRActive = ref(false);
const uploadPlatform = ref("");
const uploadPercent = ref(0); const uploadPercent = ref(0);
const uploadIndeterminate = ref(false); const uploadIndeterminate = ref(false);
const uploadSpeedText = ref("—"); const uploadSpeedText = ref("—");
@@ -136,6 +149,12 @@ const uploadRef = ref(null);
const displayFileList = ref([]); const displayFileList = ref([]);
const uploadedLabel = ref(""); const uploadedLabel = ref("");
const appsUpgradeCateId = ref(null); const appsUpgradeCateId = ref(null);
const platformOptions = [
{ label: "Windows", value: "windows" },
{ label: "macOS", value: "mac" },
{ label: "Ubuntu", value: "ubuntu" },
{ label: "Linux", value: "linux" },
];
const form = reactive({ const form = reactive({
id: 0, id: 0,
@@ -144,6 +163,7 @@ const form = reactive({
latestVersion: "0.0.1", latestVersion: "0.0.1",
fileId: null, fileId: null,
downloadUrl: "", downloadUrl: "",
downloadUrls: {},
forceUpdate: 0, forceUpdate: 0,
releaseNotes: "", releaseNotes: "",
status: 1, status: 1,
@@ -203,6 +223,21 @@ async function ensureAppsUpgradeCate() {
return null; return null;
} }
function normalizeDownloadUrls(value) {
if (!value) return {};
if (typeof value === "string") {
try {
return JSON.parse(value) || {};
} catch {
return {};
}
}
if (typeof value === "object") {
return { ...value };
}
return {};
}
function reset() { function reset() {
form.id = 0; form.id = 0;
form.name = ""; form.name = "";
@@ -210,6 +245,7 @@ function reset() {
form.latestVersion = "0.0.1"; form.latestVersion = "0.0.1";
form.fileId = null; form.fileId = null;
form.downloadUrl = ""; form.downloadUrl = "";
form.downloadUrls = {};
form.forceUpdate = 0; form.forceUpdate = 0;
form.releaseNotes = ""; form.releaseNotes = "";
form.status = 1; form.status = 1;
@@ -218,6 +254,7 @@ function reset() {
displayFileList.value = []; displayFileList.value = [];
uploadedLabel.value = ""; uploadedLabel.value = "";
uploadXHRActive.value = false; uploadXHRActive.value = false;
uploadPlatform.value = "";
uploadPercent.value = 0; uploadPercent.value = 0;
uploadIndeterminate.value = false; uploadIndeterminate.value = false;
uploadSpeedText.value = "—"; uploadSpeedText.value = "—";
@@ -250,13 +287,14 @@ function onExceedReplace(files) {
} }
} }
async function handlePackageUpload(options) { async function handlePackageUpload(options, platform = "") {
const file = options.file?.raw != null ? options.file.raw : options.file; const file = options.file?.raw != null ? options.file.raw : options.file;
if (!file) { if (!file) {
options.onError?.(new Error("无文件")); options.onError?.(new Error("无文件"));
return; return;
} }
uploading.value = true; uploading.value = true;
uploadPlatform.value = platform;
try { try {
const cateId = await ensureAppsUpgradeCate(); const cateId = await ensureAppsUpgradeCate();
if (!cateId) { if (!cateId) {
@@ -307,10 +345,14 @@ async function handlePackageUpload(options) {
}); });
// //
form.fileId = result.id; if (platform) {
form.downloadUrl = result.url; form.downloadUrls[platform] = result.url;
uploadedLabel.value = result.name || file.name || "安装包"; } else {
displayFileList.value = [{ name: uploadedLabel.value, uid: `pkg-${form.fileId}-${Date.now()}` }]; form.fileId = result.id;
form.downloadUrl = result.url;
uploadedLabel.value = result.name || file.name || "安装包";
displayFileList.value = [{ name: uploadedLabel.value, uid: `pkg-${form.fileId}-${Date.now()}` }];
}
ElMessage.success("上传成功"); ElMessage.success("上传成功");
options.onSuccess?.(result); options.onSuccess?.(result);
} catch (e) { } catch (e) {
@@ -318,6 +360,7 @@ async function handlePackageUpload(options) {
options.onError?.(e); options.onError?.(e);
} finally { } finally {
uploading.value = false; uploading.value = false;
uploadPlatform.value = "";
uploadXHRActive.value = false; uploadXHRActive.value = false;
uploadPercent.value = 0; uploadPercent.value = 0;
uploadIndeterminate.value = false; uploadIndeterminate.value = false;
@@ -349,6 +392,7 @@ async function open(id) {
form.latestVersion = d.latestVersion || "0.0.1"; form.latestVersion = d.latestVersion || "0.0.1";
form.fileId = d.fileId != null ? Number(d.fileId) : null; form.fileId = d.fileId != null ? Number(d.fileId) : null;
form.downloadUrl = d.downloadUrl || d.resolvedDownloadUrl || ""; form.downloadUrl = d.downloadUrl || d.resolvedDownloadUrl || "";
form.downloadUrls = normalizeDownloadUrls(d.downloadUrls || d.resolvedDownloadUrls);
form.forceUpdate = d.forceUpdate ? 1 : 0; form.forceUpdate = d.forceUpdate ? 1 : 0;
form.releaseNotes = d.releaseNotes || ""; form.releaseNotes = d.releaseNotes || "";
form.status = d.status !== 0 ? 1 : 0; form.status = d.status !== 0 ? 1 : 0;
@@ -381,11 +425,21 @@ async function submit() {
} catch { } catch {
return; return;
} }
const cleanDownloadUrls = {};
Object.entries(form.downloadUrls || {}).forEach(([platform, url]) => {
const key = String(platform || "").trim().toLowerCase();
const value = String(url || "").trim();
if (key && value) {
cleanDownloadUrls[key] = value;
}
});
const payload = { const payload = {
name: form.name.trim(), name: form.name.trim(),
code: form.code.trim(), code: form.code.trim(),
latestVersion: form.latestVersion.trim(), latestVersion: form.latestVersion.trim(),
downloadUrl: form.downloadUrl.trim() || undefined, downloadUrl: form.downloadUrl.trim() || undefined,
downloadUrls: cleanDownloadUrls,
forceUpdate: form.forceUpdate, forceUpdate: form.forceUpdate,
releaseNotes: form.releaseNotes.trim() || undefined, releaseNotes: form.releaseNotes.trim() || undefined,
status: form.status, status: form.status,
@@ -432,6 +486,20 @@ defineExpose({ open });
background: var(--el-fill-color-light); background: var(--el-fill-color-light);
border-radius: 4px; border-radius: 4px;
} }
.platform-package-list {
width: 100%;
}
.platform-package-row {
display: grid;
grid-template-columns: 86px 1fr auto;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
.platform-label {
display: flex;
align-items: center;
}
.pkg-upload { .pkg-upload {
width: 100%; width: 100%;
} }
@@ -41,10 +41,24 @@
<el-table-column prop="name" label="软件名称" min-width="140" /> <el-table-column prop="name" label="软件名称" min-width="140" />
<el-table-column prop="code" label="产品code" width="140" /> <el-table-column prop="code" label="产品code" width="140" />
<el-table-column prop="latestVersion" label="最新版本" width="100" align="center" /> <el-table-column prop="latestVersion" label="最新版本" width="100" align="center" />
<el-table-column label="文件 ID" width="88" align="center"> <el-table-column label="平台包" min-width="260">
<template #default="{ row }">{{ row.fileId ?? "—" }}</template> <template #default="{ row }">
<div v-if="Object.keys(normalizeDownloadUrls(row.resolvedDownloadUrls || row.downloadUrls)).length" class="platform-url-list">
<el-tag
v-for="(url, platform) in normalizeDownloadUrls(row.resolvedDownloadUrls || row.downloadUrls)"
:key="platform"
effect="plain"
class="platform-url-tag"
>
<el-link :href="url" type="primary" target="_blank" :underline="false">
{{ platform }}
</el-link>
</el-tag>
</div>
<span v-else class="muted"></span>
</template>
</el-table-column> </el-table-column>
<el-table-column label="解析下载地址" min-width="200" show-overflow-tooltip> <el-table-column label="兼容下载地址" min-width="200" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<el-link v-if="row.resolvedDownloadUrl" :href="row.resolvedDownloadUrl" type="primary" target="_blank"> <el-link v-if="row.resolvedDownloadUrl" :href="row.resolvedDownloadUrl" type="primary" target="_blank">
{{ row.resolvedDownloadUrl }} {{ row.resolvedDownloadUrl }}
@@ -98,12 +112,14 @@
<ul> <ul>
<li><code>code</code>必填与后台配置的产品标识一致</li> <li><code>code</code>必填与后台配置的产品标识一致</li>
<li><code>version</code>可选当前客户端版本缺省按 <code>0.0.0</code></li> <li><code>version</code>可选当前客户端版本缺省按 <code>0.0.0</code></li>
<li><code>platform</code>可选客户端运行环境 <code>windows</code><code>mac</code><code>ubuntu</code><code>linux</code>客户端也可以自行从 <code>downloadUrls</code> 取对应平台 URL</li>
</ul> </ul>
<p>响应 <code>data</code> 字段说明</p> <p>响应 <code>data</code> 字段说明</p>
<ul> <ul>
<li><code>upToDate</code>是否已是最新当前版本 后台最新版本</li> <li><code>upToDate</code>是否已是最新当前版本 后台最新版本</li>
<li><code>latestVersion</code>后台配置的最新版本号</li> <li><code>latestVersion</code>后台配置的最新版本号</li>
<li><code>downloadUrl</code>安装包地址自定义 URL 或由文件 ID 解析</li> <li><code>downloadUrl</code>兼容旧客户端的单安装包地址自定义 URL 或由文件 ID 解析</li>
<li><code>downloadUrls</code>多平台安装包地址 <code>windows</code><code>mac</code><code>ubuntu</code><code>linux</code></li>
<li><code>forceUpdate</code>是否建议强制更新</li> <li><code>forceUpdate</code>是否建议强制更新</li>
<li><code>releaseNotes</code>更新说明</li> <li><code>releaseNotes</code>更新说明</li>
</ul> </ul>
@@ -147,6 +163,21 @@ const list = ref([]);
const searchForm = reactive({ keyword: "" }); const searchForm = reactive({ keyword: "" });
const pagination = reactive({ page: 1, pageSize: 20, total: 0 }); const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
function normalizeDownloadUrls(value) {
if (!value) return {};
if (typeof value === "string") {
try {
return JSON.parse(value) || {};
} catch {
return {};
}
}
if (typeof value === "object") {
return value;
}
return {};
}
async function fetchList() { async function fetchList() {
loading.value = true; loading.value = true;
try { try {
@@ -222,6 +253,14 @@ onMounted(() => {
.muted { .muted {
color: var(--el-text-color-placeholder); color: var(--el-text-color-placeholder);
} }
.platform-url-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.platform-url-tag {
margin: 0;
}
.api-doc { .api-doc {
padding-right: 12px; padding-right: 12px;
font-size: 13px; font-size: 13px;