1.修改首页下载功能

This commit is contained in:
whm
2026-03-18 18:43:23 +08:00
parent c67346626a
commit b17e99eb93
4 changed files with 150 additions and 8 deletions

View File

@@ -64,10 +64,15 @@ export const deletePage = (id) => request.delete(`/admin/pages/${id}`)
export const getHomepage = (siteId) => request.get(`/admin/sites/${siteId}/homepage`) export const getHomepage = (siteId) => request.get(`/admin/sites/${siteId}/homepage`)
export const updateHomepage = (siteId, data) => request.put(`/admin/sites/${siteId}/homepage`, data) export const updateHomepage = (siteId, data) => request.put(`/admin/sites/${siteId}/homepage`, data)
export const downloadHomepage = (siteId) => request.get(`/admin/sites/${siteId}/homepage/download`, { responseType: 'blob' }) export const downloadHomepage = (siteId) => request.get(`/admin/sites/${siteId}/homepage/download`, { responseType: 'blob' })
export const getDownloadableAssets = (siteId) => request.get(`/admin/sites/${siteId}/assets/downloadable`)
// 文件管理(功能模块:多级目录、可下载) // 文件管理(功能模块:多级目录、可下载)
export const getSiteAssets = (siteId, path) => export const getSiteAssets = (siteId, path, opts = {}) => {
request.get(`/admin/sites/${siteId}/assets`, { params: path ? { path } : {} }) const params = {}
if (path) params.path = path
if (opts.downloadable) params.downloadable = '1'
return request.get(`/admin/sites/${siteId}/assets`, { params })
}
export const uploadSiteAsset = (siteId, file, opts = {}) => { export const uploadSiteAsset = (siteId, file, opts = {}) => {
const form = new FormData() const form = new FormData()
form.append('file', file) form.append('file', file)

View File

@@ -43,14 +43,16 @@
<el-input v-model="form.download_text" placeholder="START EXPLORING" /> <el-input v-model="form.download_text" placeholder="START EXPLORING" />
</el-form-item> </el-form-item>
<el-form-item label="按钮链接"> <el-form-item label="按钮链接">
<el-input v-model="form.download_url" placeholder="#" /> <el-input v-model="form.download_url" placeholder="#" style="flex: 1" />
<el-button type="primary" link @click="openFilePicker('download')">选择可下载文件</el-button>
</el-form-item> </el-form-item>
<el-divider content-position="left">平台轨道</el-divider> <el-divider content-position="left">平台轨道</el-divider>
<el-form-item label="平台列表"> <el-form-item label="平台列表">
<div v-for="(p, i) in form.platforms" :key="i" style="display: flex; gap: 8px; margin-bottom: 8px"> <div v-for="(p, i) in form.platforms" :key="i" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center">
<el-input v-model="p.name" placeholder="如 WINDOWS" style="width: 140px" /> <el-input v-model="p.name" placeholder="如 WINDOWS" style="width: 140px" />
<el-input v-model="p.url" placeholder="链接" style="flex: 1" /> <el-input v-model="p.url" placeholder="链接" style="flex: 1" />
<el-button type="primary" link @click="openFilePicker('platform', i)">选择文件</el-button>
<el-button link type="danger" @click="form.platforms.splice(i, 1)">删除</el-button> <el-button link type="danger" @click="form.platforms.splice(i, 1)">删除</el-button>
</div> </div>
<el-button link type="primary" @click="form.platforms.push({ name: '', url: '#' })">+ 添加平台</el-button> <el-button link type="primary" @click="form.platforms.push({ name: '', url: '#' })">+ 添加平台</el-button>
@@ -84,13 +86,31 @@
<el-empty v-else description="请先选择站点" /> <el-empty v-else description="请先选择站点" />
</el-card> </el-card>
<el-dialog v-model="filePickerVisible" title="选择可下载文件" width="640px">
<el-table
v-loading="fileListLoading"
:data="downloadableFiles"
highlight-current-row
@current-change="onSelectFile"
>
<el-table-column property="name" label="文件名" min-width="180" />
<el-table-column property="file_path" label="路径" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button type="primary" link @click="confirmSelectFile(row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!fileListLoading && downloadableFiles.length === 0" description="暂无可下载文件,请在文件管理中上传并勾选“允许下载”" />
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, watch } from 'vue' import { ref, reactive, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getSites, getOfficialSite, getHomepage, updateHomepage } from '../../api/admin' import { getSites, getOfficialSite, getHomepage, updateHomepage, getDownloadableAssets } from '../../api/admin'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
const siteId = ref('') const siteId = ref('')
@@ -98,6 +118,10 @@ const sites = ref([])
const saving = ref(false) const saving = ref(false)
const downloading = ref(false) const downloading = ref(false)
const formRef = ref(null) const formRef = ref(null)
const filePickerVisible = ref(false)
const fileListLoading = ref(false)
const downloadableFiles = ref([])
const filePickerTarget = ref({ type: 'download' }) // { type: 'download' } | { type: 'platform', index: number }
const defaultForm = () => ({ const defaultForm = () => ({
logo_text: 'YUHENG ONE', logo_text: 'YUHENG ONE',
@@ -203,6 +227,45 @@ const handleDownload = async () => {
} }
} }
function openFilePicker(type, index) {
filePickerTarget.value = type === 'platform' ? { type: 'platform', index } : { type: 'download' }
filePickerVisible.value = true
fetchDownloadableFiles()
}
async function fetchDownloadableFiles() {
if (!siteId.value) return
fileListLoading.value = true
try {
const res = await getDownloadableAssets(siteId.value)
downloadableFiles.value = res.list || []
} catch (e) {
ElMessage.error(e.message)
downloadableFiles.value = []
} finally {
fileListLoading.value = false
}
}
function buildDownloadUrl(asset) {
return `/api/web/sites/${siteId.value}/assets/${asset.id}/download`
}
function confirmSelectFile(asset) {
const url = buildDownloadUrl(asset)
if (filePickerTarget.value.type === 'download') {
form.download_url = url
} else {
form.platforms[filePickerTarget.value.index].url = url
}
filePickerVisible.value = false
ElMessage.success('已选择:' + asset.name)
}
function onSelectFile(row) {
if (row) confirmSelectFile(row)
}
onMounted(() => { onMounted(() => {
fetchSites().then(() => fetchData()) fetchSites().then(() => fetchData())
}) })

View File

@@ -32,13 +32,18 @@ func pathPrefix(siteID string) string {
return "sites/" + siteID + "/" return "sites/" + siteID + "/"
} }
// ListSiteAssets 站点功能模块/上传文件列表query path 为当前目录相对路径(空为根) // ListSiteAssets 站点功能模块/上传文件列表query path 为当前目录相对路径(空为根)downloadable=1 时返回该站点下所有可下载文件(供首页编辑选择)
func ListSiteAssets(c *gin.Context) { func ListSiteAssets(c *gin.Context) {
siteID := c.Param("site_id") siteID := c.Param("site_id")
if siteID == "" { if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"}) c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return return
} }
onlyDownloadable := c.Query("downloadable") == "1" || c.Query("downloadable") == "true"
if onlyDownloadable {
listDownloadableAssets(c, siteID)
return
}
path := c.Query("path") path := c.Query("path")
prefix := pathPrefix(siteID) prefix := pathPrefix(siteID)
if path != "" { if path != "" {
@@ -52,7 +57,6 @@ func ListSiteAssets(c *gin.Context) {
defer cancel() defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets") coll := config.GetDB(config.DBName).Collection("site_assets")
// 仅当前目录下直接文件file_path 为 prefix + 不含 / 的文件名)
filter := bson.M{"site_id": siteID, "file_path": bson.M{"$regex": "^" + regexp.QuoteMeta(prefix) + "[^/]+$"}} filter := bson.M{"site_id": siteID, "file_path": bson.M{"$regex": "^" + regexp.QuoteMeta(prefix) + "[^/]+$"}}
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}}) opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := coll.Find(ctx, filter, opts) cursor, err := coll.Find(ctx, filter, opts)
@@ -68,11 +72,40 @@ func ListSiteAssets(c *gin.Context) {
return return
} }
total, _ := coll.CountDocuments(ctx, filter) total, _ := coll.CountDocuments(ctx, filter)
// 子目录列表:从 file_path 中提取当前 path 下的一级子目录名
subDirs := listSubDirs(c, siteID, path) subDirs := listSubDirs(c, siteID, path)
c.JSON(http.StatusOK, gin.H{"list": list, "total": total, "sub_dirs": subDirs}) c.JSON(http.StatusOK, gin.H{"list": list, "total": total, "sub_dirs": subDirs})
} }
func listDownloadableAssets(c *gin.Context, siteID string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
filter := bson.M{"site_id": siteID, "downloadable": true}
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := coll.Find(ctx, filter, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var list []models.SiteAsset
if err = cursor.All(ctx, &list); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"list": list, "total": len(list)})
}
// ListDownloadableAssets 仅返回可下载文件列表(供首页编辑选择,仅需 homepage:edit 权限)
func ListDownloadableAssets(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
listDownloadableAssets(c, siteID)
}
func listSubDirs(c *gin.Context, siteID, currentPath string) []string { func listSubDirs(c *gin.Context, siteID, currentPath string) []string {
prefix := pathPrefix(siteID) prefix := pathPrefix(siteID)
if currentPath != "" { if currentPath != "" {
@@ -250,3 +283,41 @@ func CreateSiteFolder(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{"message": "创建成功", "path": filepath.ToSlash(clean)}) c.JSON(http.StatusOK, gin.H{"message": "创建成功", "path": filepath.ToSlash(clean)})
} }
// DownloadSiteAsset 前台公开下载:仅当资源标记为可下载时返回文件(供首页等使用)
func DownloadSiteAsset(c *gin.Context) {
siteID := c.Param("site_id")
assetIDStr := c.Param("asset_id")
if siteID == "" || assetIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
oid, err := bson.ObjectIDFromHex(assetIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
var asset models.SiteAsset
err = coll.FindOne(ctx, bson.M{"_id": oid, "site_id": siteID}).Decode(&asset)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
if !asset.Downloadable {
c.JSON(http.StatusForbidden, gin.H{"error": "该资源不可下载"})
return
}
fullPath := filepath.Join(getUploadDir(), filepath.FromSlash(asset.FilePath))
if _, err := os.Stat(fullPath); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
c.Header("Content-Disposition", "attachment; filename=\""+asset.Name+"\"")
if asset.ContentType != "" {
c.Header("Content-Type", asset.ContentType)
}
c.File(fullPath)
}

View File

@@ -161,6 +161,7 @@ func main() {
admin.GET("/sites/:site_id/homepage/download", handlers.RequirePermission(models.PermHomepageEdit), handlers.DownloadHomepage) admin.GET("/sites/:site_id/homepage/download", handlers.RequirePermission(models.PermHomepageEdit), handlers.DownloadHomepage)
admin.GET("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.GetHomepage) admin.GET("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.GetHomepage)
admin.PUT("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.UpdateHomepage) admin.PUT("/sites/:site_id/homepage", handlers.RequirePermission(models.PermHomepageEdit), handlers.UpdateHomepage)
admin.GET("/sites/:site_id/assets/downloadable", handlers.RequirePermission(models.PermHomepageEdit), handlers.ListDownloadableAssets)
admin.GET("/sites/:site_id/assets", handlers.RequirePermission(models.PermSiteManage), handlers.ListSiteAssets) admin.GET("/sites/:site_id/assets", handlers.RequirePermission(models.PermSiteManage), handlers.ListSiteAssets)
admin.POST("/sites/:site_id/assets", handlers.RequirePermission(models.PermModuleUpload), handlers.UploadSiteAsset) admin.POST("/sites/:site_id/assets", handlers.RequirePermission(models.PermModuleUpload), handlers.UploadSiteAsset)
admin.POST("/sites/:site_id/folders", handlers.RequirePermission(models.PermModuleUpload), handlers.CreateSiteFolder) admin.POST("/sites/:site_id/folders", handlers.RequirePermission(models.PermModuleUpload), handlers.CreateSiteFolder)
@@ -212,6 +213,8 @@ func main() {
web.GET("/info", func(c *gin.Context) { web.GET("/info", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "web api"}) c.JSON(http.StatusOK, gin.H{"message": "web api"})
}) })
// 可下载资源公开下载(首页等链接指向此路径)
web.GET("/sites/:site_id/assets/:asset_id/download", handlers.DownloadSiteAsset)
} }
port := os.Getenv("PORT") port := os.Getenv("PORT")