Skip to content

Commit ad7f7b2

Browse files
committed
feat: 市场收藏功能-收藏/取消收藏市场条目(租户级t_market_favorite表),列表显示收藏星标,支持只看收藏过滤,市场条目与收藏表接入多租户全局过滤器
1 parent 5a77565 commit ad7f7b2

5 files changed

Lines changed: 99 additions & 5 deletions

File tree

‎Juggle.Api/Controllers/Api/MarketController.cs‎

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,60 @@ public async Task<ApiResult> ImportFile([FromBody] MarketImportFileRequest req)
192192
}
193193
}
194194

195-
/// <summary>市场列表(平台级条目 + 我的条目),可按类型筛选</summary>
195+
/// <summary>市场列表(平台级条目 + 我的条目),可按类型筛选,favorite=1 只看收藏</summary>
196196
[HttpGet("list")]
197-
public async Task<ApiResult> List([FromQuery] string? type)
197+
public async Task<ApiResult> List([FromQuery] string? type, [FromQuery] int favorite = 0)
198198
{
199+
var tid = _tenant.TenantId;
199200
var query = _db.MarketItems.Where(m => m.Deleted == 0 && m.Enabled == 1);
200201
if (!string.IsNullOrWhiteSpace(type)) query = query.Where(m => m.ItemType == type);
202+
203+
var favIds = (await _db.MarketFavorites
204+
.Where(f => f.Deleted == 0 && f.TenantId == tid)
205+
.Select(f => f.MarketItemId)
206+
.ToListAsync()).ToHashSet();
207+
if (favorite == 1)
208+
query = query.Where(m => favIds.Contains(m.Id));
209+
201210
var items = await query.OrderByDescending(m => m.DownloadCount).ThenByDescending(m => m.Id).ToListAsync();
202211
return ApiResult.Success(items.Select(m => new
203212
{
204213
m.Id, m.ItemType, m.ItemName, m.Description, m.GroupName, m.DownloadCount,
205-
m.MarketItemId, m.Icon, m.Author, m.Version, m.UpdatedAt, m.ContentJson
214+
m.MarketItemId, m.Icon, m.Author, m.Version, m.UpdatedAt, m.ContentJson,
215+
favorited = favIds.Contains(m.Id)
206216
}));
207217
}
208218

219+
/// <summary>收藏市场条目(幂等)。</summary>
220+
[HttpPost("favorite/{id}")]
221+
public async Task<ApiResult> Favorite(long id)
222+
{
223+
var tid = _tenant.TenantId;
224+
var exists = await _db.MarketFavorites.AnyAsync(f => f.MarketItemId == id && f.TenantId == tid && f.Deleted == 0);
225+
if (exists) return ApiResult.Success(true);
226+
_db.MarketFavorites.Add(new MarketFavoriteEntity
227+
{
228+
MarketItemId = id,
229+
TenantId = tid,
230+
CreatedBy = tid,
231+
CreatedAt = DateTime.Now.ToString("o")
232+
});
233+
await _db.SaveChangesAsync();
234+
return ApiResult.Success(true);
235+
}
236+
237+
/// <summary>取消收藏市场条目。</summary>
238+
[HttpPost("unfavorite/{id}")]
239+
public async Task<ApiResult> Unfavorite(long id)
240+
{
241+
var tid = _tenant.TenantId;
242+
var fav = await _db.MarketFavorites.FirstOrDefaultAsync(f => f.MarketItemId == id && f.TenantId == tid && f.Deleted == 0);
243+
if (fav == null) return ApiResult.Success(false);
244+
fav.Deleted = 1;
245+
await _db.SaveChangesAsync();
246+
return ApiResult.Success(false);
247+
}
248+
209249
/// <summary>我发布的市场条目(管理/下架)</summary>
210250
[HttpGet("my-list")]
211251
public async Task<ApiResult> MyList([FromQuery] string? type)

