feat:修改星榜的样式
88
CLAUDE.md
@ -163,3 +163,91 @@ Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need.
|
|||||||
- 一次修复引入新 bug,调试时间翻倍
|
- 一次修复引入新 bug,调试时间翻倍
|
||||||
- 用户对代码质量失去信任
|
- 用户对代码质量失去信任
|
||||||
- 提交历史变成"反复横跳"的打补丁记录
|
- 提交历史变成"反复横跳"的打补丁记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口开发规范
|
||||||
|
|
||||||
|
### 核心规则:添加/修改接口必须使用工程化方式完成
|
||||||
|
|
||||||
|
**每次新增或修改 API 接口时,必须以工程化、标准化方式完成**,禁止"能跑就行"的临时拼凑。完成后必须能直接通过代码审查,不需要大改。
|
||||||
|
|
||||||
|
### 工程化要求清单
|
||||||
|
|
||||||
|
1. **分层架构**(强制):
|
||||||
|
- `handler`(控制器):接收请求、参数绑定与校验、调用 service、组装响应
|
||||||
|
- `service`(业务层):业务逻辑编排、事务控制、调用 repository
|
||||||
|
- `repository` / `dao`(数据层):纯数据库操作,不含业务逻辑
|
||||||
|
- handler 中**禁止**直接调用 repository / 直接写 SQL
|
||||||
|
- service 中**禁止**直接操作 HTTP 请求/响应对象
|
||||||
|
|
||||||
|
2. **请求/响应 DTO**:
|
||||||
|
- 入参和出参使用独立的结构体(`XxxRequest` / `XxxResponse`)
|
||||||
|
- **禁止**直接用 DB model 当作入参或返回值
|
||||||
|
- 字段命名遵循项目既有规范(snake_case / camelCase)
|
||||||
|
- 敏感字段(密码、手机号、token)在响应中**必须脱敏或排除**
|
||||||
|
|
||||||
|
3. **参数校验**:
|
||||||
|
- 使用 `binding` / `validate` tag 在 handler 层做必填、长度、格式、枚举校验
|
||||||
|
- 业务规则校验放在 service 层
|
||||||
|
- 校验失败的错误信息要明确指出哪个字段、什么问题
|
||||||
|
|
||||||
|
4. **错误处理**:
|
||||||
|
- 使用项目统一的错误码/错误类型(如 `ErrCodeXxx`)
|
||||||
|
- **禁止**把原始 error 直接返回给前端
|
||||||
|
- **禁止**用 `_` 吞掉错误
|
||||||
|
- 关键业务错误必须打 ERROR 级别日志(含 trace_id / request_id)
|
||||||
|
|
||||||
|
5. **日志规范**:
|
||||||
|
- 接口入口:记录 method、path、request_id、用户身份(脱敏)
|
||||||
|
- 业务关键节点:状态流转、跨服务调用、缓存命中/未命中
|
||||||
|
- 异常退出:必须记录 stack trace 或 error cause
|
||||||
|
|
||||||
|
6. **API 文档**:
|
||||||
|
- 同步更新 Swagger / OpenAPI 注释
|
||||||
|
- 包含:接口描述、请求参数、响应示例、错误码列表、权限要求
|
||||||
|
- 字段类型、是否必填、示例值都要写清楚
|
||||||
|
|
||||||
|
7. **数据库变更**:
|
||||||
|
- 表结构变更必须写 migration
|
||||||
|
- 索引、外键、唯一约束、默认值要显式声明
|
||||||
|
- 影响现有数据的变更要考虑兼容方案(默认值、backfill)
|
||||||
|
|
||||||
|
8. **缓存策略**:
|
||||||
|
- 是否需要缓存、用什么 key、过期时间、缓存更新/失效策略要明确
|
||||||
|
- **禁止**缓存与 DB 数据不一致的方案(如只 set 不 delete)
|
||||||
|
|
||||||
|
9. **测试**:
|
||||||
|
- service 层核心业务逻辑必须覆盖单元测试
|
||||||
|
- handler 层至少一个 happy path + 一个 error case
|
||||||
|
- 数据库相关测试考虑使用事务回滚或测试容器
|
||||||
|
|
||||||
|
10. **遵循项目既有约定**:
|
||||||
|
- 命名风格、目录结构、文件命名、错误码定义与项目保持一致
|
||||||
|
- 复用项目已有的工具函数、中间件、错误处理逻辑
|
||||||
|
- **禁止**引入与项目风格冲突的新写法(例如项目用 snake_case 却写 camelCase)
|
||||||
|
|
||||||
|
### 禁止的反模式
|
||||||
|
|
||||||
|
- ❌ 在 handler 里直接写 SQL / ORM 调用
|
||||||
|
- ❌ 把 DB model 直接作为 API 入参或返回值
|
||||||
|
- ❌ 复制粘贴老接口代码不做适配(路径、参数、错误处理不一致)
|
||||||
|
- ❌ 用 `if err != nil { return err }` 一把梭,没有业务错误码
|
||||||
|
- ❌ 缺少或忘记更新 API 文档
|
||||||
|
- ❌ 改了表结构但没写 migration
|
||||||
|
- ❌ 没有写测试或测试只覆盖了 happy path
|
||||||
|
- ❌ 临时引入新的库/框架(未和项目既有技术栈对齐)
|
||||||
|
|
||||||
|
### 完成自检
|
||||||
|
|
||||||
|
接口写完后,逐项确认:
|
||||||
|
|
||||||
|
- [ ] 分层结构正确(handler / service / repository 各司其职)
|
||||||
|
- [ ] 入参/出参是独立 DTO
|
||||||
|
- [ ] 参数校验完整(必填、长度、格式、边界)
|
||||||
|
- [ ] 错误处理统一(错误码 + 友好提示 + 日志)
|
||||||
|
- [ ] 关键路径有日志
|
||||||
|
- [ ] Swagger 文档已更新
|
||||||
|
- [ ] DB 变更已写 migration
|
||||||
|
- [ ] 相关测试已编写并通过
|
||||||
|
- [ ] 命名、风格与项目既有代码一致
|
||||||
|
|||||||
@ -111,7 +111,9 @@
|
|||||||
"
|
"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<text class="like-count">{{ formatCount(item.like_count) }}</text>
|
<text class="like-count">{{
|
||||||
|
formatCount(item.like_count)
|
||||||
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="user-name">{{
|
<text class="user-name">{{
|
||||||
item.owner_nickname || item.creator_name || item.name || ""
|
item.owner_nickname || item.creator_name || item.name || ""
|
||||||
@ -132,22 +134,13 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 分页底部状态:加载中 / 没有更多了 / 暂无数据 -->
|
<!-- 分页底部状态:加载中 / 没有更多了 / 暂无数据 -->
|
||||||
<view
|
<view v-if="loadingMore" class="load-more-tip load-more-tip-loading">
|
||||||
v-if="loadingMore"
|
|
||||||
class="load-more-tip load-more-tip-loading"
|
|
||||||
>
|
|
||||||
<text class="load-more-text">加载中...</text>
|
<text class="load-more-text">加载中...</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view v-else-if="!hasMore && items.length > 0" class="load-more-tip">
|
||||||
v-else-if="!hasMore && items.length > 0"
|
|
||||||
class="load-more-tip"
|
|
||||||
>
|
|
||||||
<text class="load-more-text">— 没有更多了 —</text>
|
<text class="load-more-text">— 没有更多了 —</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view v-else-if="!loading && items.length === 0" class="load-more-tip">
|
||||||
v-else-if="!loading && items.length === 0"
|
|
||||||
class="load-more-tip"
|
|
||||||
>
|
|
||||||
<text class="load-more-text">暂无数据</text>
|
<text class="load-more-text">暂无数据</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -203,7 +196,7 @@ const PAGE_SIZE = 10;
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
key: "hot",
|
key: "hot",
|
||||||
label: "热度榜",
|
label: "点赞榜",
|
||||||
icon: "/static/square/galaxy/dianzanbang.png",
|
icon: "/static/square/galaxy/dianzanbang.png",
|
||||||
iconWidth: 64,
|
iconWidth: 64,
|
||||||
iconHeight: 72,
|
iconHeight: 72,
|
||||||
@ -380,7 +373,11 @@ const loadData = async ({ append = false } = {}) => {
|
|||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
// 同步路径下已经有精确 URL(命中缓存 / 完整未过期)则无需再请求
|
// 同步路径下已经有精确 URL(命中缓存 / 完整未过期)则无需再请求
|
||||||
const instant = it.cover_url;
|
const instant = it.cover_url;
|
||||||
if (instant && instant !== PLACEHOLDER_IMAGE && instant === getInstantAssetCoverUrl(raw)) {
|
if (
|
||||||
|
instant &&
|
||||||
|
instant !== PLACEHOLDER_IMAGE &&
|
||||||
|
instant === getInstantAssetCoverUrl(raw)
|
||||||
|
) {
|
||||||
// 已是精确 URL;仍然异步校准一次以处理过期(getAssetCoverRealUrl 内部命中缓存就是同步快速返回)
|
// 已是精确 URL;仍然异步校准一次以处理过期(getAssetCoverRealUrl 内部命中缓存就是同步快速返回)
|
||||||
}
|
}
|
||||||
getAssetCoverRealUrl(raw)
|
getAssetCoverRealUrl(raw)
|
||||||
@ -447,16 +444,16 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
// padding: 0 9.5rpx;
|
// padding: 0 9.5rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: url("/static/square/galaxy/xbbj.png") center no-repeat;
|
background: url("/static/square/galaxy/xbbj.png") center no-repeat;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-scroll {
|
.content-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -465,7 +462,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 80rpx;
|
padding: 0 64rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 16rpx;
|
top: 16rpx;
|
||||||
margin: 0 24rpx;
|
margin: 0 24rpx;
|
||||||
@ -476,12 +473,16 @@ onUnmounted(() => {
|
|||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(184deg, rgba(255, 90, 93, 0.47) -36.55%, rgba(194, 235, 255, 0.47) 121.2%);
|
background: linear-gradient(
|
||||||
filter: blur(5.849999904632568px);
|
184deg,
|
||||||
|
rgba(255, 90, 93, 0.47) -36.55%,
|
||||||
|
rgba(194, 235, 255, 0.47) 121.2%
|
||||||
|
);
|
||||||
|
filter: blur(5.9px);
|
||||||
// opacity: 0.8;
|
// opacity: 0.8;
|
||||||
// Figma 用的就是 filter: blur(图形模糊),不是 backdrop-filter(背景模糊)
|
// Figma 用的就是 filter: blur(图形模糊),不是 backdrop-filter(背景模糊)
|
||||||
// filter: blur(3.7px);
|
// filter: blur(3.7px);
|
||||||
-webkit-filter: blur(3.7px);
|
-webkit-filter: blur(5.9px);
|
||||||
border-top-left-radius: 14px;
|
border-top-left-radius: 14px;
|
||||||
border-top-right-radius: 13px;
|
border-top-right-radius: 13px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
@ -491,7 +492,7 @@ filter: blur(5.849999904632568px);
|
|||||||
|
|
||||||
.ranking-tab-item {
|
.ranking-tab-item {
|
||||||
height: 80rpx;
|
height: 80rpx;
|
||||||
width: 88rpx;
|
width: 99.2rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -500,7 +501,7 @@ filter: blur(5.849999904632568px);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ranking-tab-item.active {
|
.ranking-tab-item.active {
|
||||||
width: 96rpx;
|
width: 99.2rpx;
|
||||||
height: 160rpx;
|
height: 160rpx;
|
||||||
top: 40rpx;
|
top: 40rpx;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -536,7 +537,6 @@ filter: blur(5.849999904632568px);
|
|||||||
.ranking-tab-icon {
|
.ranking-tab-icon {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ranking-tab-item.active .ranking-tab-label {
|
.ranking-tab-item.active .ranking-tab-label {
|
||||||
@ -606,23 +606,23 @@ filter: blur(5.849999904632568px);
|
|||||||
/* 内容网格 */
|
/* 内容网格 */
|
||||||
.items-grid {
|
.items-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
// background: linear-gradient(
|
// background: linear-gradient(
|
||||||
// 145.83deg,
|
// 145.83deg,
|
||||||
// rgba(255, 90, 93, 0.2) 16.63%,
|
// rgba(255, 90, 93, 0.2) 16.63%,
|
||||||
// rgba(76, 237, 255, 0.2) 48.19%,
|
// rgba(76, 237, 255, 0.2) 48.19%,
|
||||||
// rgba(255, 122, 124, 0.2) 83.71%
|
// rgba(255, 122, 124, 0.2) 83.71%
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// pointer-events: none;
|
// pointer-events: none;
|
||||||
// backdrop-filter: blur(4.65px);
|
// backdrop-filter: blur(4.65px);
|
||||||
border-top-left-radius: 13px;
|
border-top-left-radius: 13px;
|
||||||
border-top-right-radius: 12px;
|
border-top-right-radius: 12px;
|
||||||
border-bottom-right-radius: 12px;
|
border-bottom-right-radius: 12px;
|
||||||
border-bottom-left-radius: 12px;
|
border-bottom-left-radius: 12px;
|
||||||
// opacity: 0.8;
|
// opacity: 0.8;
|
||||||
padding: 40rpx 20rpx 32rpx;
|
padding: 40rpx 20rpx 32rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@ -713,7 +713,7 @@ filter: blur(5.849999904632568px);
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
background: url("/static/square/galaxy/TOP4.png") center no-repeat;
|
background: url("/static/square/galaxy/TOP4.png") center no-repeat;
|
||||||
background-size: 100% 100%;
|
background-size: 100% 100%;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
@ -796,7 +796,7 @@ filter: blur(5.849999904632568px);
|
|||||||
/* Top 排名标签(顶到右边) */
|
/* Top 排名标签(顶到右边) */
|
||||||
.top-badge {
|
.top-badge {
|
||||||
margin-left: auto; /* 推到右侧 */
|
margin-left: auto; /* 推到右侧 */
|
||||||
margin-right:8rpx;
|
margin-right: 16rpx;
|
||||||
min-width: 100rpx;
|
min-width: 100rpx;
|
||||||
height: 36rpx;
|
height: 36rpx;
|
||||||
border-radius: 18rpx;
|
border-radius: 18rpx;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 3.1 KiB |