文章目录
Softhub软件下载站实战开发(十四):软件收藏集设计 💾
前面几篇我们讲了软件管理相关实现,本篇我们实现后台管理最后一个功能,软件收藏集
引言:为什么我们需要收藏集功能?
在当今数字化时代,软件资源呈现爆炸式增长。用户面对海量软件时,常常会遇到几个核心痛点:
- 资源分散:相关软件分散在不同分类中,难以集中管理
- 效率低下:每次使用都需要重新搜索和下载
- 知识沉淀:优秀的软件组合无法有效保存和分享
- 个性化缺失:平台难以根据用户偏好提供精准推荐
收藏集(Resource Set)功能的引入,正是为了解决这些痛点。它就像数字世界的"收藏夹",但比传统收藏功能更强大、更系统化。在Softhub平台中,收藏集不仅是简单的软件列表,更是一个完整的资源组织系统。
收藏集功能的核心价值
1. 资源整合与分类管理 📂
收藏集允许用户将相关软件组织在一起形成主题合集。例如:
- “前端开发必备工具集”:包含VS Code、Chrome、Git等
- “设计师创意套装”:包含Photoshop、Illustrator、Figma等
- “效率提升神器”:包含Notion、Todoist、RescueTime等
这种组织方式实现了多维度的资源聚合。
技术架构设计
数据库设计
核心接口设计
我们设计了完整的RESTful API接口:
端点 | 方法 | 描述 | 参数 |
---|---|---|---|
/dsResourceSet | GET | 获取收藏集列表 | 分页、搜索条件 |
/dsResourceSet/add | POST | 创建新收藏集 | 名称、图标等 |
/dsResourceSet/edit | PUT | 修改收藏集 | ID、新数据 |
/dsResourceSet/del | DELETE | 删除收藏集 | ID |
/dsResourceSetRel/softwareList | GET | 获取收藏集软件列表 | 收藏集ID |
/dsResourceSetRel/addSoftware | POST | 添加软件到收藏集 | 收藏集ID、软件ID数组 |
/dsResourceSetRel/removeSoftware | DELETE | 从收藏集移除软件 | 收藏集ID、软件ID |
后端实现详解
1. 收藏集服务层
type IDsResourceSet interface {
List(ctx context.Context, req *api.DsResourceSetListReq) (total interface{}, res []*model.DsResourceSetInfo, err error)
Add(ctx context.Context, req *api.DsResourceSetAddReq) (err error)
Edit(ctx context.Context, req *api.DsResourceSetEditReq) (err error)
Delete(ctx context.Context, id uint) (err error)
BatchDelete(ctx context.Context, ids []uint) (err error)
GetById(ctx context.Context, id uint) (res *model.DsResourceSetInfo, err error)
ClientList(ctx context.Context, req *clientApi.DsResourceSetListReq) (total interface{}, res []*model.DsResourceSetInfo, err error)
}
type IDsSoftwareResource interface {
List(ctx context.Context, req *api.DsSoftwareResourceListReq) (total interface{}, res []*model.DsSoftwareResourceInfo, err error)
Add(ctx context.Context, req *api.DsSoftwareResourceAddReq) (err error)
Edit(ctx context.Context, req *api.DsSoftwareResourceEditReq) (err error)
Delete(ctx context.Context, id interface{}) (err error)
BatchDelete(ctx context.Context, ids []interface{}) (err error)
GetById(ctx context.Context, id interface{}) (res *model.DsSoftwareResourceInfo, err error)
AddCount(ctx context.Context, resourceId interface{}) (count uint, err error)
SwitchDefault(ctx context.Context, req *api.DsSoftwareResourceSwitchDefaultReq) (err error)
InitChunkUpload(ctx context.Context, req *api.ChunkInitReq) (res *api.ChunkInitRes, err error)
UploadChunk(ctx context.Context, req *api.ChunkUploadReq) (res *api.ChunkUploadRes, err error)
MergeChunks(ctx context.Context, req *api.ChunkMergeReq) (res *api.ChunkMergeRes, err error)
}
2. 列表查询实现
func (s sDsResourceSet) List(ctx context.Context, req *api.DsResourceSetListReq) (total interface{}, dsResourceSetList []*model.DsResourceSetInfo, err error) {
err = g.Try(ctx, func(ctx context.Context) {
m := dao.DsResourceSet.Ctx(ctx)
columns := dao.DsResourceSet.Columns()
if req.Name != "" {
m = m.Where(fmt.Sprintf("%s like ?", columns.Name), "%"+req.Name+"%")
}
total, err = m.Count()
liberr.ErrIsNil(ctx, err, "获取资源集管理列表失败")
orderBy := req.OrderBy
if orderBy == "" {
orderBy = "created_at desc"
}
// 使用LEFT JOIN统计软件数量
err = m.Fields(fmt.Sprintf("%s.*, COUNT(DISTINCT rel.software_id) as software_count", dao.DsResourceSet.Table())).
LeftJoin("ds_resource_set_rel rel", fmt.Sprintf("%s.id = rel.set_id", dao.DsResourceSet.Table())).
Group(fmt.Sprintf("%s.id", dao.DsResourceSet.Table())).
Page(req.PageNum, req.PageSize).
Order(orderBy).
Scan(&dsResourceSetList)
liberr.ErrIsNil(ctx, err, "获取资源集管理列表失败")
})
return
}
3. 添加软件实现
func (s sDsResourceSet) Add(ctx context.Context, req *api.DsResourceSetAddReq) (err error) {
err = g.Try(ctx, func(ctx context.Context) {
// 查询是否已经存在
// add
_, err = dao.DsResourceSet.Ctx(ctx).Insert(do.DsResourceSet{
Name: req.Name, // 资源集名称
Icon: req.Icon, // 图标
Description: req.Description, // 资源描述
Remark: req.Remark, // 备注
CreatedBy: SystemS.Context().GetUserId(ctx),
UpdatedBy: SystemS.Context().GetUserId(ctx),
})
liberr.ErrIsNil(ctx, err, "新增资源集管理失败")
})
return
}
前端实现详解
1. 收藏集列表页面
<template>
<div class="system-dsResourceSet-container">
<el-card shadow="hover">
<div class="system-dsResourceSet-search mb15">
<el-form :inline="true">
<el-form-item label="资源集名称">
<el-input
size="default"
v-model="state.tableData.param.name"
style="width: 240px"
placeholder="请输入资源集名称"
class="w-50 m-2"
clearable
@keyup.enter="dsResourceSetList"
/>
</el-form-item>
<el-form-item>
<el-button size="default" type="primary" class="ml10" @click="dsResourceSetList">
<el-icon>
<ele-Search />
</el-icon>
查询
</el-button>
<el-button size="default" type="success" class="ml10" @click="onOpenAddDsResourceSet">
<el-icon>
<ele-FolderAdd />
</el-icon>
新增
</el-button>
<el-button size="default" type="danger" class="ml10" @click="onRowDel(null)">
<el-icon>
<ele-Delete />
</el-icon>
删除
</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="state.tableData.data" style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="name" label="资源集名称" show-overflow-tooltip></el-table-column>
<el-table-column prop="icon" label="图标" show-overflow-tooltip>
<template #default="scope">
<font-awesome-icon v-if="scope.row.icon" :icon="getIconObject(scope.row.icon)" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="description" label="资源描述" show-overflow-tooltip></el-table-column>
<el-table-column prop="softwareCount" label="软件数量" width="100" align="center">
<template #default="scope">
<el-tag type="info" size="small">{{ scope.row.softwareCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="updatedAt" label="更新时间" width="180" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="280">
<template #default="scope">
<el-button size="small" text type="primary" @click="onOpenEditDsResourceSet(scope.row)">修改</el-button>
<el-button size="small" text type="success" @click="onOpenSoftwareManage(scope.row)">软件管理</el-button>
<el-button size="small" text type="danger" @click="onRowDel(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="state.tableData.total>0"
:total="state.tableData.total"
v-model:page="state.tableData.param.pageNum"
v-model:limit="state.tableData.param.pageSize"
@pagination="dsResourceSetList"
/>
</el-card>
<EditDsResourceSet ref="editDsResourceSetRef" @getDsResourceSetList="dsResourceSetList"/>
<SoftwareManageDialog ref="softwareManageDialogRef" />
</div>
</template>
2. 软件管理弹窗组件
代码实现
<template>
<div class="software-manage-dialog">
<el-dialog v-model="state.isShowDialog" width="1000px" title="软件管理">
<div class="software-manage-content">
<!-- 当前资源集下的软件列表 -->
<div class="current-software-section">
<div class="section-header">
<h3>当前资源集下的软件</h3>
<el-button size="small" type="primary" @click="onOpenAddSoftwareDialog">
<el-icon><ele-Plus /></el-icon>
添加软件
</el-button>
</div>
<el-table :data="state.currentSoftwareList" style="width: 100%" v-loading="state.loading">
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="softwareName" label="软件名称" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
<el-table-column prop="officialWebsite" label="官网地址" show-overflow-tooltip />
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button size="small" text type="danger" @click="onRemoveSoftware(scope.row)">
移除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 添加软件弹窗 -->
<el-dialog v-model="state.isShowAddDialog" width="800px" title="添加软件" append-to-body>
<div class="add-software-content">
<!-- 搜索框 -->
<div class="search-section mb15">
<el-form :inline="true">
<el-form-item label="软件名称">
<el-input
v-model="state.searchKeyword"
placeholder="请输入软件名称"
clearable
size="default"
style="width: 200px"
@keyup.enter="onSearch"
@input="onSearchKeywordChange"
/>
</el-form-item>
<el-form-item>
<el-button size="default" type="primary" @click="onSearch">
<el-icon><ele-Search /></el-icon>
搜索
</el-button>
<el-button size="default" @click="onResetSearch">
<el-icon><ele-Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</div>
<el-table
:data="state.allSoftwareList"
style="width: 100%"
v-loading="state.addLoading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="softwareName" label="软件名称" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" show-overflow-tooltip />
<el-table-column prop="officialWebsite" label="官网地址" show-overflow-tooltip />
</el-table>
<!-- 分页 -->
<pagination
v-show="state.allSoftwareTotal > 0"
:total="state.allSoftwareTotal"
v-model:page="state.allSoftwareParam.pageNum"
v-model:limit="state.allSoftwareParam.pageSize"
@pagination="getAllSoftwareList"
/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="state.isShowAddDialog = false" size="default">取消</el-button>
<el-button type="primary" @click="onAddSoftware" size="default" :loading="state.adding">
确定添加
</el-button>
</span>
</template>
</el-dialog>
</el-dialog>
</div>
</template>
3. 软件选择与添加逻辑
// 获取所有软件列表(带搜索和分页)
const getAllSoftwareList = () => {
state.addLoading = true;
const params = {
...state.allSoftwareParam,
softwareName: state.searchKeyword || undefined
};
getDsSoftwareList(params)
.then(res => {
state.allSoftwareList = processBigIntIds(res.data.dsSoftwareList || []);
state.allSoftwareTotal = res.data.total || 0;
})
.finally(() => {
state.addLoading = false;
});
};
// 添加选中软件到收藏集
const onAddSoftware = () => {
if (state.selectedSoftwareIds.length === 0) {
ElMessage.warning('请选择要添加的软件');
return;
}
state.adding = true;
const softwareIds = state.selectedSoftwareIds.map(id => toBigIntString(id));
addSoftwareToResourceSet({
setId: state.currentSetId,
softwareIds: softwareIds
})
.then(() => {
ElMessage.success('添加软件成功');
state.isShowAddDialog = false;
getCurrentSoftwareList();
})
.finally(() => {
state.adding = false;
});
};
softhub系列往期文章
- Softhub软件下载站实战开发(一):项目总览
- Softhub软件下载站实战开发(二):项目基础框架搭建
- Softhub软件下载站实战开发(三):平台管理模块实战
- Softhub软件下载站实战开发(四):代码生成器设计与实现
- Softhub软件下载站实战开发(五):分类模块实现
- Softhub软件下载站实战开发(六):软件配置面板实现
- Softhub软件下载站实战开发(七):集成MinIO实现文件存储功能
- Softhub软件下载站实战开发(八):编写软件后台管理
- Softhub软件下载站实战开发(九):编写软件配置管理界面
- Softhub软件下载站实战开发(十):实现图片视频上传下载接口
- Softhub软件下载站实战开发(十一):软件分片上传接口实现
- Softhub软件下载站实战开发(十二):软件管理编辑页面实现
- Softhub软件下载站实战开发(十三):软件管理前端分片上传实现