收藏功能基本完成

close #100
This commit is contained in:
目棃
2024-03-22 20:36:42 +08:00
parent 417e98a99c
commit 17c5d879cd
5 changed files with 389 additions and 76 deletions

View File

@@ -42,7 +42,6 @@
<img :src="card.forum.icon" :alt="card.forum.name" />
<span>{{ card.forum.name }}</span>
</div>
<!-- todo 需要测试 -->
<div v-if="props.selectMode" class="tpc-select">
<v-checkbox-btn v-model="selectedList" :value="props.modelValue.post.post_id" />
</div>
@@ -60,12 +59,23 @@ interface TPostCardProps {
selected?: string[];
}
interface TPostCardEmits {
(e: "update:selected", value: string[]): void;
}
const props = withDefaults(defineProps<TPostCardProps>(), {
selectMode: false,
});
const emits = defineEmits<TPostCardEmits>();
const isAct = ref<boolean>(false);
const card = ref<TGApp.Plugins.Mys.News.RenderCard>();
const selectedList = computed(() => props.selected);
const selectedList = computed({
get: () => props.selected,
set: (v) => {
if (v === undefined) return;
emits("update:selected", v);
},
});
onBeforeMount(() => {
card.value = getPostCard(props.modelValue);
@@ -235,7 +245,7 @@ function getPostCard(item: TGApp.Plugins.Mys.Post.FullData): TGApp.Plugins.Mys.N
.tpc-select {
position: absolute;
top: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;

View File

@@ -0,0 +1,198 @@
<template>
<TOverlay v-model="visible" hide :to-click="onCancel" blur-val="20px">
<div class="tocp-container">
<div class="tocp-title">选择分类</div>
<div class="tocp-list">
<v-list-item v-for="item in collectList" :key="item.id">
<template #prepend>
<v-list-item-action start>
<v-checkbox-btn v-model="select" :value="item.title" />
</v-list-item-action>
</template>
<template #title>{{ item.title }}</template>
<template #subtitle>{{ item.desc }}</template>
</v-list-item>
</div>
<div class="tocp-bottom">
<v-btn class="tocp-btn" rounded @click="newCollect">新建分类</v-btn>
<v-btn class="tocp-btn" rounded @click="onCancel">取消</v-btn>
<v-btn :loading="submit" class="tocp-btn" rounded @click="onSubmit">确定</v-btn>
</div>
</div>
</TOverlay>
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import TSUserCollection from "../../plugins/Sqlite/modules/userCollect";
import showConfirm from "../func/confirm";
import showSnackbar from "../func/snackbar";
import TOverlay from "../main/t-overlay.vue";
interface ToPostCollectProps {
modelValue: boolean;
post: string[];
}
interface ToPostCollectEmits {
(e: "update:modelValue", value: boolean): void;
(e: "submit"): void;
}
const props = defineProps<ToPostCollectProps>();
const emits = defineEmits<ToPostCollectEmits>();
const select = ref<string>();
const collectList = ref<TGApp.Sqlite.UserCollection.UFCollection[]>([]);
const submit = ref(false);
const visible = computed({
get: () => props.modelValue,
set: (value) => {
emits("update:modelValue", value);
},
});
function onCancel() {
visible.value = false;
}
watch(
() => props.modelValue,
async (val) => {
if (val) {
await freshData();
}
},
);
async function onSubmit(): Promise<void> {
if (!select.value) {
showSnackbar({
text: "请选择分类",
color: "error",
});
return;
}
submit.value = true;
let force = false;
const forceCheck = await showConfirm({
title: "是否保留原分类",
text: "若否则仅保留新分类",
});
if (forceCheck === false) force = true;
const check = await TSUserCollection.updatePostsCollect(props.post, select.value, force);
if (!check) {
showSnackbar({
text: "处理失败",
color: "error",
});
submit.value = false;
return;
}
showSnackbar({
text: `成功处理 ${props.post.length} 个帖子`,
color: "success",
});
submit.value = false;
visible.value = false;
emits("submit");
}
async function newCollect(): Promise<void> {
let title, desc;
const titleC = await showConfirm({
mode: "input",
title: "新建分类",
text: "请输入分类名称",
});
if (titleC === undefined || titleC === false) return;
if (titleC === "未分类") {
showSnackbar({
text: "分类名不可为未分类",
color: "error",
});
return;
}
if (collectList.value.find((i) => i.title === titleC)) {
showSnackbar({
text: "分类已存在",
color: "error",
});
return;
}
title = titleC;
const descC = await showConfirm({
mode: "input",
title: "新建分类",
text: "请输入分类描述",
});
if (descC === false) return;
if (descC === undefined) desc = title;
else desc = descC;
const res = await TSUserCollection.createCollect(title, desc);
if (res) {
showSnackbar({
text: "新建成功",
color: "success",
});
await freshData();
} else {
showSnackbar({
text: "新建失败",
color: "error",
});
}
}
async function freshData(): Promise<void> {
collectList.value = await TSUserCollection.getCollectList();
select.value = undefined;
}
</script>
<style lang="css" scoped>
.tocp-container {
display: flex;
width: 400px;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 10px;
border-radius: 10px;
background: var(--app-page-bg);
row-gap: 10px;
}
.tocp-title {
font-family: var(--font-title);
font-size: 20px;
}
.tocp-list {
display: flex;
width: 100%;
max-height: 300px;
flex-direction: column;
overflow-y: auto;
row-gap: 10px;
}
.tocp-bottom {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
margin-left: auto;
column-gap: 10px;
}
.tocp-btn {
background: var(--btn-bg-1);
color: var(--btn-text-1);
font-family: var(--font-title);
}
.dark .tocp-btn {
border: 1px solid var(--common-shadow-2);
}
</style>

View File

@@ -9,7 +9,7 @@
当前所属分类{{ postCollect.map((i) => i.collection).join(",") }}
</div>
<div v-else class="topc-cur-collect">当前所属分类未分类</div>
<div class="tpoc-collect-list">
<div class="topc-collect-list">
<v-list-item v-for="item in collectList" :key="item.id">
<template #prepend>
<v-list-item-action start>
@@ -208,8 +208,8 @@ function onCancel() {
}
.topc-post-info {
font-family: var(--font-title);
font-size: 18px;
font-weight: bold;
}
.topc-cur-collect {
@@ -218,7 +218,7 @@ function onCancel() {
word-break: break-all;
}
.tpoc-collect-list {
.topc-collect-list {
display: flex;
width: 100%;
max-height: 300px;

View File

@@ -1,6 +1,6 @@
<template>
<ToLoading v-model="loading" :title="loadingTitle" :subtitle="loadingSub" />
<div class="pc-container">
<ToLoading v-model="loading" :title="loadingTitle" :subtitle="loadingSub" />
<div class="pc-top">
<v-select
v-model="curSelect"
@@ -30,31 +30,56 @@
@click="freshOther"
title="导入其他用户收藏"
/>
<v-btn size="small" class="pc-btn" icon="mdi-pencil" @click="toSelect()" title="编辑收藏" />
<v-btn
:disabled="selectedMode"
size="small"
v-if="curSelect !== '未分类'"
class="pc-btn"
icon="mdi-info"
@click="toEdit()"
title="编辑分类"
:icon="selectedMode ? 'mdi-folder-move' : 'mdi-pencil'"
@click="toSelect()"
title="编辑收藏"
/>
<v-btn
:disabled="selectedMode"
size="small"
v-if="curSelect !== '未分类'"
class="pc-btn"
icon="mdi-plus"
@click="addCollect"
title="新建分类"
/>
<v-btn
:disabled="selectedMode || curSelect === '未分类'"
size="small"
class="pc-btn"
icon="mdi-information"
@click="toEdit()"
title="编辑分类"
/>
<v-btn
size="small"
:disabled="curSelect === '未分类'"
class="pc-btn"
icon="mdi-delete"
@click="deleteCollect()"
title="删除合集"
@click="deleteOper(false)"
:title="selectedMode ? '删除帖子分类' : '清空合集'"
/>
<v-btn
size="small"
class="pc-btn"
icon="mdi-delete-forever"
@click="deleteOper(true)"
:title="selectedMode ? '删除帖子' : '删除合集'"
/>
<v-pagination class="pc-page" v-model="page" :total-visible="view" :length="length" />
</div>
<div class="pc-posts">
<div v-for="item in getPageItems()" :key="item.post.post_id">
<TPostCard :model-value="item" :selected="selectedPost" :select-mode="selectedMode" />
<TPostCard
@update:selected="updateSelected"
:model-value="item"
:selected="selectedPost"
:select-mode="selectedMode"
/>
</div>
<ToCollectPost @submit="load" :post="selectedPost" v-model="showOverlay" />
</div>
</div>
</template>
@@ -65,6 +90,7 @@ import { computed, onBeforeMount, onMounted, ref, watch } from "vue";
import showConfirm from "../../components/func/confirm";
import showSnackbar from "../../components/func/snackbar";
import TPostCard from "../../components/main/t-postcard.vue";
import ToCollectPost from "../../components/overlay/to-collectPost.vue";
import ToLoading from "../../components/overlay/to-loading.vue";
import TGSqlite from "../../plugins/Sqlite";
import TSUserCollection from "../../plugins/Sqlite/modules/userCollect";
@@ -89,6 +115,7 @@ const view = computed(() => {
const selectedMode = ref<boolean>(false);
const selectedPost = ref<Array<string>>([]);
const showOverlay = ref(false);
onBeforeMount(async () => {
if (!(await TGSqlite.checkTableExist("UFPost"))) {
@@ -102,6 +129,10 @@ onBeforeMount(async () => {
onMounted(async () => await load());
function updateSelected(v: string[]) {
selectedPost.value = v;
}
async function load(): Promise<void> {
loadingTitle.value = "获取收藏帖子...";
loading.value = true;
@@ -116,16 +147,66 @@ async function load(): Promise<void> {
selected.value = await TSUserCollection.getCollectPostList(collections.value[0].title);
curSelect.value = collections.value[0].title;
}
selectedMode.value = false;
selectedPost.value = [];
page.value = 1;
loading.value = false;
}
function toSelect() {
if (!selectedMode.value) {
selectedPost.value = [];
selectedMode.value = true;
} else {
// todo
console.log(selectedPost.value);
selectedMode.value = false;
if (selectedPost.value.length === 0) return;
showOverlay.value = true;
}
}
async function addCollect(): Promise<void> {
let title, desc;
const titleC = await showConfirm({
mode: "input",
title: "新建分类",
text: "请输入分类名称",
});
if (titleC === undefined || titleC === false) return;
if (titleC === "未分类") {
showSnackbar({
text: "分类名不可为未分类",
color: "error",
});
return;
}
if (collections.value.find((i) => i.title === titleC)) {
showSnackbar({
text: "分类已存在",
color: "error",
});
return;
}
title = titleC;
const descC = await showConfirm({
mode: "input",
title: "新建分类",
text: "请输入分类描述",
});
if (descC === false) return;
if (descC === undefined) desc = title;
else desc = descC;
const res = await TSUserCollection.createCollect(title, desc);
if (res) {
showSnackbar({
text: "新建成功",
color: "success",
});
await load();
} else {
showSnackbar({
text: "新建失败",
color: "error",
});
}
}
@@ -144,6 +225,13 @@ async function toEdit(): Promise<void> {
text: "请输入分类标题",
input: collect.title,
});
if (cTc === false) {
showSnackbar({
text: "取消修改分类信息",
color: "cancel",
});
return;
}
if (typeof cTc !== "string") cTc = collect.title;
if (cTc === "未分类") {
showSnackbar({
@@ -194,11 +282,60 @@ async function toEdit(): Promise<void> {
await load();
}
async function deleteCollect(): Promise<void> {
async function deleteOper(forever: boolean): Promise<void> {
if (selectedMode.value) {
await deletePost(forever);
} else {
await deleteCollect(forever);
}
}
async function deletePost(force: boolean = false): Promise<void> {
if (selectedPost.value.length === 0) {
showSnackbar({
text: "未选择帖子",
color: "error",
});
return;
}
const title = force ? "删除帖子" : "移除帖子分类";
const res = await showConfirm({
title: "确定删除分类?",
text: selected.value.length > 0 ? `该分类下 ${selected.value.length} 条帖子将变为未分类` : "",
title: `确定${title}?`,
text: ` ${selectedPost.value.length} 条帖子`,
});
if (!res) {
showSnackbar({
text: "取消操作",
color: "cancel",
});
return;
}
loadingTitle.value = `正在${title}...`;
loading.value = true;
let success = 0;
for (const post of selectedPost.value) {
const check = await TSUserCollection.deletePostCollect(post, force);
if (!check) {
showSnackbar({
text: `帖子 ${post} 操作失败`,
color: "error",
});
await new Promise((resolve) => setTimeout(resolve, 1500));
} else {
success++;
}
}
loading.value = false;
showSnackbar({
text: `成功${title} ${success}`,
color: "success",
});
await load();
}
async function deleteCollect(force: boolean): Promise<void> {
const title = force ? "删除分类" : "清空分类";
const res = await showConfirm({ title: `确定${title}?` });
if (!res) {
showSnackbar({
text: "取消删除",
@@ -206,16 +343,16 @@ async function deleteCollect(): Promise<void> {
});
return;
}
const resD = await TSUserCollection.deleteCollect(curSelect.value);
const resD = await TSUserCollection.deleteCollect(curSelect.value, force);
if (resD) {
showSnackbar({
text: "删除成功",
text: `成功 ${title}`,
color: "success",
});
await load();
} else {
showSnackbar({
text: "删除失败",
text: `${title} 失败`,
color: "error",
});
}

View File

@@ -95,17 +95,20 @@ async function createCollect(title: string, desc: string): Promise<boolean> {
* @description 删除收藏合集
* @since Beta v0.4.5
* @param {string} title 收藏合集标题
* @param {boolean} force 是否强制删除
* @return {Promise<boolean>} 返回是否删除成功
*/
async function deleteCollect(title: string): Promise<boolean> {
async function deleteCollect(title: string, force: boolean): Promise<boolean> {
const db = await TGSqlite.getDB();
const sql = "SELECT id FROM UFCollection WHERE title = ?";
const res: Array<{ id: number }> = await db.select(sql, [title]);
if (res.length === 0) {
return false;
}
const deleteSql = "DELETE FROM UFCollection WHERE title = ?";
await db.execute(deleteSql, [title]);
if (force) {
const deleteSql = "DELETE FROM UFCollection WHERE title = ?";
await db.execute(deleteSql, [title]);
}
const deleteRefSql = "DELETE FROM UFMap WHERE collectionId = ?";
await db.execute(deleteRefSql, [res[0].id]);
return true;
@@ -210,25 +213,6 @@ async function addCollect(
return true;
}
/**
* @description 删除合集中的收藏
* @since Beta v0.4.5
* @param {string} postId 文章 id
* @param {string} collection 收藏合集标题
* @return {Promise<boolean>} 返回是否删除成功
*/
async function deleteCollectPost(postId: string, collection: string): Promise<boolean> {
const db = await TGSqlite.getDB();
const sql = "SELECT id FROM UFCollection WHERE title = ?";
const res: Array<{ id: number }> = await db.select(sql, [collection]);
if (res.length === 0) {
return false;
}
const deleteSql = "DELETE FROM UFMap WHERE postId = ? AND collectionId = ?";
await db.execute(deleteSql, [postId, res[0].id]);
return true;
}
/**
* @description 更新帖子信息
* @since Beta v0.4.5
@@ -275,12 +259,12 @@ async function deletePostCollect(postId: string, force: boolean = false): Promis
if (selectRes.length === 0 && !force) {
return false;
}
const deleteSql = "DELETE FROM UFMap WHERE postId = ?";
await db.execute(deleteSql, [postId]);
if (force) {
const deletePostSql = "DELETE FROM UFPost WHERE id = ?";
await db.execute(deletePostSql, [postId]);
}
const deleteSql = "DELETE FROM UFMap WHERE postId = ?";
await db.execute(deleteSql, [postId]);
return true;
}
@@ -328,13 +312,13 @@ async function updatePostCollect(postId: string, collections: string[]): Promise
* @since Beta v0.4.5
* @param {string[]} postIds 文章 id
* @param {string} collection 收藏合集标题
* @param {string} oldCollection 旧的收藏合集标题
* @param {boolean} force 是否修改的同时移除其他收藏
* @return {Promise<boolean>} 返回是否修改成功
*/
async function updatePostsCollect(
postIds: string[],
collection: string,
oldCollection: string | undefined,
force: boolean = false,
): Promise<boolean> {
const db = await TGSqlite.getDB();
const collectionSql = "SELECT * FROM UFCollection WHERE title = ?";
@@ -344,37 +328,22 @@ async function updatePostsCollect(
if (collectionRes.length === 0) {
return false;
}
let oldCollectionInfo: TGApp.Sqlite.UserCollection.UFCollection | undefined;
if (oldCollection !== undefined) {
const oldCollectionRes: TGApp.Sqlite.UserCollection.UFCollection[] = await db.select(
collectionSql,
[oldCollection],
);
if (oldCollectionRes.length === 0) {
return false;
}
oldCollectionInfo = oldCollectionRes[0];
}
for (let i = 0; i < postIds.length; i++) {
const postSql = "SELECT id,title FROM UFPost WHERE id = ?";
const postRes: Array<{ id: number; title: string }> = await db.select(postSql, [postIds[i]]);
if (postRes.length === 0) {
return false;
}
if (oldCollectionInfo !== undefined) {
const updateSql =
"UPDATE UFMap SET collectionId = ?,post=?,collection=?,desc=?,updated=? WHERE postId = ? AND collectionId = ?";
await db.execute(updateSql, [
collectionRes[0].id,
postRes[0].title,
collection,
collectionRes[0].desc,
new Date().getTime(),
postIds[i],
oldCollectionInfo.id,
]);
continue;
if (force) {
const deleteCheck = await deletePostCollect(postIds[i]);
if (!deleteCheck) return false;
}
const mapSql = "SELECT * FROM UFMap WHERE postId = ? AND collectionId = ?";
const mapRes: TGApp.Sqlite.UserCollection.UFMap[] = await db.select(mapSql, [
postIds[i],
collectionRes[0].id,
]);
if (mapRes.length > 0) continue;
const insertSql =
"INSERT INTO UFMap (postId, collectionId,post, collection, desc, updated) VALUES (?, ?, ?, ?, ?, ?)";
await db.execute(insertSql, [
@@ -399,7 +368,6 @@ const TSUserCollection = {
updateCollect,
addCollect,
updatePostInfo,
deleteCollectPost,
deletePostCollect,
updatePostCollect,
updatePostsCollect,