Softhub软件下载站实战开发(十四):软件收藏集设计

Softhub软件下载站实战开发(十四):软件收藏集设计 💾

前面几篇我们讲了软件管理相关实现,本篇我们实现后台管理最后一个功能,软件收藏集

引言:为什么我们需要收藏集功能?

在当今数字化时代,软件资源呈现爆炸式增长。用户面对海量软件时,常常会遇到几个核心痛点:

  1. ​资源分散​​:相关软件分散在不同分类中,难以集中管理
  2. ​效率低下​​:每次使用都需要重新搜索和下载
  3. ​知识沉淀​​:优秀的软件组合无法有效保存和分享
  4. ​个性化缺失​​:平台难以根据用户偏好提供精准推荐

收藏集(Resource Set)功能的引入,正是为了解决这些痛点。它就像数字世界的"收藏夹",但比传统收藏功能更强大、更系统化。在Softhub平台中,收藏集不仅是简单的软件列表,更是一个完整的资源组织系统。

收藏集功能的核心价值

1. 资源整合与分类管理 📂

收藏集允许用户将相关软件组织在一起形成主题合集。例如:

  • “前端开发必备工具集”:包含VS Code、Chrome、Git等
  • “设计师创意套装”:包含Photoshop、Illustrator、Figma等
  • “效率提升神器”:包含Notion、Todoist、RescueTime等

这种组织方式实现了多维度的资源聚合。

技术架构设计

数据库设计

核心接口设计

我们设计了完整的RESTful API接口:

端点方法描述参数
/dsResourceSetGET获取收藏集列表分页、搜索条件
/dsResourceSet/addPOST创建新收藏集名称、图标等
/dsResourceSet/editPUT修改收藏集ID、新数据
/dsResourceSet/delDELETE删除收藏集ID
/dsResourceSetRel/softwareListGET获取收藏集软件列表收藏集ID
/dsResourceSetRel/addSoftwarePOST添加软件到收藏集收藏集ID、软件ID数组
/dsResourceSetRel/removeSoftwareDELETE从收藏集移除软件收藏集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系列往期文章

  1. Softhub软件下载站实战开发(一):项目总览
  2. Softhub软件下载站实战开发(二):项目基础框架搭建
  3. Softhub软件下载站实战开发(三):平台管理模块实战
  4. Softhub软件下载站实战开发(四):代码生成器设计与实现
  5. Softhub软件下载站实战开发(五):分类模块实现
  6. Softhub软件下载站实战开发(六):软件配置面板实现
  7. Softhub软件下载站实战开发(七):集成MinIO实现文件存储功能
  8. Softhub软件下载站实战开发(八):编写软件后台管理
  9. Softhub软件下载站实战开发(九):编写软件配置管理界面
  10. Softhub软件下载站实战开发(十):实现图片视频上传下载接口
  11. Softhub软件下载站实战开发(十一):软件分片上传接口实现
  12. Softhub软件下载站实战开发(十二):软件管理编辑页面实现
  13. Softhub软件下载站实战开发(十三):软件管理前端分片上传实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值