Attachments

This commit is contained in:
2024-03-10 18:38:42 +08:00
parent 192d0c40bb
commit 38ba4d9c75
17 changed files with 328 additions and 60 deletions

View File

@ -0,0 +1,45 @@
<template>
<v-card variant="tonal" class="max-w-[540px]" :ripple="canLightbox" @click="openLightbox">
<div class="content">
<img v-if="current.type === 1" :src="getUrl(current)" />
<video v-if="current.type === 2" controls class="w-full">
<source :src="getUrl(current)"></source>
</video>
</div>
<vue-easy-lightbox teleport="#app" :visible="lightbox" :imgs="[getUrl(current)]" @hide="lightbox = false">
<template v-slot:close-btn="{ close }">
<v-btn class="fixed left-2 top-2" icon="mdi-close" variant="text" color="white" @click="close" />
</template>
</vue-easy-lightbox>
</v-card>
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
import VueEasyLightbox from "vue-easy-lightbox"
const props = defineProps<{ attachments: any[] }>()
const lightbox = ref(false)
const focus = ref(0)
const current = computed(() => props.attachments[focus.value])
const canLightbox = computed(() => current.value.type === 1)
function getUrl(item: any) {
return item.external_url ? item.external_url : `/api/attachments/o/${item.file_id}`
}
function openLightbox() {
if (canLightbox.value) {
lightbox.value = true
}
}
</script>
<style>
.vel-model {
z-index: 10;
}
</style>

View File

@ -18,6 +18,8 @@
<component :is="renderer[props.item?.model_type]" v-bind="props" />
<post-attachment v-if="props.item?.attachments" :attachments="props.item?.attachments" />
<post-reaction
size="small"
:item="props.item"
@ -34,6 +36,7 @@ import type { Component } from "vue"
import ArticleContent from "@/components/posts/ArticleContent.vue"
import MomentContent from "@/components/posts/MomentContent.vue"
import CommentContent from "@/components/posts/CommentContent.vue"
import PostAttachment from "./PostAttachment.vue"
import PostReaction from "@/components/posts/PostReaction.vue"
const props = defineProps<{ item: any; brief?: boolean }>()

View File

