fix: 修复首页空白/失效外链/云函数越权,补齐缺失页面与种子数据
## 阻断性缺陷 - list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在, 而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。 按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。 - parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。 - 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。 ## 越权与数据一致性 - uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、 改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。 - comments.likeComment/relikeComment:直接采信客户端传入的 user_id, 可冒名点赞刷计数。改为以令牌为准,并纳入事务。 - comments.updateComment:对数组取 .author_id,权限判断恒失败; 字段名 updateTime 与 ip_location 类型与 schema 不符。 - comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减; 且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。 - cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效, 可无限重复刷计数;补唯一索引并事务化。 - cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。 - cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。 - article_info:get() 使用未定义变量必崩;读接口全部无鉴权。 - user-info:公开资料接口可查任意用户 last_login_ip。 ## 资源与数据 - 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403), 改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。 - 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记, 同时补上点赞、收藏、投票、浏览日志的唯一索引。 ## 功能 - 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。 原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。 - 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。 ## 工具 - tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查 - tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译) - tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过" - tools/gen-*.py:模板与图标资源生成脚本
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<view class="pages">
|
||||
<!-- 顶部搜索框:点击跳转到搜索页 -->
|
||||
<view class="nav-box">
|
||||
<view class="nav">
|
||||
<view class="uni-search-box">
|
||||
<uni-search-bar ref="searchBar" radius="100" cancelButton="none" disabled
|
||||
:placeholder="inputPlaceholder" />
|
||||
<view class="cover-search-bar" @click="searchClick"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类标签:多选,全不选时展示全部 -->
|
||||
<view class="tag-bar" v-if="tags.length">
|
||||
<uni-data-checkbox mode="tag" multiple v-model="tags_v" :localdata="tags"></uni-data-checkbox>
|
||||
</view>
|
||||
|
||||
<unicloud-db ref='udb' v-slot:default="{ pagination, hasMore, loading, error, options }" @error="onqueryerror"
|
||||
:collection="colList" :page-size="10" orderby="publish_date desc" @load="listLoad">
|
||||
<scroll-view scroll-y class="uni-list" refresher-enabled
|
||||
:refresher-triggered="loadType === 'refresh'" @refresherrefresh="refresh" @scrolltolower="loadMore">
|
||||
|
||||
<!-- 列表渲染 -->
|
||||
<template v-for="item in listData" :key="item._id">
|
||||
<not-cover v-if="item.thumbnail && item.thumbnail.length === 0" :data="getNewItem(item)">
|
||||
</not-cover>
|
||||
<right-small-cover v-else-if="item.thumbnail && item.thumbnail.length === 1"
|
||||
:data="getNewItem(item)"></right-small-cover>
|
||||
<three-cover v-else-if="item.thumbnail && item.thumbnail.length === 3" :data="getNewItem(item)">
|
||||
</three-cover>
|
||||
</template>
|
||||
|
||||
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
|
||||
<uni-load-state @networkResume="refresh"
|
||||
:state="{ data: listData, pagination, hasMore, loading, error }" @loadMore="loadMore">
|
||||
</uni-load-state>
|
||||
</scroll-view>
|
||||
</unicloud-db>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import notCover from "@/uni_modules/uni-cms-article/components/list-template/not-cover.vue";
|
||||
import rightSmallCover from "@/uni_modules/uni-cms-article/components/list-template/right-small-cover.vue";
|
||||
import threeCover from "@/uni_modules/uni-cms-article/components/list-template/three-cover.vue";
|
||||
import {
|
||||
parseImageUrl
|
||||
} from "@/uni_modules/uni-cms-article/common/parse-image-url";
|
||||
|
||||
const db = uniCloud.database();
|
||||
const articleDBName = 'uni-cms-articles'
|
||||
const userDBName = 'uni-id-users'
|
||||
const categoryFilterKey = '__cms_category_filter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
notCover,
|
||||
rightSmallCover,
|
||||
threeCover,
|
||||
},
|
||||
computed: {
|
||||
// 根据当前语言返回不同的搜索框占位符
|
||||
inputPlaceholder() {
|
||||
if (uni.getStorageSync('CURRENT_LANG') == "en") {
|
||||
return 'Please enter the search content'
|
||||
}
|
||||
return '请输入搜索内容'
|
||||
},
|
||||
// 连表查询,返回两个集合的查询结果
|
||||
colList() {
|
||||
const where = {
|
||||
article_status: 1
|
||||
};
|
||||
if (this.tags_v && this.tags_v.length) {
|
||||
where.category_id = db.command.in(this.tags_v);
|
||||
}
|
||||
return [
|
||||
db.collection(articleDBName).where(where).field(
|
||||
'thumbnail,title,publish_date,user_id,excerpt').getTemp(), // 文章集合
|
||||
db.collection(userDBName).field('_id,nickname').getTemp() // 用户集合
|
||||
]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tags_v: [],
|
||||
tags: [],
|
||||
listData: [], // 列表数据
|
||||
loadType: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.getTags();
|
||||
},
|
||||
onShow() {
|
||||
// tabBar 页面切回来时要重新应用首页传来的分类筛选
|
||||
this.applyPendingCategory();
|
||||
},
|
||||
methods: {
|
||||
// 首页「快捷分类」通过缓存传递筛选目标
|
||||
applyPendingCategory() {
|
||||
const pending = uni.getStorageSync(categoryFilterKey);
|
||||
if (!pending) return;
|
||||
uni.removeStorageSync(categoryFilterKey);
|
||||
if (this.tags_v.indexOf(pending) === -1) {
|
||||
this.tags_v = this.tags_v.concat([pending]);
|
||||
}
|
||||
},
|
||||
getNewItem(item) {
|
||||
if (!item.excerpt) {
|
||||
item.excerpt = "";
|
||||
} else if (item.excerpt.length > 50) {
|
||||
item.excerpt = item.excerpt.slice(0, 50) + " ... ...";
|
||||
}
|
||||
return item;
|
||||
},
|
||||
async getTags() {
|
||||
try {
|
||||
const res = await uniCloud.importObject("uni-cms-categories").getTags();
|
||||
if (res && String(res.code) === '200') {
|
||||
this.tags = res.tags || [];
|
||||
this.applyPendingCategory();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('分类加载失败:', e);
|
||||
}
|
||||
},
|
||||
async listLoad(data) {
|
||||
const listData = data.map(item => {
|
||||
if (typeof item.thumbnail === 'string') {
|
||||
item.thumbnail = [item.thumbnail]
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
for (const article of listData) {
|
||||
const parsed = await parseImageUrl(article.thumbnail)
|
||||
article.thumbnail = parsed.map(image => image.src)
|
||||
}
|
||||
|
||||
this.listData = this.loadType === 'loadMore' ? this.listData.concat(listData) : listData
|
||||
this.loadType = null
|
||||
},
|
||||
// 点击搜索框
|
||||
searchClick() {
|
||||
uni.hideKeyboard();
|
||||
uni.navigateTo({
|
||||
url: '/uni_modules/uni-cms-article/pages/search/search'
|
||||
});
|
||||
},
|
||||
// 刷新
|
||||
refresh() {
|
||||
this.loadType = 'refresh'
|
||||
this.$refs.udb.loadData({
|
||||
clear: true
|
||||
}, () => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
// 加载更多
|
||||
loadMore() {
|
||||
this.loadType = 'loadMore'
|
||||
this.$refs.udb.loadMore()
|
||||
},
|
||||
onqueryerror(e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
// #ifdef H5
|
||||
onPullDownRefresh() {
|
||||
this.refresh()
|
||||
},
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pages {
|
||||
background-color: #FFFFFF;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-box {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.uni-search-box {
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::v-deep .uni-searchbar {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::v-deep .uni-searchbar__box {
|
||||
height: 32px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* 覆盖层:让禁用状态的搜索框整体可点 */
|
||||
.cover-search-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.tag-bar {
|
||||
padding: 10rpx 20rpx 20rpx 20rpx;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.uni-list {
|
||||
height: calc(100vh - 160rpx);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user