‎Juggle.Api/Program.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ await ctx.Response.WriteAsync(
267267
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_market_item ADD COLUMN icon TEXT DEFAULT NULL;"); } catch { }
268268
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_market_item ADD COLUMN author TEXT DEFAULT NULL;"); } catch { }
269269
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_market_item ADD COLUMN version TEXT DEFAULT NULL;"); } catch { }
270+
// 市场收藏表
271+
try { db.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS t_market_favorite(id INTEGER PRIMARY KEY AUTOINCREMENT, market_item_id INTEGER, created_at TEXT, created_by INTEGER, updated_at TEXT, updated_by INTEGER, tenant_id INTEGER, deleted INTEGER DEFAULT 0);"); } catch { }
270272
// AI 供应商能力字段
271273
try { db.Database.ExecuteSqlRaw("ALTER TABLE t_ai_provider ADD COLUMN capabilities TEXT DEFAULT NULL;"); } catch { }
272274
// 知识库 4 表
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Juggle.Domain.Entities;
2+
3+
/// <summary>
4+
/// 市场收藏:租户对市场条目的收藏记录(支持过滤是否收藏)。
5+
/// </summary>
6+
public class MarketFavoriteEntity : BaseEntity
7+
{
8+
/// <summary>市场条目 ID(t_market_item.id)</summary>
9+
public long MarketItemId { get; set; }
10+
}

‎Juggle.Infrastructure/Persistence/JuggleDbContext.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public JuggleDbContext(DbContextOptions<JuggleDbContext> options, ICurrentTenant
6868
public DbSet<RedisConfigEntity> RedisConfigs { get; set; } = null!;
6969
public DbSet<SkillEntity> Skills { get; set; } = null!;
7070
public DbSet<MarketItemEntity> MarketItems { get; set; } = null!;
71+
public DbSet<MarketFavoriteEntity> MarketFavorites { get; set; } = null!;
7172

7273
protected override void OnModelCreating(ModelBuilder modelBuilder)
7374
{
@@ -271,6 +272,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
271272
e.Property(p => p.Author).HasColumnName("author");
272273
e.Property(p => p.Version).HasColumnName("version");
273274
});
275+
modelBuilder.Entity<MarketFavoriteEntity>().ToTable("t_market_favorite");
276+
modelBuilder.Entity<MarketFavoriteEntity>(e => {
277+
e.Property(p => p.Id).HasColumnName("id");
278+
e.Property(p => p.Deleted).HasColumnName("deleted");
279+
e.Property(p => p.CreatedAt).HasColumnName("created_at");
280+
e.Property(p => p.CreatedBy).HasColumnName("created_by");
281+
e.Property(p => p.UpdatedAt).HasColumnName("updated_at");
282+
e.Property(p => p.UpdatedBy).HasColumnName("updated_by");
283+
e.Property(p => p.TenantId).HasColumnName("tenant_id");
284+
e.Property(p => p.MarketItemId).HasColumnName("market_item_id");
285+
});
274286
modelBuilder.Entity<AlertRuleEntity>().ToTable("t_alert_rule");
275287
modelBuilder.Entity<AlertRecordEntity>().ToTable("t_alert_record");
276288
modelBuilder.Entity<DataViewEntity>().ToTable("t_data_view");
@@ -811,5 +823,9 @@ void AddTenantIdCol<T>() where T : BaseEntity
811823
modelBuilder.Entity<SuiteEntity>().HasQueryFilter(e => CurrentTenantId == null || e.TenantId == null || e.TenantId == CurrentTenantId);
812824
modelBuilder.Entity<ApiEntity>().HasQueryFilter(e => CurrentTenantId == null || e.TenantId == null || e.TenantId == CurrentTenantId);
813825
modelBuilder.Entity<ParameterEntity>().HasQueryFilter(e => CurrentTenantId == null || e.TenantId == null || e.TenantId == CurrentTenantId);
826+
827+
// 市场条目/收藏:平台级共享 + 租户级收藏
828+
modelBuilder.Entity<MarketItemEntity>().HasQueryFilter(e => CurrentTenantId == null || e.TenantId == null || e.TenantId == CurrentTenantId);
829+
modelBuilder.Entity<MarketFavoriteEntity>().HasQueryFilter(e => CurrentTenantId == null || e.TenantId == CurrentTenantId);
814830
}
815831
}

