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,调试时间翻倍
|
||||
- 用户对代码质量失去信任
|
||||
- 提交历史变成"反复横跳"的打补丁记录
|
||||
|
||||
---
|
||||
|
||||
## 接口开发规范
|
||||
|
||||
### 核心规则:添加/修改接口必须使用工程化方式完成
|
||||
|
||||
**每次新增或修改 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"
|
||||
/>
|
||||
<text class="like-count">{{ formatCount(item.like_count) }}</text>
|
||||
<text class="like-count">{{
|
||||
formatCount(item.like_count)
|
||||
}}</text>
|
||||
</view>
|
||||
<text class="user-name">{{
|
||||
item.owner_nickname || item.creator_name || item.name || ""
|
||||
@ -132,22 +134,13 @@
|
||||
</view>
|
||||
|
||||
<!-- 分页底部状态:加载中 / 没有更多了 / 暂无数据 -->
|
||||
<view
|
||||
v-if="loadingMore"
|
||||
class="load-more-tip load-more-tip-loading"
|
||||
>
|
||||
<view v-if="loadingMore" class="load-more-tip load-more-tip-loading">
|
||||
<text class="load-more-text">加载中...</text>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="!hasMore && items.length > 0"
|
||||
class="load-more-tip"
|
||||
>
|
||||
<view v-else-if="!hasMore && items.length > 0" class="load-more-tip">
|
||||
<text class="load-more-text">— 没有更多了 —</text>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="!loading && items.length === 0"
|
||||
class="load-more-tip"
|
||||
>
|
||||
<view v-else-if="!loading && items.length === 0" class="load-more-tip">
|
||||
<text class="load-more-text">暂无数据</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -203,7 +196,7 @@ const PAGE_SIZE = 10;
|
||||
const tabs = [
|
||||
{
|
||||
key: "hot",
|
||||
label: "热度榜",
|
||||
label: "点赞榜",
|
||||
icon: "/static/square/galaxy/dianzanbang.png",
|
||||
iconWidth: 64,
|
||||
iconHeight: 72,
|
||||
@ -380,7 +373,11 @@ const loadData = async ({ append = false } = {}) => {
|
||||
if (!raw) return;
|
||||
// 同步路径下已经有精确 URL(命中缓存 / 完整未过期)则无需再请求
|
||||
const instant = it.cover_url;
|
||||
if (instant && instant !== PLACEHOLDER_IMAGE && instant === getInstantAssetCoverUrl(raw)) {
|
||||
if (
|
||||
instant &&
|
||||
instant !== PLACEHOLDER_IMAGE &&
|
||||
instant === getInstantAssetCoverUrl(raw)
|
||||
) {
|
||||
// 已是精确 URL;仍然异步校准一次以处理过期(getAssetCoverRealUrl 内部命中缓存就是同步快速返回)
|
||||
}
|
||||
getAssetCoverRealUrl(raw)
|
||||
@ -447,7 +444,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
// padding: 0 9.5rpx;
|
||||
// padding: 0 9.5rpx;
|
||||
border-radius: 24rpx;
|
||||
position: relative;
|
||||
background: url("/static/square/galaxy/xbbj.png") center no-repeat;
|
||||
@ -465,7 +462,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 80rpx;
|
||||
padding: 0 64rpx;
|
||||
position: relative;
|
||||
top: 16rpx;
|
||||
margin: 0 24rpx;
|
||||
@ -476,12 +473,16 @@ onUnmounted(() => {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(184deg, rgba(255, 90, 93, 0.47) -36.55%, rgba(194, 235, 255, 0.47) 121.2%);
|
||||
filter: blur(5.849999904632568px);
|
||||
background: linear-gradient(
|
||||
184deg,
|
||||
rgba(255, 90, 93, 0.47) -36.55%,
|
||||
rgba(194, 235, 255, 0.47) 121.2%
|
||||
);
|
||||
filter: blur(5.9px);
|
||||
// opacity: 0.8;
|
||||
// Figma 用的就是 filter: blur(图形模糊),不是 backdrop-filter(背景模糊)
|
||||
// filter: blur(3.7px);
|
||||
-webkit-filter: blur(3.7px);
|
||||
-webkit-filter: blur(5.9px);
|
||||
border-top-left-radius: 14px;
|
||||
border-top-right-radius: 13px;
|
||||
border-bottom-right-radius: 8px;
|
||||
@ -491,7 +492,7 @@ filter: blur(5.849999904632568px);
|
||||
|
||||
.ranking-tab-item {
|
||||
height: 80rpx;
|
||||
width: 88rpx;
|
||||
width: 99.2rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
@ -500,7 +501,7 @@ filter: blur(5.849999904632568px);
|
||||
}
|
||||
|
||||
.ranking-tab-item.active {
|
||||
width: 96rpx;
|
||||
width: 99.2rpx;
|
||||
height: 160rpx;
|
||||
top: 40rpx;
|
||||
z-index: 1;
|
||||
@ -536,7 +537,6 @@ filter: blur(5.849999904632568px);
|
||||
.ranking-tab-icon {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
}
|
||||
|
||||
.ranking-tab-item.active .ranking-tab-label {
|
||||
@ -796,7 +796,7 @@ filter: blur(5.849999904632568px);
|
||||
/* Top 排名标签(顶到右边) */
|
||||
.top-badge {
|
||||
margin-left: auto; /* 推到右侧 */
|
||||
margin-right:8rpx;
|
||||
margin-right: 16rpx;
|
||||
min-width: 100rpx;
|
||||
height: 36rpx;
|
||||
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 |