diff --git a/backend/app/http/v1/content.go b/backend/app/http/v1/content.go index ce915a5..e8713f0 100644 --- a/backend/app/http/v1/content.go +++ b/backend/app/http/v1/content.go @@ -4,10 +4,10 @@ import ( "quyun/v2/app/http/v1/dto" "quyun/v2/app/requests" "quyun/v2/app/services" + "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/gofiber/fiber/v3" - "github.com/spf13/cast" ) // @provider diff --git a/backend/app/http/v1/dto/tenant.go b/backend/app/http/v1/dto/tenant.go index b6e20bd..fbfc794 100644 --- a/backend/app/http/v1/dto/tenant.go +++ b/backend/app/http/v1/dto/tenant.go @@ -1,5 +1,12 @@ package dto +import "quyun/v2/app/requests" + +type TenantListFilter struct { + requests.Pagination + Keyword *string `query:"keyword"` +} + type TenantProfile struct { ID string `json:"id"` Name string `json:"name"` diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go index d4aa5ff..1a636c7 100644 --- a/backend/app/http/v1/routes.gen.go +++ b/backend/app/http/v1/routes.gen.go @@ -241,17 +241,15 @@ func (r *Routes) Register(router fiber.Router) { Body[dto.Settings]("form"), )) // Register routes for controller: Storage - r.log.Debugf("Registering route: Get /v1/storage/:key -> storage.Download") - router.Get("/v1/storage/:key"[len(r.Path()):], Func3( + r.log.Debugf("Registering route: Get /v1/storage/* -> storage.Download") + router.Get("/v1/storage/*"[len(r.Path()):], Func2( r.storage.Download, - PathParam[string]("key"), QueryParam[string]("expires"), QueryParam[string]("sign"), )) - r.log.Debugf("Registering route: Put /v1/storage/:key -> storage.Upload") - router.Put("/v1/storage/:key"[len(r.Path()):], DataFunc3( + r.log.Debugf("Registering route: Put /v1/storage/* -> storage.Upload") + router.Put("/v1/storage/*"[len(r.Path()):], DataFunc2( r.storage.Upload, - PathParam[string]("key"), QueryParam[string]("expires"), QueryParam[string]("sign"), )) @@ -268,6 +266,11 @@ func (r *Routes) Register(router fiber.Router) { PathParam[string]("id"), Query[dto.ContentListFilter]("filter"), )) + r.log.Debugf("Registering route: Get /v1/tenants -> tenant.List") + router.Get("/v1/tenants"[len(r.Path()):], DataFunc1( + r.tenant.List, + Query[dto.TenantListFilter]("filter"), + )) r.log.Debugf("Registering route: Get /v1/tenants/:id -> tenant.Get") router.Get("/v1/tenants/:id"[len(r.Path()):], DataFunc2( r.tenant.Get, diff --git a/backend/app/http/v1/storage.go b/backend/app/http/v1/storage.go index fe51803..ba7f070 100644 --- a/backend/app/http/v1/storage.go +++ b/backend/app/http/v1/storage.go @@ -17,19 +17,18 @@ type Storage struct { // Upload file // -// @Router /v1/storage/:key [put] +// @Router /v1/storage/* [put] // @Summary Upload file // @Tags Storage // @Accept octet-stream // @Produce json -// @Param key path string true "Object Key" // @Param expires query string true "Expiry" // @Param sign query string true "Signature" // @Success 200 {string} string "success" -// @Bind key path key(key) // @Bind expires query // @Bind sign query -func (s *Storage) Upload(ctx fiber.Ctx, key, expires, sign string) (string, error) { +func (s *Storage) Upload(ctx fiber.Ctx, expires, sign string) (string, error) { + key := ctx.Params("*") if err := s.storage.Verify("PUT", key, expires, sign); err != nil { return "", fiber.NewError(fiber.StatusForbidden, err.Error()) } @@ -59,19 +58,18 @@ func (s *Storage) Upload(ctx fiber.Ctx, key, expires, sign string) (string, erro // Download file // -// @Router /v1/storage/:key [get] +// @Router /v1/storage/* [get] // @Summary Download file // @Tags Storage // @Accept json // @Produce octet-stream -// @Param key path string true "Object Key" // @Param expires query string true "Expiry" // @Param sign query string true "Signature" // @Success 200 {file} file -// @Bind key path key(key) // @Bind expires query // @Bind sign query -func (s *Storage) Download(ctx fiber.Ctx, key, expires, sign string) error { +func (s *Storage) Download(ctx fiber.Ctx, expires, sign string) error { + key := ctx.Params("*") if err := s.storage.Verify("GET", key, expires, sign); err != nil { return fiber.NewError(fiber.StatusForbidden, err.Error()) } diff --git a/backend/app/http/v1/tenant.go b/backend/app/http/v1/tenant.go index 4b96015..669bc47 100644 --- a/backend/app/http/v1/tenant.go +++ b/backend/app/http/v1/tenant.go @@ -34,6 +34,23 @@ func (t *Tenant) ListContents(ctx fiber.Ctx, id string, filter *dto.ContentListF return services.Content.List(ctx, filter) } +// List tenants (search) +// +// @Router /v1/tenants [get] +// @Summary List tenants +// @Description Search tenants +// @Tags TenantPublic +// @Accept json +// @Produce json +// @Param keyword query string false "Keyword" +// @Param page query int false "Page" +// @Param limit query int false "Limit" +// @Success 200 {object} requests.Pager +// @Bind filter query +func (t *Tenant) List(ctx fiber.Ctx, filter *dto.TenantListFilter) (*requests.Pager, error) { + return services.Tenant.List(ctx, filter) +} + // Get tenant public profile // // @Router /v1/tenants/:id [get] diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index e1b9c14..c3aebc1 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -7,6 +7,7 @@ import ( "quyun/v2/app/errorx" "quyun/v2/app/http/v1/dto" + "quyun/v2/app/requests" "quyun/v2/database/models" "quyun/v2/pkg/consts" @@ -18,6 +19,52 @@ import ( // @provider type tenant struct{} +func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*requests.Pager, error) { + tbl, q := models.TenantQuery.QueryContext(ctx) + q = q.Where(tbl.Status.Eq(consts.TenantStatusVerified)) + + if filter.Keyword != nil && *filter.Keyword != "" { + q = q.Where(tbl.Name.Like("%" + *filter.Keyword + "%")) + } + + filter.Pagination.Format() + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + var data []dto.TenantProfile + for _, t := range list { + followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID)).Count() + contents, _ := models.ContentQuery.WithContext(ctx). + Where(models.ContentQuery.TenantID.Eq(t.ID), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)). + Count() + + cfg := t.Config.Data() + data = append(data, dto.TenantProfile{ + ID: cast.ToString(t.ID), + Name: t.Name, + Avatar: cfg.Avatar, + Bio: cfg.Bio, + Stats: dto.Stats{ + Followers: int(followers), + Contents: int(contents), + }, + }) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: data, + }, nil +} + func (s *tenant) GetPublicProfile(ctx context.Context, userID int64, id string) (*dto.TenantProfile, error) { tid := cast.ToInt64(id) t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tid)).First() diff --git a/frontend/portal/src/api/tenant.js b/frontend/portal/src/api/tenant.js index 56c3d7f..c4f72bd 100644 --- a/frontend/portal/src/api/tenant.js +++ b/frontend/portal/src/api/tenant.js @@ -2,6 +2,10 @@ import { request } from '../utils/request'; export const tenantApi = { get: (id) => request(`/tenants/${id}`), + list: (params) => { + const qs = new URLSearchParams(params).toString(); + return request(`/tenants?${qs}`); + }, follow: (id) => request(`/tenants/${id}/follow`, { method: 'POST' }), unfollow: (id) => request(`/tenants/${id}/follow`, { method: 'DELETE' }), }; diff --git a/frontend/portal/src/views/HomeView.vue b/frontend/portal/src/views/HomeView.vue index b4d5b54..6b6df5d 100644 --- a/frontend/portal/src/views/HomeView.vue +++ b/frontend/portal/src/views/HomeView.vue @@ -1,3 +1,49 @@ + + + diff --git a/frontend/portal/src/views/creator/SettingsView.vue b/frontend/portal/src/views/creator/SettingsView.vue index 44f3fac..8bf06a6 100644 --- a/frontend/portal/src/views/creator/SettingsView.vue +++ b/frontend/portal/src/views/creator/SettingsView.vue @@ -24,11 +24,13 @@
+ class="w-24 h-24 rounded-full border-4 border-slate-50 shadow-sm overflow-hidden bg-slate-100 relative">
- + class="absolute inset-0 bg-black/40 flex items-center justify-center transition-opacity" + :class="{'opacity-100': currentUploadType === 'avatar' && isUploading, 'opacity-0 group-hover:opacity-100': !(currentUploadType === 'avatar' && isUploading)}"> + +
@@ -62,9 +64,14 @@ 点击上传 (建议尺寸 1280x320)
-
- 更换封面 +
+ + 更换封面
@@ -79,9 +86,11 @@
-
@@ -193,6 +202,9 @@ const fileInput = ref(null); const currentUploadType = ref(''); const showAddAccount = ref(false); const payoutAccounts = ref([]); +const saveLoading = ref(false); +const isUploading = ref(false); +const uploadProgress = ref(0); const newAccount = reactive({ type: 'bank', @@ -240,9 +252,13 @@ const handleFileChange = async (event) => { const file = event.target.files[0]; if (!file) return; + isUploading.value = true; + uploadProgress.value = 0; + try { - toast.add({ severity: 'info', summary: '正在上传...', life: 2000 }); - const task = commonApi.uploadWithProgress(file, 'image'); + const task = commonApi.uploadWithProgress(file, 'image', (p) => { + uploadProgress.value = p; + }); const res = await task.promise; console.log('Upload response:', res); @@ -260,6 +276,9 @@ const handleFileChange = async (event) => { } catch (e) { console.error(e); toast.add({ severity: 'error', summary: '上传失败', detail: e.message, life: 3000 }); + } finally { + isUploading.value = false; + uploadProgress.value = 0; } event.target.value = ''; }; @@ -304,11 +323,15 @@ const saveSettings = async () => { toast.add({ severity: 'error', summary: '错误', detail: '频道名称不能为空', life: 3000 }); return; } + if (saveLoading.value) return; + saveLoading.value = true; try { await creatorApi.updateSettings(form); toast.add({ severity: 'success', summary: '保存成功', detail: '设置已更新', life: 3000 }); } catch (e) { toast.add({ severity: 'error', summary: '保存失败', detail: e.message, life: 3000 }); + } finally { + saveLoading.value = false; } }; \ No newline at end of file diff --git a/frontend/portal/src/views/user/FavoritesView.vue b/frontend/portal/src/views/user/FavoritesView.vue index b172937..e426e84 100644 --- a/frontend/portal/src/views/user/FavoritesView.vue +++ b/frontend/portal/src/views/user/FavoritesView.vue @@ -15,8 +15,10 @@
- - {{ item.duration || '专栏' }} + + 视频 + 音频 + 文章
@@ -24,11 +26,11 @@

{{ item.title }}

- - {{ item.author }} + + {{ item.author_name }}
- {{ item.time }} + {{ item.created_at }}
@@ -49,46 +51,37 @@