feat: Add historical release notes display (#10002)

Refs #9662
This commit is contained in:
ssongliu 2025-08-14 22:34:54 +08:00 committed by GitHub
parent 6b81ae1b7b
commit 6d4d53e2c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 246 additions and 9 deletions

View file

@ -21,6 +21,21 @@ func (b *BaseApi) GetUpgradeInfo(c *gin.Context) {
helper.SuccessWithData(c, info)
}
// @Tags System Setting
// @Summary Load upgrade notes
// @Success 200 {array} dto.ReleasesNotes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/settings/upgrade/releases [get]
func (b *BaseApi) LoadRelease(c *gin.Context) {
notes, err := upgradeService.LoadRelease()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, notes)
}
// @Tags System Setting
// @Summary Load release notes by version
// @Accept json
@ -28,7 +43,7 @@ func (b *BaseApi) GetUpgradeInfo(c *gin.Context) {
// @Success 200 {string} notes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /core/settings/upgrade [get]
// @Router /core/settings/upgrade/notes [post]
func (b *BaseApi) GetNotesByVersion(c *gin.Context) {
var req dto.Upgrade
if err := helper.CheckBindAndValidate(&req, c); err != nil {

View file

@ -145,6 +145,15 @@ type Upgrade struct {
Version string `json:"version" validate:"required"`
}
type ReleasesNotes struct {
Version string `json:"version"`
CreatedAt string `json:"createdAt"`
Content string `json:"content"`
NewCount int `json:"newCount"`
OptimizationCount int `json:"optimizationCount"`
FixCount int `json:"fixCount"`
}
type ProxyUpdate struct {
ProxyUrl string `json:"proxyUrl"`
ProxyType string `json:"proxyType"`

View file

@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path"
@ -29,6 +30,7 @@ type IUpgradeService interface {
Rollback(req dto.OperateByID) error
LoadNotes(req dto.Upgrade) (string, error)
SearchUpgrade() (*dto.UpgradeInfo, error)
LoadRelease() ([]dto.ReleasesNotes, error)
}
func NewIUpgradeService() IUpgradeService {
@ -208,6 +210,66 @@ func (u *UpgradeService) Rollback(req dto.OperateByID) error {
return nil
}
type noteHelper struct {
Docs []noteDetailHelper `json:"docs"`
}
type noteDetailHelper struct {
Location string `json:"location"`
Text string `json:"text"`
Title string `json:"title"`
}
func (u *UpgradeService) LoadRelease() ([]dto.ReleasesNotes, error) {
var notes []dto.ReleasesNotes
resp, err := req_helper.HandleGet("https://1panel.cn/docs/v2/search/search_index.json")
if err != nil {
return notes, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return notes, err
}
defer resp.Body.Close()
var nodeItem noteHelper
if err := json.Unmarshal(body, &nodeItem); err != nil {
return notes, err
}
for _, item := range nodeItem.Docs {
if !strings.HasPrefix(item.Location, "changelog/#v") {
continue
}
itemNote := analyzeDoc(item.Title, item.Text)
if len(itemNote.CreatedAt) != 0 {
notes = append(notes, analyzeDoc(item.Title, item.Text))
}
}
return notes, nil
}
func analyzeDoc(version, content string) dto.ReleasesNotes {
var item dto.ReleasesNotes
parts := strings.Split(content, "<p>")
if len(parts) < 3 {
return item
}
item.CreatedAt = strings.ReplaceAll(strings.TrimSpace(parts[1]), "</p>", "")
for i := 1; i < len(parts); i++ {
if strings.Contains(parts[i], "问题修复") {
item.FixCount = strings.Count(parts[i], "<li>")
}
if strings.Contains(parts[i], "新增功能") {
item.NewCount = strings.Count(parts[i], "<li>")
}
if strings.Contains(parts[i], "功能优化") {
item.OptimizationCount = strings.Count(parts[i], "<li>")
}
}
item.Content = strings.Replace(content, fmt.Sprintf("<p>%s</p>", item.CreatedAt), "", 1)
item.Version = version
return item
}
func (u *UpgradeService) handleBackup(originalDir string) error {
if err := files.CopyItem(false, true, "/usr/local/bin/1panel-core", originalDir); err != nil {
return err

View file

@ -38,6 +38,7 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
settingRouter.POST("/upgrade", baseApi.Upgrade)
settingRouter.POST("/upgrade/notes", baseApi.GetNotesByVersion)
settingRouter.GET("/upgrade/releases", baseApi.LoadRelease)
settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo)
settingRouter.POST("/api/config/generate/key", baseApi.GenerateApiKey)
settingRouter.POST("/api/config/update", baseApi.UpdateApiConfig)

View file

@ -241,6 +241,14 @@ export namespace Setting {
isBound: boolean;
name: string;
}
export interface ReleasesNotes {
Version: string;
CreatedAt: string;
Content: string;
NewCount: number;
OptimizationCount: number;
FixCount: number;
}
export interface LicenseBind {
nodeID: number;

View file

@ -170,6 +170,9 @@ export const loadUpgradeInfo = () => {
export const loadReleaseNotes = (version: string) => {
return http.post<string>(`/core/settings/upgrade/notes`, { version: version });
};
export const listReleases = () => {
return http.get<Array<Setting.ReleasesNotes>>(`/core/settings/upgrade/releases`);
};
export const upgrade = (version: string) => {
return http.post(`/core/settings/upgrade`, { version: version });
};

View file

@ -1,9 +1,9 @@
@font-face {
font-family: "iconfont"; /* Project id 4776196 */
src: url('iconfont.woff2?t=1752473267421') format('woff2'),
url('iconfont.woff?t=1752473267421') format('woff'),
url('iconfont.ttf?t=1752473267421') format('truetype'),
url('iconfont.svg?t=1752473267421#iconfont') format('svg');
src: url('iconfont.woff2?t=1755181498446') format('woff2'),
url('iconfont.woff?t=1755181498446') format('woff'),
url('iconfont.ttf?t=1755181498446') format('truetype'),
url('iconfont.svg?t=1755181498446#iconfont') format('svg');
}
.iconfont {
@ -14,6 +14,14 @@
-moz-osx-font-smoothing: grayscale;
}
.p-featureshitu:before {
content: "\e63e";
}
.p-youhuawendang:before {
content: "\e7c4";
}
.p-cluster-3:before {
content: "\e706";
}

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,20 @@
"css_prefix_text": "p-",
"description": "",
"glyphs": [
{
"icon_id": "18536446",
"name": "feature视图",
"font_class": "featureshitu",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "18133027",
"name": "优化文档",
"font_class": "youhuawendang",
"unicode": "e7c4",
"unicode_decimal": 59332
},
{
"icon_id": "88609",
"name": "cluster-3",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View file

@ -20,7 +20,7 @@
<el-link underline="never" type="primary" @click="toLxware">
{{ $t(!isMasterPro ? 'license.community' : 'license.pro') }}
</el-link>
<el-link underline="never" class="version" type="primary" @click="copyText(version)">
<el-link underline="never" class="version" type="primary" @click="releasesRef.acceptParams()">
{{ version }}
</el-link>
<el-badge is-dot class="-mt-0.5" :hidden="version === 'Waiting' || !globalStore.hasNewVersion">
@ -36,15 +36,16 @@
</div>
<Upgrade ref="upgradeRef" @search="search" />
<Releases ref="releasesRef" />
</div>
</template>
<script setup lang="ts">
import { getSettingInfo, loadUpgradeInfo } from '@/api/modules/setting';
import Upgrade from '@/components/system-upgrade/upgrade/index.vue';
import Releases from '@/components/system-upgrade/releases/index.vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { copyText } from '@/utils/util';
import { onMounted, ref } from 'vue';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
@ -52,6 +53,7 @@ import { storeToRefs } from 'pinia';
const globalStore = GlobalStore();
const { docsUrl } = storeToRefs(globalStore);
const upgradeRef = ref();
const releasesRef = ref();
const isMasterPro = computed(() => {
return globalStore.isMasterPro();
});

View file

@ -0,0 +1,104 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('app.version')" @close="handleClose" size="large">
<div class="note">
<el-collapse v-model="currentVersion" :accordion="true" v-loading="loading">
<div v-for="(item, index) in notes" :key="index">
<el-collapse-item :name="index">
<template #title>
<div>
<span class="version">{{ item.version }}</span>
<span class="date">{{ item.createdAt }}</span>
</div>
<svg-icon class="icon" iconName="p-featureshitu"></svg-icon>
<span class="icon-span">{{ item.newCount }}</span>
<svg-icon class="icon" iconName="p-youhuawendang"></svg-icon>
<span class="icon-span">{{ item.optimizationCount }}</span>
<svg-icon class="icon" iconName="p-bug"></svg-icon>
<span class="icon-span">{{ item.fixCount }}</span>
</template>
<div class="panel-MdEditor">
<MdEditor v-model="item.content" previewOnly :theme="isDarkTheme ? 'dark' : 'light'" />
</div>
</el-collapse-item>
</div>
</el-collapse>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</DrawerPro>
</template>
<script setup lang="ts">
import { listReleases } from '@/api/modules/setting';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import { ref } from 'vue';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);
const drawerVisible = ref(false);
const currentVersion = ref(0);
const notes = ref([]);
const loading = ref();
const acceptParams = (): void => {
search();
drawerVisible.value = true;
};
const handleClose = () => {
drawerVisible.value = false;
};
const search = async () => {
loading.value = true;
await listReleases()
.then((res) => {
notes.value = res.data;
loading.value = false;
})
.catch(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>
<style lang="scss" scoped>
.version {
margin-left: 10px;
display: inline-block;
width: 50px;
}
.date {
margin-left: 20px;
margin-right: 40px;
display: inline-block;
width: 100px;
}
.icon-span {
display: inline-block;
width: 10px;
}
.panel-MdEditor {
:deep(.md-editor-preview) {
font-size: 12px;
}
}
:deep(.md-editor-dark) {
background-color: var(--panel-main-bg-color-9);
}
.icon {
font-size: 7px;
margin-left: 50px;
}
</style>

View file

@ -462,7 +462,8 @@ html.dark {
.el-collapse-item__header {
color: #ffffff;
background-color: var(--panel-main-bg-color-10) !important;
border: 1px solid var(--panel-main-bg-color-10);
background-color: var(--panel-main-bg-color-9) !important;
}
.el-checkbox__input.is-checked .el-checkbox__inner::after {

View file

@ -270,4 +270,10 @@ html {
color: var(--el-color-primary) !important;
background-color: var(--el-color-primary-light-9) !important;
border-color: var(--el-button-border-color) !important;
}
.el-collapse-item__header {
color: var(--el-text-color-regular) !important;
border: 1px solid var(--panel-main-bg-color-10);
background-color: var(--panel-main-bg-color-10) !important;
}