‎JuggleNet6.Frontend/src/views/market/MarketList.vue‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
<el-tab-pane label="Skills 市场" name="skill" />
1616
<el-tab-pane label="报表市场" name="report" />
1717
</el-tabs>
18-
<el-input v-model="keyword" placeholder="搜索名称/描述" size="small" style="width:220px;margin-bottom:12px" clearable />
18+
<div style="display:flex;gap:16px;align-items:center;margin-bottom:12px">
19+
<el-input v-model="keyword" placeholder="搜索名称/描述" size="small" style="width:220px" clearable />
20+
<el-checkbox v-model="onlyFavorite" size="small" @change="loadData">⭐ 只看收藏</el-checkbox>
21+
</div>
1922
<el-table :data="filteredList" v-loading="loading">
2023
<el-table-column label="名称" width="240" show-overflow-tooltip>
2124
<template #default="{ row }"><span style="margin-right:4px">{{ row.icon || '📦' }}</span>{{ row.itemName }}</template>
@@ -26,6 +29,13 @@
2629
<el-table-column prop="updatedAt" label="更新日期" width="110" />
2730
<el-table-column prop="description" label="描述" min-width="220" show-overflow-tooltip />
2831
<el-table-column prop="downloadCount" label="下载" width="70" align="center" />
32+
<el-table-column label="收藏" width="70" align="center">
33+
<template #default="{ row }">
34+
<el-button size="small" link :loading="favoritingId === row.id" @click="toggleFavorite(row)">
35+
<span :style="`font-size:16px;color:${row.favorited ? '#f7ba2a' : '#c0c4cc'}`">{{ row.favorited ? '★' : '☆' }}</span>
36+
</el-button>
37+
</template>
38+
</el-table-column>
2939
<el-table-column label="操作" width="140" fixed="right">
3040
<template #default="{ row }">
3141
<el-button size="small" type="primary" link :loading="importingId === row.id" @click="doImport(row)">直接应用</el-button>
@@ -87,8 +97,10 @@ import request from '../../utils/request'
8797
const loading = ref(false)
8898
const activeTab = ref('api')
8999
const keyword = ref('')
100+
const onlyFavorite = ref(false)
90101
const tableData = ref<any[]>([])
91102
const importingId = ref<number | null>(null)
103+
const favoritingId = ref<number | null>(null)
92104
93105
// ====== 发现官方市场 ======
94106
const discoverVisible = ref(false)
@@ -116,11 +128,25 @@ onMounted(loadData)
116128
async function loadData() {
117129
loading.value = true
118130
try {
119-
const res: any = await request.get('/market/list', { params: { type: activeTab.value } })
131+
const res: any = await request.get('/market/list', { params: { type: activeTab.value, favorite: onlyFavorite.value ? 1 : 0 } })
120132
tableData.value = res.data || []
121133
} finally { loading.value = false }
122134
}
123135
136+
async function toggleFavorite(row: any) {
137+
favoritingId.value = row.id
138+
try {
139+
const res: any = await request.post(row.favorited ? `/market/unfavorite/${row.id}` : `/market/favorite/${row.id}`)
140+
row.favorited = res.data
141+
if (onlyFavorite.value && !row.favorited) {
142+
tableData.value = tableData.value.filter((r: any) => r.id !== row.id)
143+
}
144+
ElMessage.success(row.favorited ? '已收藏' : '已取消收藏')
145+
} catch (e: any) {
146+
ElMessage.error(e?.message || '操作失败')
147+
} finally { favoritingId.value = null }
148+
}
149+
124150
async function doImport(row: any) {
125151
importingId.value = row.id
126152
try {

0 commit comments

Comments
 (0)