@ -67,6 +67,20 @@
</div>
</template>
</v-expansion-panel>
<v-expansion-panel title="Media">
<template #text>
<div class="flex justify-between items-center">
<div>
<p class="text-xs">This article attached</p>
<p class="text-lg font-medium">
{{ data.attachments.length }} attachment(s)
</p>
</div>
<v-btn size="small" icon="mdi-camera-plus" variant="text" @click="dialogs.media = true" />
</div>
</template>
</v-expansion-panel>
</v-expansion-panels>
</v-container>
</v-card-text>
@ -74,8 +88,13 @@
</v-card>
<planned-publish v-model:show="dialogs.plan" v-model:value="data.publishedAt" />
<media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
<v-snackbar v-model="success" :timeout="3000">Your article has been published.</v-snackbar>
<v-snackbar v-model="uploading" :timeout="-1">
Uploading your media, please stand by...
<v-progress-linear class="snackbar-progress" indeterminate />
</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
@ -86,8 +105,9 @@ import { request } from "@/scripts/request"
import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { reactive, ref } from "vue"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import { useRouter } from "vue-router"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import Media from "@/components/publish/parts/Media.vue"
const editor = useEditor()
@ -101,7 +121,8 @@ const data = reactive<any>({
title: "",
content: "",
description: "",
publishedAt: null
publishedAt: null,
attachments: []
})
const router = useRouter()
@ -109,10 +130,13 @@ const router = useRouter()
const error = ref<string | null>(null)
const success = ref(false)
const loading = ref(false)
const uploading = ref(false)
async function postArticle(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement
if (uploading.value) return
if (!data.content) return
if (!data.title || !data.description) return
if (!data.publishedAt) data.publishedAt = new Date().toISOString()
@ -120,7 +144,7 @@ async function postArticle(evt: SubmitEvent) {
loading.value = true
const res = await request("/api/p/articles", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` },
body: JSON.stringify(data)
})
if (res.status === 200) {
@ -149,4 +173,12 @@ async function postArticle(evt: SubmitEvent) {
.article-container {
max-width: 720px;
}
.snackbar-progress {
margin-left: -16px;
margin-right: -16px;
margin-bottom: -14px;
margin-top: 12px;
width: calc(100% + 64px);
}
</style>

View File

@ -54,8 +54,13 @@
</v-card>
<planned-publish v-model:show="dialogs.plan" v-model:value="extras.publishedAt" />
<media v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="extras.attachments" />
<v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar>
<v-snackbar v-model="uploading" :timeout="-1">
Uploading your media, please stand by...
<v-progress-linear class="snackbar-progress" indeterminate />
</v-snackbar>
<!-- @vue-ignore -->
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
@ -67,6 +72,7 @@ import { useEditor } from "@/stores/editor"
import { getAtk } from "@/stores/userinfo"
import { reactive, ref } from "vue"
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
import Media from "@/components/publish/parts/Media.vue"
const editor = useEditor()
@ -77,12 +83,14 @@ const dialogs = reactive({
})
const extras = reactive({
publishedAt: null
publishedAt: null,
attachments: []
})
const error = ref<string | null>(null)
const success = ref(false)
const loading = ref(false)
const uploading = ref(false)
async function postMoment(evt: SubmitEvent) {
const form = evt.target as HTMLFormElement
@ -91,6 +99,8 @@ async function postMoment(evt: SubmitEvent) {
if (!extras.publishedAt) data.set("published_at", new Date().toISOString())
else data.set("published_at", extras.publishedAt)
extras.attachments.forEach((item) => data.append("attachments[]", item))
loading.value = true
const res = await request("/api/p/moments", {
method: "POST",

View File

@ -0,0 +1,109 @@
<template>
<v-dialog
eager
class="max-w-[540px]"
:model-value="props.show"
@update:model-value="(val) => emits('update:show', val)"
>
<v-card title="Media management">
<template #text>
<v-file-input
prepend-icon=""
append-icon="mdi-upload"
variant="solo-filled"
label="File Picker"
v-model="picked"
:accept="['image/*', 'video/*']"
:loading="props.uploading"
@click:append="upload()"
/>
<h2 class="px-2 mb-1">Media list</h2>
<v-card variant="tonal">
<v-list>
<v-list-item v-for="item in props.value" :title="getFileName(item)">
<template #subtitle>
{{ getFileType(item) }} · {{ formatBytes(item.filesize) }}
</template>
<template #append>
<v-btn icon="mdi-delete" size="small" variant="text" color="error" />
</template>
</v-list-item>
</v-list>
</v-card>
</template>
<template #actions>
<v-btn class="ms-auto" text="Ok" @click="emits('update:show', false)"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import { ref } from "vue"
const props = defineProps<{ show: boolean; uploading: boolean; value: any[] }>()
const emits = defineEmits(["update:show", "update:uploading", "update:value"])
const picked = ref<any[]>([])
const error = ref<string | null>(null)
async function upload(file?: any) {
if (props.uploading) return
const data = new FormData()
if (!file) {
if (!picked.value) return
data.set("attachment", picked.value[0])
} else {
data.set("attachment", file)
}
emits("update:uploading", true)
const res = await request("/api/attachments", {
method: "POST",
headers: { Authorization: `Bearer ${getAtk()}` },
body: data
})
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
emits("update:value", props.value.concat([data.info]))
picked.value = []
}
emits("update:uploading", false)
}
function getFileName(item: any) {
return item.filename.replace(/\.[^/.]+$/, "")
}
function getFileType(item: any) {
switch (item.type) {
case 1:
return "Photo"
case 2:
return "Video"
case 3:
return "Audio"
default:
return "Others"
}
}
function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return "0 Bytes"
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
</script>

View File

@ -12,7 +12,7 @@
class="mt-2"
label="Publish date"
hint="Your post will hidden for public before this time. Leave blank will publish immediately"
variant="outlined"
variant="solo-filled"
type="datetime-local"
:model-value="props.value"
@update:model-value="(val) => emits('update:value', val)"

View File

@ -17,7 +17,7 @@
<div>
<v-alert type="info" variant="tonal" class="text-sm">
We just released the brand new design system and user interface!
<a class="underline" href="https://forms.office.com/r/Uh8vYmRQ8f" target="_blank">Contribute our survey</a>
<a class="underline" href="https://tally.so/r/w2NM7g" target="_blank">Take a survey</a>
</v-alert>
</div>
</div>