1.修改首页下载功能
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user