chore: format code

pull/36/head
CXM 3 years ago
parent 33631a9806
commit b2a66c3788

@ -1,38 +1,29 @@
<template>
<n-config-provider :theme="theme">
<n-message-provider>
<div
class="app-container"
:class="{ dark: theme?.name === 'dark' }"
>
<div has-sider class="main-wrap" position="static">
<!-- -->
<sidebar />
<n-config-provider :theme="theme">
<n-message-provider>
<div class="app-container" :class="{ dark: theme?.name === 'dark' }">
<div has-sider class="main-wrap" position="static">
<!-- -->
<sidebar />
<div class="content-wrap">
<router-view class="app-wrap" v-slot="{ Component }">
<keep-alive>
<component
v-if="$route.meta.keepAlive"
:is="Component"
/>
</keep-alive>
<component
v-if="!$route.meta.keepAlive"
:is="Component"
/>
</router-view>
</div>
<div class="content-wrap">
<router-view v-slot="{ Component }" class="app-wrap">
<keep-alive>
<component :is="Component" v-if="$route.meta.keepAlive" />
</keep-alive>
<component :is="Component" v-if="!$route.meta.keepAlive" />
</router-view>
</div>
<!-- -->
<rightbar />
</div>
<!-- / -->
<auth />
</div>
</n-message-provider>
<n-global-style />
</n-config-provider>
<!-- -->
<rightbar />
</div>
<!-- / -->
<auth />
</div>
</n-message-provider>
<n-global-style />
</n-config-provider>
</template>
<script setup lang="ts">
@ -46,4 +37,4 @@ const theme = computed(() => (store.state.theme === 'dark' ? darkTheme : null));
<style lang="less">
@import '@/assets/css/main.less';
</style>
</style>

@ -1,100 +1,99 @@
:root {
// 如果要变更中间栏的大小,修改此处即可
--content-main: 500px;
// 如果要变更中间栏的大小,修改此处即可
--content-main: 500px;
}
.app-container {
margin: 0;
margin: 0;
.app-wrap {
width: 100%;
// max-width: 1000px;
margin: 0 auto;
}
.app-wrap {
width: 100%;
// max-width: 1000px;
margin: 0 auto;
}
}
.main-wrap {
min-height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
.content-wrap {
width: 100%;
max-width: var(--content-main);
position: relative;
}
.main-content-wrap {
margin: 0;
border-top: none;
border-radius: 0;
min-height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
.content-wrap {
width: 100%;
max-width: var(--content-main);
position: relative;
}
.main-content-wrap {
margin: 0;
border-top: none;
border-radius: 0;
.n-list-item {
padding: 0;
}
.n-list-item {
padding: 0;
}
}
}
.empty-wrap {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.hash-link,
.user-link {
color: #18a058;
text-decoration: none;
cursor: pointer;
color: #18a058;
text-decoration: none;
cursor: pointer;
&:hover {
opacity: 0.8;
}
&:hover {
opacity: 0.8;
}
}
.beian-link {
color: #333;
text-decoration: none;
color: #333;
text-decoration: none;
&:hover {
opacity: 0.75;
}
&:hover {
opacity: 0.75;
}
}
.username-link {
color: #000;
color: none;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
color: #000;
color: none;
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.dark {
.hash-link,
.user-link {
color: #63e2b7;
}
.username-link {
color: #eee;
}
.beian-link {
color: #ddd;
}
.hash-link,
.user-link {
color: #63e2b7;
}
.username-link {
color: #eee;
}
.beian-link {
color: #ddd;
}
}
@media screen and (max-width: 821px) {
.content-wrap {
top: 0;
left: 60px;
position: absolute !important;
width: calc(100% - 60px) !important;
}
}
.content-wrap {
top: 0;
left: 60px;
position: absolute !important;
width: calc(100% - 60px) !important;
}
}

@ -1,111 +1,111 @@
<template>
<n-modal
v-model:show="store.state.authModalShow"
class="auth-card"
preset="card"
size="small"
:mask-closable="false"
:bordered="false"
:style="{
width: '360px',
}"
>
<div class="auth-wrap">
<n-card :bordered="false">
<n-tabs
:default-value="store.state.authModelTab"
size="large"
justify-content="space-evenly"
>
<n-tab-pane name="signin" tab="登录">
<n-form
ref="loginRef"
:model="loginForm"
:rules="{
username: {
required: true,
message: '',
},
password: {
required: true,
message: '',
},
}"
>
<n-form-item-row label="账户" path="username">
<n-input
v-model:value="loginForm.username"
placeholder="请输入用户名"
@keyup.enter.prevent="handleLogin"
/>
</n-form-item-row>
<n-form-item-row label="密码" path="password">
<n-input
type="password"
show-password-on="mousedown"
v-model:value="loginForm.password"
placeholder="请输入账户密码"
@keyup.enter.prevent="handleLogin"
/>
</n-form-item-row>
</n-form>
<n-button
type="primary"
block
secondary
strong
:loading="loading"
@click="handleLogin"
>
</n-button>
</n-tab-pane>
<n-tab-pane name="signup" tab="注册">
<n-form
ref="registerRef"
:model="registerForm"
:rules="registerRule"
>
<n-form-item-row label="用户名" path="username">
<n-input
v-model:value="registerForm.username"
placeholder="用户名注册后无法修改"
/>
</n-form-item-row>
<n-form-item-row label="密码" path="password">
<n-input
type="password"
show-password-on="mousedown"
placeholder="密码不少于6位"
v-model:value="registerForm.password"
@keyup.enter.prevent="handleRegister"
/>
</n-form-item-row>
<n-form-item-row label="重复密码" path="repassword">
<n-input
type="password"
show-password-on="mousedown"
placeholder="请再次输入密码"
v-model:value="registerForm.repassword"
@keyup.enter.prevent="handleRegister"
/>
</n-form-item-row>
</n-form>
<n-button
type="primary"
block
secondary
strong
:loading="loading"
@click="handleRegister"
>
</n-button>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</n-modal>
<n-modal
v-model:show="store.state.authModalShow"
class="auth-card"
preset="card"
size="small"
:mask-closable="false"
:bordered="false"
:style="{
width: '360px',
}"
>
<div class="auth-wrap">
<n-card :bordered="false">
<n-tabs
:default-value="store.state.authModelTab"
size="large"
justify-content="space-evenly"
>
<n-tab-pane name="signin" tab="登录">
<n-form
ref="loginRef"
:model="loginForm"
:rules="{
username: {
required: true,
message: '',
},
password: {
required: true,
message: '',
},
}"
>
<n-form-item-row label="账户" path="username">
<n-input
v-model:value="loginForm.username"
placeholder="请输入用户名"
@keyup.enter.prevent="handleLogin"
/>
</n-form-item-row>
<n-form-item-row label="密码" path="password">
<n-input
v-model:value="loginForm.password"
type="password"
show-password-on="mousedown"
placeholder="请输入账户密码"
@keyup.enter.prevent="handleLogin"
/>
</n-form-item-row>
</n-form>
<n-button
type="primary"
block
secondary
strong
:loading="loading"
@click="handleLogin"
>
</n-button>
</n-tab-pane>
<n-tab-pane name="signup" tab="注册">
<n-form
ref="registerRef"
:model="registerForm"
:rules="registerRule"
>
<n-form-item-row label="用户名" path="username">
<n-input
v-model:value="registerForm.username"
placeholder="用户名注册后无法修改"
/>
</n-form-item-row>
<n-form-item-row label="密码" path="password">
<n-input
v-model:value="registerForm.password"
type="password"
show-password-on="mousedown"
placeholder="密码不少于6位"
@keyup.enter.prevent="handleRegister"
/>
</n-form-item-row>
<n-form-item-row label="重复密码" path="repassword">
<n-input
v-model:value="registerForm.repassword"
type="password"
show-password-on="mousedown"
placeholder="请再次输入密码"
@keyup.enter.prevent="handleRegister"
/>
</n-form-item-row>
</n-form>
<n-button
type="primary"
block
secondary
strong
:loading="loading"
@click="handleRegister"
>
</n-button>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</n-modal>
</template>
<script setup lang="ts">
@ -119,138 +119,138 @@ const store = useStore();
const loading = ref(false);
const loginRef = ref<FormInst>();
const loginForm = reactive({
username: '',
password: '',
username: '',
password: '',
});
const registerRef = ref<FormInst>();
const registerForm = reactive({
username: '',
password: '',
repassword: '',
username: '',
password: '',
repassword: '',
});
const registerRule = {
username: {
required: true,
message: '',
username: {
required: true,
message: '',
},
password: {
required: true,
message: '',
},
repassword: [
{
required: true,
message: '',
},
password: {
required: true,
message: '',
{
validator: (rule: FormItemRule, value: any) => {
return (
!!registerForm.password &&
registerForm.password.startsWith(value) &&
registerForm.password.length >= value.length
);
},
message: '',
trigger: 'input',
},
repassword: [
{
required: true,
message: '',
},
{
validator: (rule: FormItemRule, value: any) => {
return (
!!registerForm.password &&
registerForm.password.startsWith(value) &&
registerForm.password.length >= value.length
);
},
message: '',
trigger: 'input',
},
],
],
};
const handleLogin = (e: Event) => {
e.preventDefault();
e.stopPropagation();
e.preventDefault();
e.stopPropagation();
loginRef.value?.validate((errors) => {
if (!errors) {
loading.value = true;
loginRef.value?.validate((errors) => {
if (!errors) {
loading.value = true;
userLogin({
username: loginForm.username,
password: loginForm.password,
})
.then((res) => {
const token = res?.token || '';
// 写入用户信息
localStorage.setItem('PAOPAO_TOKEN', token);
userLogin({
username: loginForm.username,
password: loginForm.password,
})
.then((res) => {
const token = res?.token || '';
// 写入用户信息
localStorage.setItem('PAOPAO_TOKEN', token);
return userInfo(token);
})
.then((res) => {
window.$message.success('');
loading.value = false;
return userInfo(token);
})
.then((res) => {
window.$message.success('');
loading.value = false;
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
loginForm.username = '';
loginForm.password = '';
})
.catch((err) => {
loading.value = false;
});
}
});
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
loginForm.username = '';
loginForm.password = '';
})
.catch((err) => {
loading.value = false;
});
}
});
};
const handleRegister = (e: Event) => {
e.preventDefault();
e.stopPropagation();
e.preventDefault();
e.stopPropagation();
registerRef.value?.validate((errors) => {
if (!errors) {
loading.value = true;
registerRef.value?.validate((errors) => {
if (!errors) {
loading.value = true;
userRegister({
username: registerForm.username,
password: registerForm.password,
})
.then((res) => {
return userLogin({
username: registerForm.username,
password: registerForm.password,
});
})
.then((res) => {
const token = res?.token || '';
// 写入用户信息
localStorage.setItem('PAOPAO_TOKEN', token);
userRegister({
username: registerForm.username,
password: registerForm.password,
})
.then((res) => {
return userLogin({
username: registerForm.username,
password: registerForm.password,
});
})
.then((res) => {
const token = res?.token || '';
// 写入用户信息
localStorage.setItem('PAOPAO_TOKEN', token);
return userInfo(token);
})
.then((res) => {
window.$message.success('');
loading.value = false;
return userInfo(token);
})
.then((res) => {
window.$message.success('');
loading.value = false;
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
registerForm.username = '';
registerForm.password = '';
registerForm.repassword = '';
})
.catch((err) => {
loading.value = false;
});
}
});
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
registerForm.username = '';
registerForm.password = '';
registerForm.repassword = '';
})
.catch((err) => {
loading.value = false;
});
}
});
};
onMounted(() => {
const token = localStorage.getItem('PAOPAO_TOKEN') || '';
if (token) {
userInfo(token)
.then((res) => {
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
})
.catch((err) => {
store.commit('userLogout');
});
} else {
const token = localStorage.getItem('PAOPAO_TOKEN') || '';
if (token) {
userInfo(token)
.then((res) => {
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
})
.catch((err) => {
store.commit('userLogout');
}
});
} else {
store.commit('userLogout');
}
});
</script>
<style lang="less" scoped>
.auth-wrap {
margin-top: -30px;
margin-top: -30px;
}
</style>
</style>

@ -1,100 +1,89 @@
<template>
<div class="comment-item">
<n-thing content-indented>
<template #avatar>
<n-avatar round :size="30" :src="comment.user.avatar" />
<div class="comment-item">
<n-thing content-indented>
<template #avatar>
<n-avatar round :size="30" :src="comment.user.avatar" />
</template>
<template #header>
<span class="nickname-wrap">
<router-link
class="username-link"
:to="{
name: 'user',
query: { username: comment.user.username },
}"
@click.stop
>
{{ comment.user.nickname }}
</router-link>
</span>
<span class="username-wrap"> @{{ comment.user.username }} </span>
</template>
<template #header-extra>
<div class="opt-wrap">
<span class="timestamp">
{{ comment.ip_loc ? comment.ip_loc + ' · ' : comment.ip_loc }}
{{ formatRelativeTime(comment.created_on) }}
</span>
<n-popconfirm
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === comment.user.id
"
negative-text="取消"
positive-text="确认"
@positive-click="execDelAction"
>
<template #trigger>
<n-button quaternary circle size="tiny" class="del-btn">
<template #icon>
<n-icon>
<trash />
</n-icon>
</template>
</n-button>
</template>
<template #header>
<span class="nickname-wrap">
<router-link
@click.stop
class="username-link"
:to="{
name: 'user',
query: { username: comment.user.username },
}"
>
{{ comment.user.nickname }}
</router-link>
</span>
<span class="username-wrap">
@{{ comment.user.username }}
</span>
</template>
<template #header-extra>
<div class="opt-wrap">
<span class="timestamp">
{{
comment.ip_loc
? comment.ip_loc + ' · '
: comment.ip_loc
}}
{{ formatRelativeTime(comment.created_on) }}
</span>
<n-popconfirm
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === comment.user.id
"
negative-text="取消"
positive-text="确认"
@positive-click="execDelAction"
>
<template #trigger>
<n-button
quaternary
circle
size="tiny"
class="del-btn"
>
<template #icon>
<n-icon>
<trash />
</n-icon>
</template>
</n-button>
</template>
</n-popconfirm>
</div>
</template>
<template #description v-if="comment.texts.length > 0">
<span
v-for="content in comment.texts"
:key="content.id"
class="comment-text"
@click.stop="doClickText($event, comment.id)"
v-html="parsePostTag(content.content).content"
>
</span>
</template>
<template #footer>
<post-image :imgs="comment.imgs" />
<!-- -->
<div class="reply-wrap">
<reply-item
v-for="reply in comment.replies"
:key="reply.id"
:reply="reply"
@focusReply="focusReply"
@reload="reload"
/>
</div>
<!-- -->
<compose-reply
ref="replyComposeRef"
v-if="store.state.userInfo.id > 0"
:comment-id="comment.id"
:at-userid="replyAtUserID"
:at-username="replyAtUsername"
@reload="reload"
@reset="resetReply"
/>
</template>
</n-thing>
</div>
</n-popconfirm>
</div>
</template>
<template v-if="comment.texts.length > 0" #description>
<span
v-for="content in comment.texts"
:key="content.id"
class="comment-text"
@click.stop="doClickText($event, comment.id)"
v-html="parsePostTag(content.content).content"
>
</span>
</template>
<template #footer>
<post-image :imgs="comment.imgs" />
<!-- -->
<div class="reply-wrap">
<reply-item
v-for="reply in comment.replies"
:key="reply.id"
:reply="reply"
@focusReply="focusReply"
@reload="reload"
/>
</div>
<!-- -->
<compose-reply
v-if="store.state.userInfo.id > 0"
ref="replyComposeRef"
:comment-id="comment.id"
:at-userid="replyAtUserID"
:at-username="replyAtUsername"
@reload="reload"
@reset="resetReply"
/>
</template>
</n-thing>
</div>
</template>
<script setup lang="ts">
@ -113,137 +102,140 @@ const replyAtUsername = ref('');
const replyComposeRef = ref();
const emit = defineEmits<{
(e: "reload"): void
(e: 'reload'): void;
}>();
const props = withDefaults(defineProps<{
comment: Item.CommentProps
}>(), {})
const props = withDefaults(
defineProps<{
comment: Item.CommentProps;
}>(),
{},
);
const comment = computed(() => {
let comment: Item.CommentComponentProps = Object.assign(
{
texts: [],
imgs: [],
},
props.comment
);
comment.contents.map((content :any) => {
if (+content.type === 1 || +content.type === 2) {
comment.texts.push(content);
}
if (+content.type === 3) {
comment.imgs.push(content);
}
});
return comment;
let comment: Item.CommentComponentProps = Object.assign(
{
texts: [],
imgs: [],
},
props.comment,
);
comment.contents.map((content: any) => {
if (+content.type === 1 || +content.type === 2) {
comment.texts.push(content);
}
if (+content.type === 3) {
comment.imgs.push(content);
}
});
return comment;
});
const doClickText = (e: MouseEvent, id: number | string) => {
let _target = e.target as any;
if (_target.dataset.detail) {
const d = _target.dataset.detail.split(':');
if (d.length === 2) {
store.commit('refresh');
if (d[0] === 'tag') {
window.$message.warning('');
} else {
router.push({
name: 'user',
query: {
username: d[1],
},
});
}
}
let _target = e.target as any;
if (_target.dataset.detail) {
const d = _target.dataset.detail.split(':');
if (d.length === 2) {
store.commit('refresh');
if (d[0] === 'tag') {
window.$message.warning('');
} else {
router.push({
name: 'user',
query: {
username: d[1],
},
});
}
}
}
};
const focusReply = (reply: Item.ReplyProps) => {
replyAtUserID.value = reply.user_id;
replyAtUsername.value = reply.user?.username || '';
replyComposeRef.value?.switchReply(true);
replyAtUserID.value = reply.user_id;
replyAtUsername.value = reply.user?.username || '';
replyComposeRef.value?.switchReply(true);
};
const reload = () => {
emit('reload');
emit('reload');
};
const resetReply = () => {
replyAtUserID.value = 0;
replyAtUsername.value = '';
replyAtUserID.value = 0;
replyAtUsername.value = '';
};
const execDelAction = () => {
deleteComment({
id: comment.value.id,
deleteComment({
id: comment.value.id,
})
.then((res) => {
window.$message.success('');
setTimeout(() => {
reload();
}, 50);
})
.then((res) => {
window.$message.success('');
setTimeout(() => {
reload();
}, 50);
})
.catch((err) => {});
.catch((err) => {});
};
</script>
<style lang="less" scoped>
.comment-item {
width: 100%;
padding: 16px;
box-sizing: border-box;
.nickname-wrap {
font-size: 14px;
width: 100%;
padding: 16px;
box-sizing: border-box;
.nickname-wrap {
font-size: 14px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
}
.opt-wrap {
display: flex;
align-items: center;
.timestamp {
opacity: 0.75;
font-size: 12px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
.del-btn {
margin-left: 4px;
}
.opt-wrap {
display: flex;
align-items: center;
.timestamp {
opacity: 0.75;
font-size: 12px;
}
.del-btn {
margin-left: 4px;
}
}
.comment-text {
display: block;
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
}
.comment-text {
display: block;
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
}
}
.reply-wrap {
margin-top: 10px;
border-radius: 5px;
background: #fafafc;
.reply-item {
&:last-child {
border-bottom: none;
}
margin-top: 10px;
border-radius: 5px;
background: #fafafc;
.reply-item {
&:last-child {
border-bottom: none;
}
}
}
.dark {
.reply-wrap {
background: #18181c;
}
.reply-wrap {
background: #18181c;
}
}
</style>
</style>

@ -1,168 +1,159 @@
<template>
<div>
<div class="compose-wrap" v-if="store.state.userInfo.id > 0">
<div class="compose-line">
<div class="compose-user">
<n-avatar
round
:size="30"
:src="store.state.userInfo.avatar"
/>
</div>
<n-mention
type="textarea"
size="large"
autosize
:bordered="false"
:options="optionsRef"
:prefix="['@']"
:loading="loading"
:value="content"
:disabled="props.lock === 1"
@update:value="changeContent"
@search="handleSearch"
@focus="focusComment"
:placeholder="
props.lock === 1
? ''
: '...'
"
/>
</div>
<n-upload
v-if="showBtn"
ref="uploadRef"
abstract
list-type="image"
:multiple="true"
:max="9"
:action="uploadGateway"
:headers="{
Authorization: uploadToken,
}"
:data="{
type: uploadType,
}"
@before-upload="beforeUpload"
@finish="finishUpload"
@error="failUpload"
@remove="removeUpload"
@update:file-list="updateUpload"
>
<div class="compose-line compose-options">
<div class="attachment">
<n-upload-trigger #="{ handleClick }" abstract>
<n-button
:disabled="
(fileQueue.length > 0 &&
uploadType === 'public/video') ||
fileQueue.length === 9
"
@click="
() => {
setUploadType('public/image');
handleClick();
}
"
quaternary
circle
type="primary"
>
<template #icon>
<n-icon
size="20"
color="var(--primary-color)"
>
<image-outline />
</n-icon>
</template>
</n-button>
</n-upload-trigger>
<div>
<div v-if="store.state.userInfo.id > 0" class="compose-wrap">
<div class="compose-line">
<div class="compose-user">
<n-avatar round :size="30" :src="store.state.userInfo.avatar" />
</div>
<n-mention
type="textarea"
size="large"
autosize
:bordered="false"
:options="optionsRef"
:prefix="['@']"
:loading="loading"
:value="content"
:disabled="props.lock === 1"
:placeholder="
props.lock === 1
? ''
: '...'
"
@update:value="changeContent"
@search="handleSearch"
@focus="focusComment"
/>
</div>
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-progress
class="text-statistic"
type="circle"
:show-indicator="false"
status="success"
:stroke-width="10"
:percentage="(content.length / 200) * 100"
/>
</template>
{{ content.length }} / 200
</n-tooltip>
</div>
<n-upload
v-if="showBtn"
ref="uploadRef"
abstract
list-type="image"
:multiple="true"
:max="9"
:action="uploadGateway"
:headers="{
Authorization: uploadToken,
}"
:data="{
type: uploadType,
}"
@before-upload="beforeUpload"
@finish="finishUpload"
@error="failUpload"
@remove="removeUpload"
@update:file-list="updateUpload"
>
<div class="compose-line compose-options">
<div class="attachment">
<n-upload-trigger #="{ handleClick }" abstract>
<n-button
:disabled="
(fileQueue.length > 0 && uploadType === 'public/video') ||
fileQueue.length === 9
"
quaternary
circle
type="primary"
@click="
() => {
setUploadType('public/image');
handleClick();
}
"
>
<template #icon>
<n-icon size="20" color="var(--primary-color)">
<image-outline />
</n-icon>
</template>
</n-button>
</n-upload-trigger>
<div class="submit-wrap">
<n-button
quaternary
round
type="tertiary"
class="cancel-btn"
size="small"
@click="cancelComment"
>
</n-button>
<n-button
:loading="submitting"
@click="submitPost"
type="primary"
secondary
size="small"
round
>
</n-button>
</div>
</div>
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-progress
class="text-statistic"
type="circle"
:show-indicator="false"
status="success"
:stroke-width="10"
:percentage="(content.length / 200) * 100"
/>
</template>
{{ content.length }} / 200
</n-tooltip>
</div>
<div class="attachment-list-wrap">
<n-upload-file-list />
</div>
</n-upload>
<div class="submit-wrap">
<n-button
quaternary
round
type="tertiary"
class="cancel-btn"
size="small"
@click="cancelComment"
>
</n-button>
<n-button
:loading="submitting"
type="primary"
secondary
size="small"
round
@click="submitPost"
>
</n-button>
</div>
</div>
<div class="compose-wrap" v-else>
<div class="login-wrap">
<span class="login-banner"> </span>
</div>
<div class="login-wrap">
<n-button
strong
secondary
round
type="primary"
@click="triggerAuth('signin')"
>
</n-button>
<n-button
strong
secondary
round
type="info"
@click="triggerAuth('signup')"
>
</n-button>
</div>
<div class="attachment-list-wrap">
<n-upload-file-list />
</div>
</n-upload>
</div>
</template>
<div v-else class="compose-wrap">
<div class="login-wrap">
<span class="login-banner"> </span>
</div>
<div class="login-wrap">
<n-button
strong
secondary
round
type="primary"
@click="triggerAuth('signin')"
>
</n-button>
<n-button
strong
secondary
round
type="info"
@click="triggerAuth('signup')"
>
</n-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useStore } from 'vuex';
import { debounce } from 'lodash';
import {
ImageOutline,
VideocamOutline,
AttachOutline,
CompassOutline,
ImageOutline,
VideocamOutline,
AttachOutline,
CompassOutline,
} from '@vicons/ionicons5';
import { createComment } from '@/api/post';
import { getSuggestUsers } from '@/api/user';
@ -170,15 +161,18 @@ import { parsePostTag } from '@/utils/content';
import type { MentionOption, UploadFileInfo, UploadInst } from 'naive-ui';
const emit = defineEmits<{
(e: "post-success"): void
(e: 'post-success'): void;
}>();
const props = withDefaults(defineProps<{
lock: number,
postId: number,
}>(), {
const props = withDefaults(
defineProps<{
lock: number;
postId: number;
}>(),
{
lock: 0,
postId: 0
});
postId: 0,
},
);
const store = useStore();
@ -197,232 +191,235 @@ const uploadToken = ref();
// 加载at用户列表
const loadSuggestionUsers = debounce((k) => {
getSuggestUsers({
k,
})
.then((res) => {
let options: MentionOption[] = [];
res.map((i) => {
options.push({
label: i,
value: i,
});
});
optionsRef.value = options;
loading.value = false;
})
.catch((err) => {
loading.value = false;
getSuggestUsers({
k,
})
.then((res) => {
let options: MentionOption[] = [];
res.map((i) => {
options.push({
label: i,
value: i,
});
});
optionsRef.value = options;
loading.value = false;
})
.catch((err) => {
loading.value = false;
});
}, 200);
const handleSearch = (k: string, prefix: string) => {
if (loading.value) {
return;
}
loading.value = true;
if (prefix === '@') {
loadSuggestionUsers(k);
}
if (loading.value) {
return;
}
loading.value = true;
if (prefix === '@') {
loadSuggestionUsers(k);
}
};
const changeContent = (v: string) => {
if (v.length > 200) {
return;
}
content.value = v;
if (v.length > 200) {
return;
}
content.value = v;
};
const setUploadType = (type: string) => {
uploadType.value = type;
uploadType.value = type;
};
const updateUpload = (list: UploadFileInfo[]) => {
fileQueue.value = list;
fileQueue.value = list;
};
const beforeUpload = async (data: any) => {
// 图片类型校验
if (
uploadType.value === 'public/image' &&
!['image/png', 'image/jpg', 'image/jpeg', 'image/gif'].includes(
(data.file as any).file?.type
)
) {
window.$message.warning(' png/jpg/gif ');
return false;
}
// 图片类型校验
if (
uploadType.value === 'public/image' &&
!['image/png', 'image/jpg', 'image/jpeg', 'image/gif'].includes(
(data.file as any).file?.type,
)
) {
window.$message.warning(' png/jpg/gif ');
return false;
}
if (uploadType.value === 'image' && (data.file as any).file?.size > 10485760) {
window.$message.warning('10MB');
return false;
}
if (
uploadType.value === 'image' &&
(data.file as any).file?.size > 10485760
) {
window.$message.warning('10MB');
return false;
}
return true;
return true;
};
const finishUpload = ({ file, event }: any): any => {
try {
let data = JSON.parse(event.target?.response);
try {
let data = JSON.parse(event.target?.response);
if (data.code === 0) {
if (uploadType.value === 'public/image') {
imageContents.value.push({
id: file.id,
content: data.data.content,
} as Item.CommentItemProps);
}
}
} catch (error) {
window.$message.error('');
if (data.code === 0) {
if (uploadType.value === 'public/image') {
imageContents.value.push({
id: file.id,
content: data.data.content,
} as Item.CommentItemProps);
}
}
} catch (error) {
window.$message.error('');
}
};
const failUpload = ({ file, event }: any): any => {
try {
let data = JSON.parse(event.target?.response);
try {
let data = JSON.parse(event.target?.response);
if (data.code !== 0) {
let errMsg = data.msg || '';
if (data.details && data.details.length > 0) {
data.details.map((detail: string) => {
errMsg += ':' + detail;
});
}
window.$message.error(errMsg);
}
} catch (error) {
window.$message.error('');
if (data.code !== 0) {
let errMsg = data.msg || '';
if (data.details && data.details.length > 0) {
data.details.map((detail: string) => {
errMsg += ':' + detail;
});
}
window.$message.error(errMsg);
}
} catch (error) {
window.$message.error('');
}
};
const removeUpload = ({ file }: any) => {
let idx = imageContents.value.findIndex((item) => item.id === file.id);
if (idx > -1) {
imageContents.value.splice(idx, 1);
}
let idx = imageContents.value.findIndex((item) => item.id === file.id);
if (idx > -1) {
imageContents.value.splice(idx, 1);
}
};
const focusComment = () => {
showBtn.value = true;
showBtn.value = true;
};
const cancelComment = () => {
showBtn.value = false;
// 置空
uploadRef.value?.clear();
fileQueue.value = [];
content.value = '';
imageContents.value = [];
showBtn.value = false;
// 置空
uploadRef.value?.clear();
fileQueue.value = [];
content.value = '';
imageContents.value = [];
};
// 发布动态
const submitPost = () => {
if (content.value.trim().length === 0) {
window.$message.warning('');
return;
}
if (content.value.trim().length === 0) {
window.$message.warning('');
return;
}
// 解析用户at
let { users } = parsePostTag(content.value);
// 解析用户at
let { users } = parsePostTag(content.value);
const contents = [];
let sort = 100;
const contents = [];
let sort = 100;
contents.push({
content: content.value,
type: 2, // 文字
sort,
});
imageContents.value.map((img) => {
sort++;
contents.push({
content: content.value,
type: 2, // 文字
sort,
});
imageContents.value.map((img) => {
sort++;
contents.push({
content: img.content,
type: 3, // 图片
sort,
});
content: img.content,
type: 3, // 图片
sort,
});
});
submitting.value = true;
createComment({
contents,
post_id: props.postId,
users: Array.from(new Set(users)),
})
.then((res) => {
window.$message.success('');
submitting.value = false;
emit('post-success');
submitting.value = true;
createComment({
contents,
post_id: props.postId,
users: Array.from(new Set(users)),
})
.then((res) => {
window.$message.success('');
submitting.value = false;
emit('post-success');
// 置空
cancelComment();
})
.catch((err) => {
submitting.value = false;
});
// 置空
cancelComment();
})
.catch((err) => {
submitting.value = false;
});
};
const triggerAuth = (key: string) => {
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', key);
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', key);
};
onMounted(() => {
uploadToken.value = 'Bearer ' + localStorage.getItem('PAOPAO_TOKEN');
uploadToken.value = 'Bearer ' + localStorage.getItem('PAOPAO_TOKEN');
});
</script>
<style lang="less" scoped>
.compose-wrap {
width: 100%;
padding: 16px;
box-sizing: border-box;
width: 100%;
padding: 16px;
box-sizing: border-box;
.compose-line {
display: flex;
flex-direction: row;
.compose-line {
display: flex;
flex-direction: row;
.compose-user {
width: 42px;
height: 42px;
display: flex;
align-items: center;
}
.compose-user {
width: 42px;
height: 42px;
display: flex;
align-items: center;
}
&.compose-options {
margin-top: 6px;
padding-left: 42px;
display: flex;
justify-content: space-between;
&.compose-options {
margin-top: 6px;
padding-left: 42px;
display: flex;
justify-content: space-between;
.submit-wrap {
display: flex;
align-items: center;
.submit-wrap {
display: flex;
align-items: center;
.cancel-btn {
margin-right: 8px;
}
}
.cancel-btn {
margin-right: 8px;
}
}
}
.login-wrap {
display: flex;
justify-content: center;
width: 100%;
.login-banner {
margin-bottom: 12px;
opacity: 0.8;
}
button {
margin: 0 4px;
}
}
.login-wrap {
display: flex;
justify-content: center;
width: 100%;
.login-banner {
margin-bottom: 12px;
opacity: 0.8;
}
button {
margin: 0 4px;
}
}
}
.attachment {
display: flex;
align-items: center;
.text-statistic {
margin-left: 8px;
width: 18px;
height: 18px;
transform: rotate(180deg);
}
display: flex;
align-items: center;
.text-statistic {
margin-left: 8px;
width: 18px;
height: 18px;
transform: rotate(180deg);
}
}
.attachment-list-wrap {
margin-top: 12px;
margin-left: 42px;
.n-upload-file-info__thumbnail {
overflow: hidden;
}
margin-top: 12px;
margin-left: 42px;
.n-upload-file-info__thumbnail {
overflow: hidden;
}
}
</style>
</style>

@ -1,41 +1,39 @@
<template>
<div class="reply-compose-wrap">
<div class="reply-switch">
<span class="show" v-if="!showReply" @click="switchReply(true)">
</span>
<span class="hide" v-if="showReply" @click="switchReply(false)">
</span>
</div>
<div class="reply-compose-wrap">
<div class="reply-switch">
<span v-if="!showReply" class="show" @click="switchReply(true)">
</span>
<span v-if="showReply" class="hide" @click="switchReply(false)">
</span>
</div>
<div class="reply-input-wrap" v-if="showReply">
<n-input-group>
<n-input
ref="inputInstRef"
size="small"
:placeholder="
props.atUsername
? '@' + props.atUsername
: '..'
"
maxlength="100"
v-model:value="replyContent"
show-count
clearable
/>
<n-button
type="primary"
size="small"
ghost
:loading="submitting"
@click="submitReply"
>
</n-button>
</n-input-group>
</div>
<div v-if="showReply" class="reply-input-wrap">
<n-input-group>
<n-input
ref="inputInstRef"
v-model:value="replyContent"
size="small"
:placeholder="
props.atUsername ? '@' + props.atUsername : '..'
"
maxlength="100"
show-count
clearable
/>
<n-button
type="primary"
size="small"
ghost
:loading="submitting"
@click="submitReply"
>
</n-button>
</n-input-group>
</div>
</div>
</template>
<script setup lang="ts">
@ -43,70 +41,73 @@ import { ref } from 'vue';
import { createCommentReply } from '@/api/post';
import { InputInst } from 'naive-ui';
const props = withDefaults(defineProps<{
commentId: number,
atUserid: number,
atUsername: string,
}>(), {
const props = withDefaults(
defineProps<{
commentId: number;
atUserid: number;
atUsername: string;
}>(),
{
commentId: 0,
atUserid: 0,
atUsername: ""
});
atUsername: '',
},
);
const emit = defineEmits<{
(e: "reload"): void,
(e: "reset"): void
(e: 'reload'): void;
(e: 'reset'): void;
}>();
const inputInstRef = ref<InputInst>();
const showReply = ref(false);
const replyContent = ref('');
const submitting = ref(false);
const switchReply = (status: boolean) => {
showReply.value = status;
showReply.value = status;
if (status) {
setTimeout(() => {
inputInstRef.value?.focus();
}, 10);
} else {
submitting.value = false;
replyContent.value = '';
emit('reset');
}
if (status) {
setTimeout(() => {
inputInstRef.value?.focus();
}, 10);
} else {
submitting.value = false;
replyContent.value = '';
emit('reset');
}
};
const submitReply = () => {
submitting.value = true;
createCommentReply({
comment_id: props.commentId,
at_user_id: props.atUserid,
content: replyContent.value,
submitting.value = true;
createCommentReply({
comment_id: props.commentId,
at_user_id: props.atUserid,
content: replyContent.value,
})
.then((res) => {
switchReply(false);
window.$message.success('');
emit('reload');
})
.then((res) => {
switchReply(false);
window.$message.success('');
emit('reload');
})
.catch((err) => {
submitting.value = false;
});
.catch((err) => {
submitting.value = false;
});
};
defineExpose({ switchReply });
</script>
<style lang="less" scoped>
.reply-compose-wrap {
.reply-switch {
text-align: right;
font-size: 12px;
margin: 10px 0;
.reply-switch {
text-align: right;
font-size: 12px;
margin: 10px 0;
.show {
color: #18a058;
cursor: pointer;
}
.hide {
opacity: 0.75;
cursor: pointer;
}
.show {
color: #18a058;
cursor: pointer;
}
.hide {
opacity: 0.75;
cursor: pointer;
}
}
}
</style>
</style>

File diff suppressed because it is too large Load Diff

@ -1,35 +1,35 @@
<template>
<n-card size="small" :bordered="true" class="nav-title-card">
<template #header>
<div class="navbar">
<n-button
class="back-btn"
v-if="back"
@click="goBack"
quaternary
circle
size="small"
>
<template #icon>
<n-icon><chevron-left-round /></n-icon>
</template>
</n-button>
<n-card size="small" :bordered="true" class="nav-title-card">
<template #header>
<div class="navbar">
<n-button
v-if="back"
class="back-btn"
quaternary
circle
size="small"
@click="goBack"
>
<template #icon>
<n-icon><chevron-left-round /></n-icon>
</template>
</n-button>
{{ props.title }}
{{ props.title }}
<n-switch
:value="store.state.theme === 'dark'"
@update:value="switchTheme"
size="small"
class="theme-switch-wrap"
>
<template #icon>
<dark-mode-outlined />
</template>
</n-switch>
</div>
</template>
</n-card>
<n-switch
:value="store.state.theme === 'dark'"
size="small"
class="theme-switch-wrap"
@update:value="switchTheme"
>
<template #icon>
<dark-mode-outlined />
</template>
</n-switch>
</div>
</template>
</n-card>
</template>
<script setup lang="ts">
@ -43,72 +43,72 @@ const store = useStore();
const router = useRouter();
const props = withDefaults(
defineProps<{
title: string;
back?: boolean;
}>(),
{
title: '',
back: false,
}
defineProps<{
title: string;
back?: boolean;
}>(),
{
title: '',
back: false,
},
);
const switchTheme = (theme: boolean) => {
if (theme) {
localStorage.setItem('PAOPAO_THEME', 'dark');
store.commit('triggerTheme', 'dark');
} else {
localStorage.setItem('PAOPAO_THEME', 'light');
store.commit('triggerTheme', 'light');
}
if (theme) {
localStorage.setItem('PAOPAO_THEME', 'dark');
store.commit('triggerTheme', 'dark');
} else {
localStorage.setItem('PAOPAO_THEME', 'light');
store.commit('triggerTheme', 'light');
}
};
const goBack = () => {
if (window.history.length <= 1) {
router.push({
path: '/',
});
} else {
router.go(-1);
}
if (window.history.length <= 1) {
router.push({
path: '/',
});
} else {
router.go(-1);
}
};
onMounted(() => {
if (!localStorage.getItem('PAOPAO_THEME')) {
switchTheme((useOsTheme() as unknown as string) === 'dark');
}
if (!localStorage.getItem('PAOPAO_THEME')) {
switchTheme((useOsTheme() as unknown as string) === 'dark');
}
});
</script>
<style lang="less">
.nav-title-card {
z-index: 99;
width: 100%;
top: 0;
position: sticky;
border-radius: 0;
border-bottom: 0;
background-color: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(12px);
z-index: 99;
width: 100%;
top: 0;
position: sticky;
border-radius: 0;
border-bottom: 0;
background-color: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(12px);
.navbar {
height: 30px;
position: relative;
display: flex;
align-items: center;
.navbar {
height: 30px;
position: relative;
display: flex;
align-items: center;
.back-btn {
margin-right: 8px;
}
.back-btn {
margin-right: 8px;
}
.theme-switch-wrap {
position: absolute;
right: 0;
top: calc(50% - 9px);
}
.theme-switch-wrap {
position: absolute;
right: 0;
top: calc(50% - 9px);
}
}
}
.dark {
.nav-title-card {
background-color: rgba(16, 16, 20, 0.75);
}
.nav-title-card {
background-color: rgba(16, 16, 20, 0.75);
}
}
</style>
</style>

@ -1,72 +1,70 @@
<template>
<div
class="message-item"
:class="{ unread: message.is_read === 0 }"
@click="handleReadMessage(message)"
>
<n-thing content-indented>
<template #header>
<div class="sender-wrap">
<n-avatar
:size="22"
:src="
message.sender_user.id > 0
? message.sender_user.avatar
: defaultavatar
"
/>
<span class="nickname" v-if="message.sender_user.id > 0">
<router-link
@click.stop
class="username-link"
:to="{
name: 'user',
query: {
username: message.sender_user.username,
},
}"
>
{{ message.sender_user.nickname }}
</router-link>
<span class="username">
@{{ message.sender_user.username }}
</span>
</span>
<span class="nickname" v-else> </span>
</div>
</template>
<template #header-extra>
<span class="timestamp">
<n-badge v-if="message.is_read === 0" dot processing />
<span class="timestamp-txt">
{{ formatRelativeTime(message.created_on) }}
</span>
</span>
</template>
<template #description>
<n-alert
:show-icon="false"
class="breif-wrap"
:type="message.is_read > 0 ? 'default' : 'success'"
>
<div class="breif-content">
{{ message.breif }}
<span
v-if="message.type !== 4"
@click.stop="viewDetail(message)"
class="hash-link view-link"
>
<n-icon><share-outline /></n-icon>
</span>
</div>
<div
class="message-item"
:class="{ unread: message.is_read === 0 }"
@click="handleReadMessage(message)"
>
<n-thing content-indented>
<template #header>
<div class="sender-wrap">
<n-avatar
:size="22"
:src="
message.sender_user.id > 0
? message.sender_user.avatar
: defaultavatar
"
/>
<span v-if="message.sender_user.id > 0" class="nickname">
<router-link
class="username-link"
:to="{
name: 'user',
query: {
username: message.sender_user.username,
},
}"
@click.stop
>
{{ message.sender_user.nickname }}
</router-link>
<span class="username"> @{{ message.sender_user.username }} </span>
</span>
<span v-else class="nickname"> </span>
</div>
</template>
<template #header-extra>
<span class="timestamp">
<n-badge v-if="message.is_read === 0" dot processing />
<span class="timestamp-txt">
{{ formatRelativeTime(message.created_on) }}
</span>
</span>
</template>
<template #description>
<n-alert
:show-icon="false"
class="breif-wrap"
:type="message.is_read > 0 ? 'default' : 'success'"
>
<div class="breif-content">
{{ message.breif }}
<span
v-if="message.type !== 4"
class="hash-link view-link"
@click.stop="viewDetail(message)"
>
<n-icon><share-outline /></n-icon>
</span>
</div>
<div v-if="message.type === 4" class="whisper-content-wrap">
{{ message.content }}
</div>
</n-alert>
</template>
</n-thing>
</div>
<div v-if="message.type === 4" class="whisper-content-wrap">
{{ message.content }}
</div>
</n-alert>
</template>
</n-thing>
</div>
</template>
<script setup lang="ts">
@ -76,99 +74,99 @@ import { readMessage } from '@/api/user';
import { formatRelativeTime } from '@/utils/formatTime';
const defaultavatar =
'https://assets.paopao.info/public/avatar/default/admin.png';
'https://assets.paopao.info/public/avatar/default/admin.png';
const router = useRouter();
const props = withDefaults(
defineProps<{
message: Item.MessageProps;
}>(),
{}
defineProps<{
message: Item.MessageProps;
}>(),
{},
);
const viewDetail = (message: Item.MessageProps) => {
handleReadMessage(message);
if (message.type === 1 || message.type === 2 || message.type === 3) {
if (message.post && message.post.id > 0) {
router.push({
name: 'post',
query: {
id: message.post_id,
},
});
} else {
window.$message.error('');
}
handleReadMessage(message);
if (message.type === 1 || message.type === 2 || message.type === 3) {
if (message.post && message.post.id > 0) {
router.push({
name: 'post',
query: {
id: message.post_id,
},
});
} else {
window.$message.error('');
}
}
};
const handleReadMessage = (message: Item.MessageProps) => {
if (message.is_read === 0) {
readMessage({
id: message.id,
})
.then((res) => {
message.is_read = 1;
})
.catch((err) => {
console.log(err);
});
}
if (message.is_read === 0) {
readMessage({
id: message.id,
})
.then((res) => {
message.is_read = 1;
})
.catch((err) => {
console.log(err);
});
}
};
</script>
<style lang="less" scoped>
.message-item {
padding: 16px;
padding: 16px;
&.unread {
background: #fcfffc;
}
&.unread {
background: #fcfffc;
}
.sender-wrap {
display: flex;
align-items: center;
.sender-wrap {
display: flex;
align-items: center;
.nickname {
margin-left: 10px;
}
.username {
opacity: 0.75;
font-size: 14px;
}
.nickname {
margin-left: 10px;
}
.timestamp {
opacity: 0.75;
font-size: 12px;
display: flex;
align-items: center;
.timestamp-txt {
margin-left: 6px;
}
.username {
opacity: 0.75;
font-size: 14px;
}
}
.breif-wrap {
margin-top: 10px;
.breif-content {
display: flex;
width: 100%;
}
.whisper-content-wrap {
margin-top: 12px;
text-decoration: underline;
}
.timestamp {
opacity: 0.75;
font-size: 12px;
display: flex;
align-items: center;
.timestamp-txt {
margin-left: 6px;
}
}
.view-link {
margin-left: 8px;
display: flex;
align-items: center;
.breif-wrap {
margin-top: 10px;
.breif-content {
display: flex;
width: 100%;
}
.whisper-content-wrap {
margin-top: 12px;
text-decoration: underline;
}
}
.view-link {
margin-left: 8px;
display: flex;
align-items: center;
}
}
.dark {
.message-item {
&.unread {
background: #0f180b;
}
.message-item {
&.unread {
background: #0f180b;
}
}
}
</style>
</style>

@ -1,27 +1,30 @@
<template>
<div class="skeleton-item" v-for="i in new Array(num)" :key="i">
<div class="content">
<n-skeleton text :repeat="2" />
<n-skeleton text style="width: 60%" />
</div>
<div v-for="i in new Array(num)" :key="i" class="skeleton-item">
<div class="content">
<n-skeleton text :repeat="2" />
<n-skeleton text style="width: 60%" />
</div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
num: number
}>(), {
num: 1
});
const props = withDefaults(
defineProps<{
num: number;
}>(),
{
num: 1,
},
);
</script>
<style lang="less" scoped>
.skeleton-item {
padding: 12px;
display: flex;
padding: 12px;
display: flex;
.content {
width: 100%;
}
.content {
width: 100%;
}
}
</style>
</style>

@ -1,38 +1,38 @@
<template>
<div class="attachment-wrap">
<div
class="attach-item"
v-for="attachment in attachments"
:key="attachment.id"
>
<n-button
@click.stop="download(attachment)"
type="primary"
size="tiny"
dashed
>
<template #icon>
<n-icon>
<cloud-download-outline />
</n-icon>
</template>
{{ attachment.type === 8 ? '' : '' }}
</n-button>
</div>
<!-- -->
<n-modal
v-model:show="showDownloadModal"
:mask-closable="false"
preset="dialog"
title="下载提示"
:content="downloadTip"
positive-text="确认下载"
negative-text="取消"
icon-placement="top"
@positive-click="execDownloadAction"
/>
<div class="attachment-wrap">
<div
v-for="attachment in attachments"
:key="attachment.id"
class="attach-item"
>
<n-button
type="primary"
size="tiny"
dashed
@click.stop="download(attachment)"
>
<template #icon>
<n-icon>
<cloud-download-outline />
</n-icon>
</template>
{{ attachment.type === 8 ? '' : '' }}
</n-button>
</div>
<!-- -->
<n-modal
v-model:show="showDownloadModal"
:mask-closable="false"
preset="dialog"
title="下载提示"
:content="downloadTip"
positive-text="确认下载"
negative-text="取消"
icon-placement="top"
@positive-click="execDownloadAction"
/>
</div>
</template>
<script setup lang="ts">
@ -41,71 +41,67 @@ import { CloudDownloadOutline } from '@vicons/ionicons5';
import { precheckAttachment, getAttachment } from '@/api/user';
const props = withDefaults(
defineProps<{
attachments: Item.PostItemProps[];
price?: number;
}>(),
{
attachments: () => [],
price: 0,
}
defineProps<{
attachments: Item.PostItemProps[];
price?: number;
}>(),
{
attachments: () => [],
price: 0,
},
);
const showDownloadModal = ref(false);
const downloadTip = ref<any>('');
const attachmentID = ref(0);
const download = (attachment: Item.PostItemProps) => {
showDownloadModal.value = true;
attachmentID.value = attachment.id;
showDownloadModal.value = true;
attachmentID.value = attachment.id;
downloadTip.value = '';
if (attachment.type === 8) {
downloadTip.value = () =>
h('div', {}, [
h(
'p',
{},
'' +
(props.price / 100).toFixed(2) +
'元',
),
]);
downloadTip.value = '';
if (attachment.type === 8) {
downloadTip.value = () =>
precheckAttachment({
id: attachmentID.value,
})
.then((res) => {
if (res.paid) {
downloadTip.value = () =>
h('div', {}, [
h(
'p',
{},
'' +
(props.price / 100).toFixed(2) +
'元'
),
h('p', {}, ''),
]);
precheckAttachment({
id: attachmentID.value,
})
.then((res) => {
if (res.paid) {
downloadTip.value = () =>
h('div', {}, [
h(
'p',
{},
''
),
]);
}
})
.catch((err) => {
showDownloadModal.value = false;
});
}
}
})
.catch((err) => {
showDownloadModal.value = false;
});
}
};
const execDownloadAction = () => {
getAttachment({
id: attachmentID.value,
getAttachment({
id: attachmentID.value,
})
.then((res) => {
window.open(res.replace('http://', 'https://'), '_blank');
})
.then((res) => {
window.open(res.replace('http://', 'https://'), '_blank');
})
.catch((err) => {
console.log(err);
});
.catch((err) => {
console.log(err);
});
};
</script>
<style lang="less" scoped>
.attach-item {
margin: 10px 0;
margin: 10px 0;
}
</style>
</style>

@ -1,155 +1,145 @@
<template>
<div class="detail-item" @click="goPostDetail(post.id)">
<n-thing>
<template #avatar>
<n-avatar round :size="30" :src="post.user.avatar" />
</template>
<template #header>
<router-link
@click.stop
class="username-link"
:to="{
name: 'user',
query: { username: post.user.username },
}"
>
{{ post.user.nickname }}
</router-link>
<span class="username-wrap"> @{{ post.user.username }} </span>
</template>
<template #header-extra>
<div
class="options"
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === post.user.id
"
>
<n-dropdown
placement="bottom-end"
trigger="click"
size="small"
:options="adminOptions"
@select="handlePostAction"
>
<n-button quaternary circle>
<template #icon>
<n-icon>
<more-horiz-filled />
</n-icon>
</template>
</n-button>
</n-dropdown>
</div>
<div class="detail-item" @click="goPostDetail(post.id)">
<n-thing>
<template #avatar>
<n-avatar round :size="30" :src="post.user.avatar" />
</template>
<template #header>
<router-link
class="username-link"
:to="{
name: 'user',
query: { username: post.user.username },
}"
@click.stop
>
{{ post.user.nickname }}
</router-link>
<span class="username-wrap"> @{{ post.user.username }} </span>
</template>
<template #header-extra>
<div
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === post.user.id
"
class="options"
>
<n-dropdown
placement="bottom-end"
trigger="click"
size="small"
:options="adminOptions"
@select="handlePostAction"
>
<n-button quaternary circle>
<template #icon>
<n-icon>
<more-horiz-filled />
</n-icon>
</template>
</n-button>
</n-dropdown>
</div>
<!-- -->
<n-modal
v-model:show="showDelModal"
:mask-closable="false"
preset="dialog"
title="提示"
content="确定删除该泡泡动态吗?"
positive-text="确认"
negative-text="取消"
@positive-click="execDelAction"
/>
<!-- -->
<n-modal
v-model:show="showLockModal"
:mask-closable="false"
preset="dialog"
title="提示"
:content="
'' +
(post.is_lock ? '' : '') +
''
"
positive-text="确认"
negative-text="取消"
@positive-click="execLockAction"
/>
<!-- -->
<n-modal
v-model:show="showStickModal"
:mask-closable="false"
preset="dialog"
title="提示"
:content="
'' +
(post.is_top ? '' : '') +
''
"
positive-text="确认"
negative-text="取消"
@positive-click="execStickAction"
/>
</template>
<div v-if="post.texts.length > 0">
<span
v-for="content in post.texts"
:key="content.id"
class="post-text"
@click.stop="doClickText($event, post.id)"
v-html="parsePostTag(content.content).content"
>
</span>
</div>
<!-- -->
<n-modal
v-model:show="showDelModal"
:mask-closable="false"
preset="dialog"
title="提示"
content="确定删除该泡泡动态吗?"
positive-text="确认"
negative-text="取消"
@positive-click="execDelAction"
/>
<!-- -->
<n-modal
v-model:show="showLockModal"
:mask-closable="false"
preset="dialog"
title="提示"
:content="
'' + (post.is_lock ? '' : '') + ''
"
positive-text="确认"
negative-text="取消"
@positive-click="execLockAction"
/>
<!-- -->
<n-modal
v-model:show="showStickModal"
:mask-closable="false"
preset="dialog"
title="提示"
:content="
'' + (post.is_top ? '' : '') + ''
"
positive-text="确认"
negative-text="取消"
@positive-click="execStickAction"
/>
</template>
<div v-if="post.texts.length > 0">
<span
v-for="content in post.texts"
:key="content.id"
class="post-text"
@click.stop="doClickText($event, post.id)"
v-html="parsePostTag(content.content).content"
>
</span>
</div>
<template #footer>
<post-attachment :attachments="post.attachments" />
<post-attachment
:attachments="post.charge_attachments"
:price="post.attachment_price"
/>
<post-image :imgs="post.imgs" />
<post-video :videos="post.videos" :full="true" />
<post-link :links="post.links" />
<div class="timestamp">
{{ formatRelativeTime(post.created_on) }}
<span v-if="post.ip_loc">
<n-divider vertical />
{{ post.ip_loc }}
</span>
<span v-if="post.created_on != post.latest_replied_on">
<n-divider vertical />
{{ formatRelativeTime(post.latest_replied_on) }}
</span>
</div>
</template>
<template #action>
<div class="opts-wrap">
<n-space justify="space-between">
<div
class="opt-item hover"
@click.stop="handlePostStar"
>
<n-icon size="20" class="opt-item-icon">
<heart-outline v-if="!hasStarred" />
<heart v-if="hasStarred" color="red" />
</n-icon>
{{ post.upvote_count }}
</div>
<div class="opt-item">
<n-icon size="20" class="opt-item-icon">
<chatbox-outline />
</n-icon>
{{ post.comment_count }}
</div>
<div
class="opt-item hover"
@click.stop="handlePostCollection"
>
<n-icon size="20" class="opt-item-icon">
<bookmark-outline v-if="!hasCollected" />
<bookmark v-if="hasCollected" color="#ff7600" />
</n-icon>
{{ post.collection_count }}
</div>
</n-space>
</div>
</template>
</n-thing>
</div>
<template #footer>
<post-attachment :attachments="post.attachments" />
<post-attachment
:attachments="post.charge_attachments"
:price="post.attachment_price"
/>
<post-image :imgs="post.imgs" />
<post-video :videos="post.videos" :full="true" />
<post-link :links="post.links" />
<div class="timestamp">
{{ formatRelativeTime(post.created_on) }}
<span v-if="post.ip_loc">
<n-divider vertical />
{{ post.ip_loc }}
</span>
<span v-if="post.created_on != post.latest_replied_on">
<n-divider vertical />
{{ formatRelativeTime(post.latest_replied_on) }}
</span>
</div>
</template>
<template #action>
<div class="opts-wrap">
<n-space justify="space-between">
<div class="opt-item hover" @click.stop="handlePostStar">
<n-icon size="20" class="opt-item-icon">
<heart-outline v-if="!hasStarred" />
<heart v-if="hasStarred" color="red" />
</n-icon>
{{ post.upvote_count }}
</div>
<div class="opt-item">
<n-icon size="20" class="opt-item-icon">
<chatbox-outline />
</n-icon>
{{ post.comment_count }}
</div>
<div class="opt-item hover" @click.stop="handlePostCollection">
<n-icon size="20" class="opt-item-icon">
<bookmark-outline v-if="!hasCollected" />
<bookmark v-if="hasCollected" color="#ff7600" />
</n-icon>
{{ post.collection_count }}
</div>
</n-space>
</div>
</template>
</n-thing>
</div>
</template>
<script setup lang="ts">
@ -159,21 +149,21 @@ import { useRouter } from 'vue-router';
import { formatRelativeTime } from '@/utils/formatTime';
import { parsePostTag } from '@/utils/content';
import {
Heart,
HeartOutline,
Bookmark,
BookmarkOutline,
ChatboxOutline,
Heart,
HeartOutline,
Bookmark,
BookmarkOutline,
ChatboxOutline,
} from '@vicons/ionicons5';
import { MoreHorizFilled } from '@vicons/material';
import {
getPostStar,
postStar,
getPostCollection,
postCollection,
deletePost,
lockPost,
stickPost,
getPostStar,
postStar,
getPostCollection,
postCollection,
deletePost,
lockPost,
stickPost,
} from '@/api/post';
const store = useStore();
@ -181,10 +171,10 @@ const router = useRouter();
const hasStarred = ref(false);
const hasCollected = ref(false);
const props = withDefaults(
defineProps<{
post: Item.PostProps;
}>(),
{}
defineProps<{
post: Item.PostProps;
}>(),
{},
);
const showDelModal = ref(false);
const showLockModal = ref(false);
@ -192,303 +182,303 @@ const showStickModal = ref(false);
const loading = ref(false);
const emit = defineEmits<{
(e: 'reload'): void;
(e: 'reload'): void;
}>();
const post = computed({
get: () => {
let post: Item.PostComponentProps = Object.assign(
{
texts: [],
imgs: [],
videos: [],
links: [],
attachments: [],
charge_attachments: [],
},
props.post
);
post.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
post.texts.push(content);
}
if (+content.type === 3) {
post.imgs.push(content);
}
if (+content.type === 4) {
post.videos.push(content);
}
if (+content.type === 6) {
post.links.push(content);
}
if (+content.type === 7) {
post.attachments.push(content);
}
if (+content.type === 8) {
post.charge_attachments.push(content);
}
});
return post;
},
set: (newVal) => {
props.post.upvote_count = newVal.upvote_count;
props.post.comment_count = newVal.comment_count;
props.post.collection_count = newVal.collection_count;
},
get: () => {
let post: Item.PostComponentProps = Object.assign(
{
texts: [],
imgs: [],
videos: [],
links: [],
attachments: [],
charge_attachments: [],
},
props.post,
);
post.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
post.texts.push(content);
}
if (+content.type === 3) {
post.imgs.push(content);
}
if (+content.type === 4) {
post.videos.push(content);
}
if (+content.type === 6) {
post.links.push(content);
}
if (+content.type === 7) {
post.attachments.push(content);
}
if (+content.type === 8) {
post.charge_attachments.push(content);
}
});
return post;
},
set: (newVal) => {
props.post.upvote_count = newVal.upvote_count;
props.post.comment_count = newVal.comment_count;
props.post.collection_count = newVal.collection_count;
},
});
const adminOptions = computed(() => {
let options = [
{
label: '',
key: 'delete',
},
];
if (post.value.is_lock === 0) {
options.push({
label: '',
key: 'lock',
});
let options = [
{
label: '',
key: 'delete',
},
];
if (post.value.is_lock === 0) {
options.push({
label: '',
key: 'lock',
});
} else {
options.push({
label: '',
key: 'unlock',
});
}
if (store.state.userInfo.is_admin) {
if (post.value.is_top === 0) {
options.push({
label: '',
key: 'stick',
});
} else {
options.push({
label: '',
key: 'unlock',
});
}
if (store.state.userInfo.is_admin) {
if (post.value.is_top === 0) {
options.push({
label: '',
key: 'stick',
});
} else {
options.push({
label: '',
key: 'unstick',
});
}
options.push({
label: '',
key: 'unstick',
});
}
return options;
}
return options;
});
const goPostDetail = (id: number) => {
router.push({
name: 'post',
query: {
id,
},
});
router.push({
name: 'post',
query: {
id,
},
});
};
const doClickText = (e: MouseEvent, id: number) => {
if ((e.target as any).dataset.detail) {
const d = (e.target as any).dataset.detail.split(':');
if (d.length === 2) {
store.commit('refresh');
if (d[0] === 'tag') {
router.push({
name: 'home',
query: {
q: d[1],
t: 'tag',
},
});
} else {
router.push({
name: 'user',
query: {
username: d[1],
},
});
}
return;
}
if ((e.target as any).dataset.detail) {
const d = (e.target as any).dataset.detail.split(':');
if (d.length === 2) {
store.commit('refresh');
if (d[0] === 'tag') {
router.push({
name: 'home',
query: {
q: d[1],
t: 'tag',
},
});
} else {
router.push({
name: 'user',
query: {
username: d[1],
},
});
}
return;
}
goPostDetail(id);
}
goPostDetail(id);
};
const handlePostAction = (
item: 'delete' | 'lock' | 'unlock' | 'stick' | 'unstick'
item: 'delete' | 'lock' | 'unlock' | 'stick' | 'unstick',
) => {
if (item === 'delete') {
showDelModal.value = true;
}
if (item === 'lock' || item === 'unlock') {
showLockModal.value = true;
}
if (item === 'stick' || item === 'unstick') {
showStickModal.value = true;
}
if (item === 'delete') {
showDelModal.value = true;
}
if (item === 'lock' || item === 'unlock') {
showLockModal.value = true;
}
if (item === 'stick' || item === 'unstick') {
showStickModal.value = true;
}
};
const execDelAction = () => {
deletePost({
id: post.value.id,
})
.then((res) => {
window.$message.success('');
router.replace('/');
deletePost({
id: post.value.id,
})
.then((res) => {
window.$message.success('');
router.replace('/');
setTimeout(() => {
store.commit('refresh');
}, 50);
})
.catch((err) => {
loading.value = false;
});
setTimeout(() => {
store.commit('refresh');
}, 50);
})
.catch((err) => {
loading.value = false;
});
};
const execLockAction = () => {
lockPost({
id: post.value.id,
lockPost({
id: post.value.id,
})
.then((res) => {
emit('reload');
if (res.lock_status === 1) {
window.$message.success('');
} else {
window.$message.success('');
}
})
.then((res) => {
emit('reload');
if (res.lock_status === 1) {
window.$message.success('');
} else {
window.$message.success('');
}
})
.catch((err) => {
loading.value = false;
});
.catch((err) => {
loading.value = false;
});
};
const execStickAction = () => {
stickPost({
id: post.value.id,
stickPost({
id: post.value.id,
})
.then((res) => {
emit('reload');
if (res.top_status === 1) {
window.$message.success('');
} else {
window.$message.success('');
}
})
.then((res) => {
emit('reload');
if (res.top_status === 1) {
window.$message.success('');
} else {
window.$message.success('');
}
})
.catch((err) => {
loading.value = false;
});
.catch((err) => {
loading.value = false;
});
};
const handlePostStar = () => {
postStar({
id: post.value.id,
postStar({
id: post.value.id,
})
.then((res) => {
hasStarred.value = res.status;
if (res.status) {
post.value = {
...post.value,
upvote_count: post.value.upvote_count + 1,
};
} else {
post.value = {
...post.value,
upvote_count: post.value.upvote_count - 1,
};
}
})
.then((res) => {
hasStarred.value = res.status;
if (res.status) {
post.value = {
...post.value,
upvote_count: post.value.upvote_count + 1,
};
} else {
post.value = {
...post.value,
upvote_count: post.value.upvote_count - 1,
};
}
})
.catch((err) => {
console.log(err);
});
.catch((err) => {
console.log(err);
});
};
const handlePostCollection = () => {
postCollection({
id: post.value.id,
postCollection({
id: post.value.id,
})
.then((res) => {
hasCollected.value = res.status;
if (res.status) {
post.value = {
...post.value,
collection_count: post.value.collection_count + 1,
};
} else {
post.value = {
...post.value,
collection_count: post.value.collection_count - 1,
};
}
})
.then((res) => {
hasCollected.value = res.status;
if (res.status) {
post.value = {
...post.value,
collection_count: post.value.collection_count + 1,
};
} else {
post.value = {
...post.value,
collection_count: post.value.collection_count - 1,
};
}
})
.catch((err) => {
console.log(err);
});
.catch((err) => {
console.log(err);
});
};
onMounted(() => {
if (store.state.userInfo.id > 0) {
getPostStar({
id: post.value.id,
})
.then((res) => {
hasStarred.value = res.status;
})
.catch((err) => {
console.log(err);
});
if (store.state.userInfo.id > 0) {
getPostStar({
id: post.value.id,
})
.then((res) => {
hasStarred.value = res.status;
})
.catch((err) => {
console.log(err);
});
getPostCollection({
id: post.value.id,
})
.then((res) => {
hasCollected.value = res.status;
})
.catch((err) => {
console.log(err);
});
}
getPostCollection({
id: post.value.id,
})
.then((res) => {
hasCollected.value = res.status;
})
.catch((err) => {
console.log(err);
});
}
});
</script>
<style lang="less">
.detail-item {
width: 100%;
padding: 16px;
box-sizing: border-box;
width: 100%;
padding: 16px;
box-sizing: border-box;
background: #f7f9f9;
.nickname-wrap {
font-size: 14px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
}
background: #f7f9f9;
.nickname-wrap {
font-size: 14px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
}
.options {
opacity: 0.75;
}
.post-text {
font-size: 16px;
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.opts-wrap {
margin-top: 20px;
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
&.hover {
cursor: pointer;
}
}
.options {
opacity: 0.75;
}
.post-text {
font-size: 16px;
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.opts-wrap {
margin-top: 20px;
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
&.hover {
cursor: pointer;
}
}
.n-thing {
.n-thing-avatar-header-wrapper {
align-items: center;
}
}
.timestamp {
opacity: 0.75;
font-size: 12px;
margin-top: 10px;
}
.n-thing {
.n-thing-avatar-header-wrapper {
align-items: center;
}
}
.timestamp {
opacity: 0.75;
font-size: 12px;
margin-top: 10px;
}
}
.dark {
.detail-item {
background: #18181c;
}
.detail-item {
background: #18181c;
}
}
</style>
</style>

@ -1,280 +1,283 @@
<template>
<div class="images-wrap">
<n-image-group v-if="[1].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="2">
<template v-for="img in props.imgs" :key="img.id">
<n-gi>
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x1"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<div class="images-wrap">
<n-image-group v-if="[1].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="2">
<template v-for="img in props.imgs" :key="img.id">
<n-gi>
<n-image
class="post-img x1"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[2, 3].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="img in props.imgs" :key="img.id">
<n-gi>
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[2, 3].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="img in props.imgs" :key="img.id">
<n-gi>
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[4].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="4">
<template v-for="img in props.imgs" :key="img.id">
<n-gi>
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[4].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="4">
<template v-for="img in props.imgs" :key="img.id">
<n-gi>
<n-image
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[5].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx < 3">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="2" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx >= 3">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x1"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[5].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx < 3">
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="2" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx >= 3">
<n-image
class="post-img x1"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[6].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx < 3">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx >= 3">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="[6].includes(props.imgs.length)">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx < 3">
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs" :key="img.id">
<n-gi v-if="idx >= 3">
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="props.imgs.length === 7">
<n-grid :x-gap="4" :y-gap="4" :cols="4">
<template v-for="(img, idx) in props.imgs">
<n-gi :key="img.id" v-if="idx < 4">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi :key="img.id" v-if="idx >= 4">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="props.imgs.length === 7">
<n-grid :x-gap="4" :y-gap="4" :cols="4">
<template v-for="(img, idx) in props.imgs">
<n-gi v-if="idx < 4" :key="img.id">
<n-image
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi v-if="idx >= 4" :key="img.id">
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="props.imgs.length === 8">
<n-grid :x-gap="4" :y-gap="4" :cols="4">
<template v-for="(img, idx) in props.imgs">
<n-gi :key="img.id" v-if="idx < 4">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="4" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi :key="img.id" v-if="idx >= 4">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="props.imgs.length === 8">
<n-grid :x-gap="4" :y-gap="4" :cols="4">
<template v-for="(img, idx) in props.imgs">
<n-gi v-if="idx < 4" :key="img.id">
<n-image
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="4" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi v-if="idx >= 4" :key="img.id">
<n-image
class="post-img x3"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
<n-image-group v-if="props.imgs.length === 9">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="(img, idx) in props.imgs">
<n-gi :key="img.id" v-if="idx < 3">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi :key="img.id" v-if="idx >= 3 && idx < 6">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi :key="img.id" v-if="idx >= 6">
<n-image
@error="() => (img.content = defaultImg)"
@click.stop
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
</div>
<n-image-group v-if="props.imgs.length === 9">
<n-grid :x-gap="4" :y-gap="4" :cols="3">
<template v-for="(img, idx) in props.imgs">
<n-gi v-if="idx < 3" :key="img.id">
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi v-if="idx >= 3 && idx < 6" :key="img.id">
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
<n-grid :x-gap="4" :y-gap="4" :cols="3" style="margin-top: 4px">
<template v-for="(img, idx) in props.imgs">
<n-gi v-if="idx >= 6" :key="img.id">
<n-image
class="post-img x2"
object-fit="cover"
:src="img.content + thumbnail"
:preview-src="img.content"
@error="() => (img.content = defaultImg)"
@click.stop
/>
</n-gi>
</template>
</n-grid>
</n-image-group>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const defaultImg =
'https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/404.png';
'https://paopao-assets.oss-cn-shanghai.aliyuncs.com/public/404.png';
const thumbnail =
'?x-oss-process=image/resize,m_fill,w_300,h_300,limit_0/auto-orient,1/format,png';
const props = withDefaults(defineProps<{
imgs: Item.PostItemProps[],
}>(), {
imgs: () => []
});
'?x-oss-process=image/resize,m_fill,w_300,h_300,limit_0/auto-orient,1/format,png';
const props = withDefaults(
defineProps<{
imgs: Item.PostItemProps[];
}>(),
{
imgs: () => [],
},
);
</script>
<style lang="less">
.images-wrap {
margin-top: 10px;
margin-top: 10px;
}
.post-img {
display: flex;
margin: 0;
border-radius: 3px;
overflow: hidden;
background: rgba(0, 0, 0, 0.1);
border: 1px solid #eee;
img {
width: 100%;
height: 100%;
}
display: flex;
margin: 0;
border-radius: 3px;
overflow: hidden;
background: rgba(0, 0, 0, 0.1);
border: 1px solid #eee;
img {
width: 100%;
height: 100%;
}
}
.x1 {
height: 140px;
height: 140px;
}
.x2 {
height: 90px;
height: 90px;
}
.x3 {
height: 80px;
height: 80px;
}
.dark {
.post-img {
border: 1px solid #333;
}
.post-img {
border: 1px solid #333;
}
}
@media screen and (max-width: 821px) {
.x1 {
height: 100px;
}
.x2 {
height: 70px;
}
.x3 {
height: 50px;
}
.x1 {
height: 100px;
}
.x2 {
height: 70px;
}
.x3 {
height: 50px;
}
}
</style>
</style>

@ -1,84 +1,84 @@
<template>
<div class="post-item" @click="goPostDetail(post.id)">
<n-thing content-indented>
<template #avatar>
<n-avatar round :size="30" :src="post.user.avatar" />
</template>
<template #header>
<span class="nickname-wrap">
<router-link
@click.stop
class="username-link"
:to="{
name: 'user',
query: { username: post.user.username },
}"
>
{{ post.user.nickname }}
</router-link>
</span>
<span class="username-wrap"> @{{ post.user.username }} </span>
<n-tag
v-if="post.is_top"
class="top-tag"
type="warning"
size="small"
round
>
</n-tag>
</template>
<template #header-extra>
<span class="timestamp">
{{ post.ip_loc ? post.ip_loc + ' · ' : post.ip_loc }}
{{ formatRelativeTime(post.created_on) }}
</span>
</template>
<template #description v-if="post.texts.length > 0">
<span
v-for="content in post.texts"
:key="content.id"
class="post-text"
@click.stop="doClickText($event, post.id)"
v-html="parsePostTag(content.content).content"
>
</span>
</template>
<div class="post-item" @click="goPostDetail(post.id)">
<n-thing content-indented>
<template #avatar>
<n-avatar round :size="30" :src="post.user.avatar" />
</template>
<template #header>
<span class="nickname-wrap">
<router-link
class="username-link"
:to="{
name: 'user',
query: { username: post.user.username },
}"
@click.stop
>
{{ post.user.nickname }}
</router-link>
</span>
<span class="username-wrap"> @{{ post.user.username }} </span>
<n-tag
v-if="post.is_top"
class="top-tag"
type="warning"
size="small"
round
>
</n-tag>
</template>
<template #header-extra>
<span class="timestamp">
{{ post.ip_loc ? post.ip_loc + ' · ' : post.ip_loc }}
{{ formatRelativeTime(post.created_on) }}
</span>
</template>
<template v-if="post.texts.length > 0" #description>
<span
v-for="content in post.texts"
:key="content.id"
class="post-text"
@click.stop="doClickText($event, post.id)"
v-html="parsePostTag(content.content).content"
>
</span>
</template>
<template #footer>
<post-attachment :attachments="post.attachments" />
<post-attachment
:attachments="post.charge_attachments"
:price="post.attachment_price"
/>
<post-image :imgs="post.imgs" />
<post-video :videos="post.videos" />
<post-link :links="post.links" />
</template>
<template #action>
<n-space justify="space-between">
<div class="opt-item">
<n-icon size="18" class="opt-item-icon">
<heart-outline />
</n-icon>
{{ post.upvote_count }}
</div>
<div class="opt-item">
<n-icon size="18" class="opt-item-icon">
<chatbox-outline />
</n-icon>
{{ post.comment_count }}
</div>
<div class="opt-item">
<n-icon size="18" class="opt-item-icon">
<bookmark-outline />
</n-icon>
{{ post.collection_count }}
</div>
</n-space>
</template>
</n-thing>
</div>
<template #footer>
<post-attachment :attachments="post.attachments" />
<post-attachment
:attachments="post.charge_attachments"
:price="post.attachment_price"
/>
<post-image :imgs="post.imgs" />
<post-video :videos="post.videos" />
<post-link :links="post.links" />
</template>
<template #action>
<n-space justify="space-between">
<div class="opt-item">
<n-icon size="18" class="opt-item-icon">
<heart-outline />
</n-icon>
{{ post.upvote_count }}
</div>
<div class="opt-item">
<n-icon size="18" class="opt-item-icon">
<chatbox-outline />
</n-icon>
{{ post.comment_count }}
</div>
<div class="opt-item">
<n-icon size="18" class="opt-item-icon">
<bookmark-outline />
</n-icon>
{{ post.collection_count }}
</div>
</n-space>
</template>
</n-thing>
</div>
</template>
<script setup lang="ts">
@ -88,142 +88,145 @@ import { useRoute, useRouter } from 'vue-router';
import { formatRelativeTime } from '@/utils/formatTime';
import { parsePostTag } from '@/utils/content';
import {
HeartOutline,
BookmarkOutline,
ChatboxOutline,
HeartOutline,
BookmarkOutline,
ChatboxOutline,
} from '@vicons/ionicons5';
const route = useRoute();
const router = useRouter();
const store = useStore();
const props = withDefaults(defineProps<{
post: Item.PostProps,
}>(), {});
const props = withDefaults(
defineProps<{
post: Item.PostProps;
}>(),
{},
);
const post = computed(() => {
let post: Item.PostComponentProps = Object.assign(
{
texts: [],
imgs: [],
videos: [],
links: [],
attachments: [],
charge_attachments: [],
},
props.post
);
post.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
post.texts.push(content);
}
if (+content.type === 3) {
post.imgs.push(content);
}
if (+content.type === 4) {
post.videos.push(content);
}
if (+content.type === 6) {
post.links.push(content);
}
if (+content.type === 7) {
post.attachments.push(content);
}
if (+content.type === 8) {
post.charge_attachments.push(content);
}
});
return post;
let post: Item.PostComponentProps = Object.assign(
{
texts: [],
imgs: [],
videos: [],
links: [],
attachments: [],
charge_attachments: [],
},
props.post,
);
post.contents.map((content) => {
if (+content.type === 1 || +content.type === 2) {
post.texts.push(content);
}
if (+content.type === 3) {
post.imgs.push(content);
}
if (+content.type === 4) {
post.videos.push(content);
}
if (+content.type === 6) {
post.links.push(content);
}
if (+content.type === 7) {
post.attachments.push(content);
}
if (+content.type === 8) {
post.charge_attachments.push(content);
}
});
return post;
});
const goPostDetail = (id: number) => {
router.push({
name: 'post',
query: {
id,
},
});
router.push({
name: 'post',
query: {
id,
},
});
};
const doClickText = (e: MouseEvent, id: number) => {
if ((e.target as any).dataset.detail) {
const d = (e.target as any).dataset.detail.split(':');
if (d.length === 2) {
store.commit('refresh');
if (d[0] === 'tag') {
router.push({
name: 'home',
query: {
q: d[1],
t: 'tag',
},
});
} else {
router.push({
name: 'user',
query: {
username: d[1],
},
});
}
return;
}
if ((e.target as any).dataset.detail) {
const d = (e.target as any).dataset.detail.split(':');
if (d.length === 2) {
store.commit('refresh');
if (d[0] === 'tag') {
router.push({
name: 'home',
query: {
q: d[1],
t: 'tag',
},
});
} else {
router.push({
name: 'user',
query: {
username: d[1],
},
});
}
return;
}
goPostDetail(id);
}
goPostDetail(id);
};
</script>
<style lang="less">
.post-item {
width: 100%;
padding: 16px;
box-sizing: border-box;
width: 100%;
padding: 16px;
box-sizing: border-box;
.nickname-wrap {
font-size: 14px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
}
.nickname-wrap {
font-size: 14px;
}
.username-wrap {
font-size: 14px;
opacity: 0.75;
}
.top-tag {
transform: scale(0.75);
}
.timestamp {
opacity: 0.75;
font-size: 12px;
}
.post-text {
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.top-tag {
transform: scale(0.75);
}
.timestamp {
opacity: 0.75;
font-size: 12px;
}
.post-text {
text-align: justify;
overflow: hidden;
white-space: pre-wrap;
word-break: break-all;
}
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
}
&:hover {
background: #f7f9f9;
cursor: pointer;
.opt-item {
display: flex;
align-items: center;
opacity: 0.7;
.opt-item-icon {
margin-right: 10px;
}
}
&:hover {
background: #f7f9f9;
cursor: pointer;
}
.n-thing-avatar {
margin-top: 0;
}
.n-thing-header {
line-height: 16px;
margin-bottom: 8px !important;
}
.n-thing-avatar {
margin-top: 0;
}
.n-thing-header {
line-height: 16px;
margin-bottom: 8px !important;
}
}
.dark {
.post-item {
&:hover {
background: #18181c;
}
.post-item {
&:hover {
background: #18181c;
}
}
}
</style>
</style>

@ -1,40 +1,38 @@
<template>
<div class="link-wrap">
<div class="link-item" v-for="link in props.links" :key="link.id">
<n-icon class="hash-link"><link-outline /></n-icon>
<a
:href="link.content"
class="hash-link"
target="_blank"
@click.stop
>
<span class="link-txt">{{ link.content }}</span>
</a>
</div>
<div class="link-wrap">
<div v-for="link in props.links" :key="link.id" class="link-item">
<n-icon class="hash-link"><link-outline /></n-icon>
<a :href="link.content" class="hash-link" target="_blank" @click.stop>
<span class="link-txt">{{ link.content }}</span>
</a>
</div>
</div>
</template>
<script setup lang="ts">
import { LinkOutline } from '@vicons/ionicons5';
const props = withDefaults(defineProps<{
links: Item.PostItemProps[]
}>(), {
links: () => []
});
const props = withDefaults(
defineProps<{
links: Item.PostItemProps[];
}>(),
{
links: () => [],
},
);
</script>
<style lang="less" scoped>
.link-wrap {
margin-bottom: 10px;
.link-item {
display: flex;
align-items: center;
.hash-link {
.link-txt {
margin-left: 4px;
word-break: break-all;
}
}
margin-bottom: 10px;
.link-item {
display: flex;
align-items: center;
.hash-link {
.link-txt {
margin-left: 4px;
word-break: break-all;
}
}
}
}
</style>
</style>

@ -1,34 +1,37 @@
<template>
<div class="skeleton-item" v-for="i in new Array(num)" :key="i">
<div class="user">
<n-skeleton circle size="small" />
</div>
<div class="content">
<n-skeleton text :repeat="3" />
<n-skeleton text style="width: 60%" />
</div>
<div v-for="i in new Array(num)" :key="i" class="skeleton-item">
<div class="user">
<n-skeleton circle size="small" />
</div>
<div class="content">
<n-skeleton text :repeat="3" />
<n-skeleton text style="width: 60%" />
</div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
num: number,
}>(), {
num: 1
});
const props = withDefaults(
defineProps<{
num: number;
}>(),
{
num: 1,
},
);
</script>
<style lang="less" scoped>
.skeleton-item {
padding: 12px;
display: flex;
padding: 12px;
display: flex;
.user {
width: 42px;
}
.user {
width: 42px;
}
.content {
width: calc(100% - 42px);
}
.content {
width: calc(100% - 42px);
}
}
</style>
</style>

@ -1,19 +1,19 @@
<template>
<div v-if="props.videos.length > 0">
<n-grid :x-gap="4" :y-gap="4" :cols="full ? 1 : 5">
<n-gi :span="full ? 1 : 3">
<n-video
@click.stop
v-for="video in props.videos"
:key="video.id"
:src="video.content"
:colors="['#18a058', '#2aca75']"
:hoverable="true"
theme="gradient"
></n-video>
</n-gi>
</n-grid>
</div>
<div v-if="props.videos.length > 0">
<n-grid :x-gap="4" :y-gap="4" :cols="full ? 1 : 5">
<n-gi :span="full ? 1 : 3">
<n-video
v-for="video in props.videos"
:key="video.id"
:src="video.content"
:colors="['#18a058', '#2aca75']"
:hoverable="true"
theme="gradient"
@click.stop
></n-video>
</n-gi>
</n-grid>
</div>
</template>
<script setup lang="ts">
@ -21,13 +21,13 @@ import { reactive } from 'vue';
import NVideo from 'nonesir-video';
const props = withDefaults(
defineProps<{
videos: Item.PostItemProps[];
full?: boolean;
}>(),
{
videos: () => [],
full: false,
}
defineProps<{
videos: Item.PostItemProps[];
full?: boolean;
}>(),
{
videos: () => [],
full: false,
},
);
</script>
</script>

@ -1,68 +1,66 @@
<template>
<div class="reply-item">
<div class="header-wrap">
<div class="username">
<router-link
class="user-link"
:to="{
name: 'user',
query: { username: props.reply.user.username },
}"
>
{{ props.reply.user.username }}
</router-link>
<span class="reply-name">
{{ props.reply.at_user_id > 0 ? '' : ':' }}
</span>
<div class="reply-item">
<div class="header-wrap">
<div class="username">
<router-link
class="user-link"
:to="{
name: 'user',
query: { username: props.reply.user.username },
}"
>
{{ props.reply.user.username }}
</router-link>
<span class="reply-name">
{{ props.reply.at_user_id > 0 ? '' : ':' }}
</span>
<router-link
class="user-link"
:to="{
name: 'user',
query: { username: props.reply.at_user.username },
}"
v-if="props.reply.at_user_id > 0"
>
{{ props.reply.at_user.username }}
</router-link>
</div>
<div class="timestamp">
{{
props.reply.ip_loc
? props.reply.ip_loc + ' · '
: props.reply.ip_loc
}}
{{ formatRelativeTime(props.reply.created_on) }}
<n-popconfirm
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === props.reply.user.id
"
negative-text="取消"
positive-text="确认"
@positive-click="execDelAction"
>
<template #trigger>
<n-button quaternary circle size="tiny" class="del-btn">
<template #icon>
<n-icon>
<trash />
</n-icon>
</template>
</n-button>
</template>
</n-popconfirm>
</div>
</div>
<router-link
v-if="props.reply.at_user_id > 0"
class="user-link"
:to="{
name: 'user',
query: { username: props.reply.at_user.username },
}"
>
{{ props.reply.at_user.username }}
</router-link>
</div>
<div class="timestamp">
{{
props.reply.ip_loc ? props.reply.ip_loc + ' · ' : props.reply.ip_loc
}}
{{ formatRelativeTime(props.reply.created_on) }}
<n-popconfirm
v-if="
store.state.userInfo.is_admin ||
store.state.userInfo.id === props.reply.user.id
"
negative-text="取消"
positive-text="确认"
@positive-click="execDelAction"
>
<template #trigger>
<n-button quaternary circle size="tiny" class="del-btn">
<template #icon>
<n-icon>
<trash />
</n-icon>
</template>
</n-button>
</template>
</n-popconfirm>
</div>
</div>
<div class="base-wrap">
<div class="content">{{ props.reply.content }}</div>
<div class="reply-switch" v-if="store.state.userInfo.id > 0">
<span class="show" @click="focusReply"> </span>
</div>
</div>
<div class="base-wrap">
<div class="content">{{ props.reply.content }}</div>
<div v-if="store.state.userInfo.id > 0" class="reply-switch">
<span class="show" @click="focusReply"> </span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
@ -71,98 +69,100 @@ import { Trash } from '@vicons/tabler';
import { formatRelativeTime } from '@/utils/formatTime';
import { deleteCommentReply } from '@/api/post';
const props = withDefaults(defineProps<{
reply: Item.ReplyProps,
}>(), {});
const props = withDefaults(
defineProps<{
reply: Item.ReplyProps;
}>(),
{},
);
const store = useStore();
const emit = defineEmits<{
(e: "focusReply", reply: Item.ReplyProps): void,
(e: "reload"): void
(e: 'focusReply', reply: Item.ReplyProps): void;
(e: 'reload'): void;
}>();
const focusReply = () => {
emit('focusReply', props.reply);
emit('focusReply', props.reply);
};
const execDelAction = () => {
deleteCommentReply({
id: props.reply.id,
})
.then((res) => {
window.$message.success('');
deleteCommentReply({
id: props.reply.id,
})
.then((res) => {
window.$message.success('');
setTimeout(() => {
emit('reload');
}, 50);
})
.catch((err) => {
console.log(err);
});
setTimeout(() => {
emit('reload');
}, 50);
})
.catch((err) => {
console.log(err);
});
};
</script>
<style lang="less" scoped>
.reply-item {
display: flex;
flex-direction: column;
font-size: 12px;
padding: 8px;
border-bottom: 1px solid #f3f3f3;
display: flex;
flex-direction: column;
font-size: 12px;
padding: 8px;
border-bottom: 1px solid #f3f3f3;
.header-wrap {
display: flex;
align-items: center;
justify-content: space-between;
.username {
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.reply-name {
margin: 0 3px;
opacity: 0.75;
}
}
.timestamp {
opacity: 0.75;
text-align: right;
display: flex;
align-items: center;
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-wrap {
display: flex;
align-items: center;
justify-content: space-between;
.username {
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.reply-name {
margin: 0 3px;
opacity: 0.75;
}
}
.timestamp {
opacity: 0.75;
text-align: right;
display: flex;
align-items: center;
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.base-wrap {
display: flex;
.content {
width: calc(100% - 40px);
margin-top: 4px;
font-size: 12px;
text-align: justify;
line-height: 2;
}
.reply-switch {
width: 40px;
text-align: right;
font-size: 12px;
margin: 10px 0 0;
}
.base-wrap {
display: flex;
.content {
width: calc(100% - 40px);
margin-top: 4px;
font-size: 12px;
text-align: justify;
line-height: 2;
}
.reply-switch {
width: 40px;
text-align: right;
font-size: 12px;
margin: 10px 0 0;
.show {
color: #18a058;
cursor: pointer;
}
.hide {
opacity: 0.75;
cursor: pointer;
}
}
.show {
color: #18a058;
cursor: pointer;
}
.hide {
opacity: 0.75;
cursor: pointer;
}
}
}
}
.dark {
.reply-item {
border-bottom: 1px solid #262628;
}
.reply-item {
border-bottom: 1px solid #262628;
}
}
</style>
</style>

@ -1,71 +1,64 @@
<template>
<div class="rightbar-wrap" v-if="!store.state.collapsedRight">
<div class="search-wrap">
<n-input
round
clearable
placeholder="搜一搜..."
v-model:value="keyword"
@keyup.enter.prevent="handleSearch"
>
<template #prefix>
<n-icon :component="Search" />
</template>
</n-input>
</div>
<n-card title="热门话题" embedded :bordered="false" size="small">
<n-spin :show="loading">
<div class="hot-tag-item" v-for="tag in tags" :key="tag.id">
<router-link
class="hash-link"
:to="{
name: 'home',
query: {
q: tag.tag,
t: 'tag',
},
}"
>
#{{ tag.tag }}
</router-link>
<div class="post-num">
{{ formatQuoteNum(tag.quote_num) }}
</div>
</div>
</n-spin>
</n-card>
<n-card class="copyright-wrap" embedded :bordered="false" size="small">
<div class="copyright">&copy; 2022 PaoPao.Info</div>
<div class="copyright">
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="beian-link"
>
ICP2020036525-5
</a>
</div>
<div>
<n-space>
<a
href="https://www.rocs.me"
target="_blank"
class="hash-link"
>Roc's Me</a
>
<a
href="https://www.rocboss.com"
target="_blank"
class="hash-link"
>
ROCBOSS
</a>
</n-space>
</div>
</n-card>
<div v-if="!store.state.collapsedRight" class="rightbar-wrap">
<div class="search-wrap">
<n-input
v-model:value="keyword"
round
clearable
placeholder="搜一搜..."
@keyup.enter.prevent="handleSearch"
>
<template #prefix>
<n-icon :component="Search" />
</template>
</n-input>
</div>
<n-card title="热门话题" embedded :bordered="false" size="small">
<n-spin :show="loading">
<div v-for="tag in tags" :key="tag.id" class="hot-tag-item">
<router-link
class="hash-link"
:to="{
name: 'home',
query: {
q: tag.tag,
t: 'tag',
},
}"
>
#{{ tag.tag }}
</router-link>
<div class="post-num">
{{ formatQuoteNum(tag.quote_num) }}
</div>
</div>
</n-spin>
</n-card>
<n-card class="copyright-wrap" embedded :bordered="false" size="small">
<div class="copyright">&copy; 2022 PaoPao.Info</div>
<div class="copyright">
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="beian-link"
>
ICP2020036525-5
</a>
</div>
<div>
<n-space>
<a href="https://www.rocs.me" target="_blank" class="hash-link"
>Roc's Me</a
>
<a href="https://www.rocboss.com" target="_blank" class="hash-link">
ROCBOSS
</a>
</n-space>
</div>
</n-card>
</div>
</template>
<script setup lang="ts">
@ -82,82 +75,82 @@ const store = useStore();
const router = useRouter();
const loadTags = () => {
loading.value = true;
getTags({
type: 'hot',
num: 12,
loading.value = true;
getTags({
type: 'hot',
num: 12,
})
.then((res) => {
tags.value = res;
loading.value = false;
})
.then((res) => {
tags.value = res;
loading.value = false;
})
.catch((err) => {
loading.value = false;
});
.catch((err) => {
loading.value = false;
});
};
const formatQuoteNum = (num: number) => {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num;
return num;
};
const handleSearch = () => {
router.push({
name: 'home',
query: {
q: keyword.value,
},
});
router.push({
name: 'home',
query: {
q: keyword.value,
},
});
};
onMounted(() => {
loadTags();
loadTags();
});
</script>
<style lang="less" scoped>
.rightbar-wrap {
width: 240px;
position: fixed;
left: calc(50% + var(--content-main) / 2 + 10px);
.search-wrap {
margin: 12px 0;
}
width: 240px;
position: fixed;
left: calc(50% + var(--content-main) / 2 + 10px);
.search-wrap {
margin: 12px 0;
}
.hot-tag-item {
line-height: 2;
position: relative;
.hot-tag-item {
line-height: 2;
position: relative;
.hash-link {
width: calc(100% - 60px);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: block;
}
.hash-link {
width: calc(100% - 60px);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: block;
}
.post-num {
position: absolute;
right: 0;
top: 0;
width: 60px;
text-align: right;
line-height: 2;
opacity: 0.5;
}
.post-num {
position: absolute;
right: 0;
top: 0;
width: 60px;
text-align: right;
line-height: 2;
opacity: 0.5;
}
}
.copyright-wrap {
margin-top: 10px;
.copyright-wrap {
margin-top: 10px;
.copyright {
font-size: 12px;
opacity: 0.75;
}
.copyright {
font-size: 12px;
opacity: 0.75;
}
.hash-link {
font-size: 12px;
}
.hash-link {
font-size: 12px;
}
}
}
</style>
</style>

@ -1,90 +1,85 @@
<template>
<div class="sidebar-wrap">
<div class="logo-wrap">
<n-image
class="logo-img"
width="36"
:src="LOGO"
:preview-disabled="true"
@click="goHome"
/>
</div>
<n-menu
:accordion="true"
:collapsed="store.state.collapsedLeft"
:collapsed-width="64"
:icon-size="24"
:options="menuOptions"
:render-label="renderMenuLabel"
:render-icon="renderMenuIcon"
:value="selectedPath"
@update:value="goRouter"
/>
<div class="user-wrap" v-if="store.state.userInfo.id > 0">
<n-avatar
class="user-avatar"
round
:size="34"
:src="store.state.userInfo.avatar"
/>
<div class="sidebar-wrap">
<div class="logo-wrap">
<n-image
class="logo-img"
width="36"
:src="LOGO"
:preview-disabled="true"
@click="goHome"
/>
</div>
<n-menu
:accordion="true"
:collapsed="store.state.collapsedLeft"
:collapsed-width="64"
:icon-size="24"
:options="menuOptions"
:render-label="renderMenuLabel"
:render-icon="renderMenuIcon"
:value="selectedPath"
@update:value="goRouter"
/>
<div class="user-info">
<div class="nickname">
<span class="nickname-txt">
{{ store.state.userInfo.nickname }}
</span>
<n-button
class="logout"
quaternary
circle
size="tiny"
@click="handleLogout"
>
<template #icon>
<n-icon><log-out-outline /></n-icon>
</template>
</n-button>
</div>
<div class="username">@{{ store.state.userInfo.username }}</div>
</div>
<div v-if="store.state.userInfo.id > 0" class="user-wrap">
<n-avatar
class="user-avatar"
round
:size="34"
:src="store.state.userInfo.avatar"
/>
<div class="user-mini-wrap">
<n-button
class="logout"
quaternary
circle
@click="handleLogout"
>
<template #icon>
<n-icon :size="24"><log-out-outline /></n-icon>
</template>
</n-button>
</div>
</div>
<div class="user-wrap" v-else>
<div class="login-wrap">
<n-button
strong
secondary
round
type="primary"
@click="triggerAuth('signin')"
>
</n-button>
<n-button
strong
secondary
round
type="info"
@click="triggerAuth('signup')"
>
</n-button>
</div>
<div class="user-info">
<div class="nickname">
<span class="nickname-txt">
{{ store.state.userInfo.nickname }}
</span>
<n-button
class="logout"
quaternary
circle
size="tiny"
@click="handleLogout"
>
<template #icon>
<n-icon><log-out-outline /></n-icon>
</template>
</n-button>
</div>
<div class="username">@{{ store.state.userInfo.username }}</div>
</div>
<div class="user-mini-wrap">
<n-button class="logout" quaternary circle @click="handleLogout">
<template #icon>
<n-icon :size="24"><log-out-outline /></n-icon>
</template>
</n-button>
</div>
</div>
<div v-else class="user-wrap">
<div class="login-wrap">
<n-button
strong
secondary
round
type="primary"
@click="triggerAuth('signin')"
>
</n-button>
<n-button
strong
secondary
round
type="info"
@click="triggerAuth('signup')"
>
</n-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
@ -92,16 +87,16 @@ import { h, ref, watch, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { NIcon, NBadge, useMessage } from 'naive-ui';
import type { RouteRecordName } from "vue-router";
import type { RouteRecordName } from 'vue-router';
import {
HomeOutline,
BookmarkOutline,
NotificationsOutline,
HeartOutline,
LeafOutline,
WalletOutline,
SettingsOutline,
LogOutOutline,
HomeOutline,
BookmarkOutline,
NotificationsOutline,
HeartOutline,
LeafOutline,
WalletOutline,
SettingsOutline,
LogOutOutline,
} from '@vicons/ionicons5';
import { Hash } from '@vicons/tabler';
import { getUnreadMsgCount } from '@/api/user';
@ -115,159 +110,159 @@ const selectedPath = ref<any>(route.name || '');
const msgLoop = ref();
watch(route, () => {
selectedPath.value = route.name;
selectedPath.value = route.name;
});
watch(store.state, () => {
if (store.state.userInfo.id > 0) {
if (!msgLoop.value) {
getUnreadMsgCount()
.then((res) => {
hasUnreadMsg.value = res.count > 0;
})
.catch((err) => {
console.log(err);
});
if (store.state.userInfo.id > 0) {
if (!msgLoop.value) {
getUnreadMsgCount()
.then((res) => {
hasUnreadMsg.value = res.count > 0;
})
.catch((err) => {
console.log(err);
});
msgLoop.value = setInterval(() => {
getUnreadMsgCount()
.then((res) => {
hasUnreadMsg.value = res.count > 0;
})
.catch((err) => {
console.log(err);
});
}, 5000);
}
} else {
if (msgLoop.value) {
clearInterval(msgLoop.value);
}
msgLoop.value = setInterval(() => {
getUnreadMsgCount()
.then((res) => {
hasUnreadMsg.value = res.count > 0;
})
.catch((err) => {
console.log(err);
});
}, 5000);
}
} else {
if (msgLoop.value) {
clearInterval(msgLoop.value);
}
}
});
onMounted(() => {
window.onresize = () => {
store.commit('triggerCollapsedLeft', document.body.clientWidth <= 821);
store.commit('triggerCollapsedRight', document.body.clientWidth <= 821);
};
window.onresize = () => {
store.commit('triggerCollapsedLeft', document.body.clientWidth <= 821);
store.commit('triggerCollapsedRight', document.body.clientWidth <= 821);
};
});
const menuOptions = computed(() => {
return store.state.userInfo.id > 0
? [
{
label: '广',
key: 'home',
icon: () => h(HomeOutline),
href: '/',
},
{
label: '',
key: 'topic',
icon: () => h(Hash),
href: '/topic',
},
{
label: '',
key: 'profile',
icon: () => h(LeafOutline),
href: '/profile',
},
{
label: '',
key: 'notification',
icon: () => h(NotificationsOutline),
href: '/notification',
},
{
label: '',
key: 'collection',
icon: () => h(BookmarkOutline),
href: '/collection',
},
{
label: '',
key: 'star',
icon: () => h(HeartOutline),
href: '/star',
},
{
label: '',
key: 'wallet',
icon: () => h(WalletOutline),
href: '/wallet',
},
{
label: '',
key: 'setting',
icon: () => h(SettingsOutline),
href: '/setting',
},
]
: [
{
label: '广',
key: 'home',
icon: () => h(HomeOutline),
href: '/',
},
{
label: '',
key: 'topic',
icon: () => h(Hash),
href: '/topic',
},
];
return store.state.userInfo.id > 0
? [
{
label: '广',
key: 'home',
icon: () => h(HomeOutline),
href: '/',
},
{
label: '',
key: 'topic',
icon: () => h(Hash),
href: '/topic',
},
{
label: '',
key: 'profile',
icon: () => h(LeafOutline),
href: '/profile',
},
{
label: '',
key: 'notification',
icon: () => h(NotificationsOutline),
href: '/notification',
},
{
label: '',
key: 'collection',
icon: () => h(BookmarkOutline),
href: '/collection',
},
{
label: '',
key: 'star',
icon: () => h(HeartOutline),
href: '/star',
},
{
label: '',
key: 'wallet',
icon: () => h(WalletOutline),
href: '/wallet',
},
{
label: '',
key: 'setting',
icon: () => h(SettingsOutline),
href: '/setting',
},
]
: [
{
label: '广',
key: 'home',
icon: () => h(HomeOutline),
href: '/',
},
{
label: '',
key: 'topic',
icon: () => h(Hash),
href: '/topic',
},
];
});
const renderMenuLabel = (option: AnyObject) => {
if ('href' in option) {
return h('div', {}, option.label);
}
return option.label;
if ('href' in option) {
return h('div', {}, option.label);
}
return option.label;
};
const renderMenuIcon = (option: AnyObject) => {
if (option.key === 'notification') {
return h(
NBadge,
if (option.key === 'notification') {
return h(
NBadge,
{
dot: true,
show: hasUnreadMsg.value,
processing: true,
},
{
default: () =>
h(
NIcon,
{
dot: true,
show: hasUnreadMsg.value,
processing: true,
color:
option.key === selectedPath.value
? 'var(--n-item-icon-color-active)'
: 'var(--n-item-icon-color)',
},
{
default: () =>
h(
NIcon,
{
color:
option.key === selectedPath.value
? 'var(--n-item-icon-color-active)'
: 'var(--n-item-icon-color)',
},
{ default: option.icon }
),
}
);
}
return h(NIcon, null, { default: option.icon });
{ default: option.icon },
),
},
);
}
return h(NIcon, null, { default: option.icon });
};
const goRouter = (name: string, item: any = {}) => {
selectedPath.value = name;
router.push({ name });
selectedPath.value = name;
router.push({ name });
};
const goHome = () => {
if (route.path === '/') {
store.commit('refresh');
}
if (route.path === '/') {
store.commit('refresh');
}
goRouter('home');
goRouter('home');
};
const triggerAuth = (key: string) => {
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', key);
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', key);
};
const handleLogout = () => {
store.commit('userLogout');
store.commit('userLogout');
};
window.$store = store;
window.$message = useMessage();
@ -275,112 +270,112 @@ window.$message = useMessage();
<style lang="less">
.sidebar-wrap {
z-index: 99;
width: 200px;
height: 100vh;
position: fixed;
right: calc(50% + var(--content-main) / 2 + 10px);
padding: 12px 0;
box-sizing: border-box;
.n-menu .n-menu-item-content::before {
border-radius: 21px;
}
z-index: 99;
width: 200px;
height: 100vh;
position: fixed;
right: calc(50% + var(--content-main) / 2 + 10px);
padding: 12px 0;
box-sizing: border-box;
.n-menu .n-menu-item-content::before {
border-radius: 21px;
}
}
.logo-wrap {
display: flex;
justify-content: flex-start;
margin-bottom: 12px;
.logo-img {
margin-left: 24px;
&:hover {
cursor: pointer;
}
display: flex;
justify-content: flex-start;
margin-bottom: 12px;
.logo-img {
margin-left: 24px;
&:hover {
cursor: pointer;
}
}
}
.user-wrap {
display: flex;
align-items: center;
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
.user-mini-wrap {
display: none;
}
.user-avatar {
margin-right: 8px;
}
display: flex;
align-items: center;
position: absolute;
bottom: 12px;
left: 12px;
right: 12px;
.user-mini-wrap {
display: none;
}
.user-avatar {
margin-right: 8px;
}
.user-info {
display: flex;
flex-direction: column;
.user-info {
display: flex;
flex-direction: column;
.nickname {
font-size: 16px;
font-weight: bold;
line-height: 16px;
height: 16px;
margin-bottom: 2px;
display: flex;
align-items: center;
.nickname {
font-size: 16px;
font-weight: bold;
line-height: 16px;
height: 16px;
margin-bottom: 2px;
display: flex;
align-items: center;
.nickname-txt {
max-width: 90px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.nickname-txt {
max-width: 90px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.logout {
margin-left: 6px;
}
}
.logout {
margin-left: 6px;
}
}
.username {
font-size: 14px;
line-height: 16px;
height: 16px;
width: 120px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
opacity: 0.75;
}
.username {
font-size: 14px;
line-height: 16px;
height: 16px;
width: 120px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
opacity: 0.75;
}
.login-wrap {
display: flex;
justify-content: center;
width: 100%;
button {
margin: 0 4px;
}
}
.login-wrap {
display: flex;
justify-content: center;
width: 100%;
button {
margin: 0 4px;
}
}
}
.auth-card {
.n-card-header {
z-index: 999;
}
.n-card-header {
z-index: 999;
}
}
@media screen and (max-width: 821px) {
.sidebar-wrap {
width: 65px;
right: calc(100% - 60px);
.sidebar-wrap {
width: 65px;
right: calc(100% - 60px);
}
.logo-wrap {
.logo-img {
margin-left: 12px !important;
}
.logo-wrap {
.logo-img {
margin-left: 12px !important;
}
}
.user-wrap {
.user-avatar,
.user-info,
.login-wrap {
display: none;
}
.user-wrap {
.user-avatar,
.user-info,
.login-wrap {
display: none;
}
.user-mini-wrap {
display: block !important;
}
.user-mini-wrap {
display: block !important;
}
}
}
</style>
</style>

@ -1,52 +1,52 @@
<template>
<n-modal
:show="show"
@update:show="closeModal"
class="whisper-card"
preset="card"
size="small"
title="私信"
:mask-closable="false"
:bordered="false"
:style="{
width: '360px',
}"
>
<div class="whisper-wrap">
<n-alert :show-icon="false">
:
<n-ellipsis style="max-width: 100%">
<n-gradient-text type="success">
{{ user.nickname }}@{{ user.username }}
</n-gradient-text>
</n-ellipsis>
</n-alert>
<div class="whisper-line">
<n-input
type="textarea"
placeholder="请输入私信内容(请勿发送不和谐内容,否则将会被封号)"
:autosize="{
minRows: 5,
maxRows: 10,
}"
v-model:value="content"
maxlength="200"
show-count
/>
</div>
<div class="whisper-line send-wrap">
<n-button
strong
secondary
type="primary"
:loading="loading"
@click="sendWhisper"
>
</n-button>
</div>
</div>
</n-modal>
<n-modal
:show="show"
class="whisper-card"
preset="card"
size="small"
title="私信"
:mask-closable="false"
:bordered="false"
:style="{
width: '360px',
}"
@update:show="closeModal"
>
<div class="whisper-wrap">
<n-alert :show-icon="false">
:
<n-ellipsis style="max-width: 100%">
<n-gradient-text type="success">
{{ user.nickname }}@{{ user.username }}
</n-gradient-text>
</n-ellipsis>
</n-alert>
<div class="whisper-line">
<n-input
v-model:value="content"
type="textarea"
placeholder="请输入私信内容(请勿发送不和谐内容,否则将会被封号)"
:autosize="{
minRows: 5,
maxRows: 10,
}"
maxlength="200"
show-count
/>
</div>
<div class="whisper-line send-wrap">
<n-button
strong
secondary
type="primary"
:loading="loading"
@click="sendWhisper"
>
</n-button>
</div>
</div>
</n-modal>
</template>
<script setup lang="ts">
@ -54,52 +54,52 @@ import { ref } from 'vue';
import { sendUserWhisper } from '@/api/user';
const props = withDefaults(
defineProps<{
show: boolean;
user: Item.UserInfo;
}>(),
{
show: false,
}
defineProps<{
show: boolean;
user: Item.UserInfo;
}>(),
{
show: false,
},
);
const content = ref('');
const loading = ref(false);
const emit = defineEmits<{
(e: 'success'): void;
(e: 'success'): void;
}>();
const closeModal = () => {
emit('success');
emit('success');
};
const sendWhisper = () => {
loading.value = true;
sendUserWhisper({
user_id: props.user.id,
content: content.value,
})
.then((res: any) => {
window.$message.success('');
loading.value = false;
content.value = '';
loading.value = true;
sendUserWhisper({
user_id: props.user.id,
content: content.value,
})
.then((res: any) => {
window.$message.success('');
loading.value = false;
content.value = '';
closeModal();
})
.catch((err: any) => {
loading.value = false;
});
closeModal();
})
.catch((err: any) => {
loading.value = false;
});
};
</script>
<style lang="less" scoped>
.whisper-wrap {
.whisper-line {
margin-top: 10px;
.whisper-line {
margin-top: 10px;
&.send-wrap {
.n-button {
width: 100%;
}
}
&.send-wrap {
.n-button {
width: 100%;
}
}
}
}
</style>
</style>

@ -1,188 +1,182 @@
declare module NetParams {
interface AuthUserLogin {
/** 用户名 */
username: string,
/** 密码 */
password: string
}
interface AuthUserRegister {
/** 用户名 */
username: string,
/** 密码 */
password: string
}
type AuthUserInfo = string
interface AuthUpdateUserPassword {
/** 新密码 */
password: string,
/** 旧密码 */
old_password: string
}
interface UserGetCollections {
page: number,
page_size: number
}
interface UserPrecheckAttachment {
id: number
}
interface UserGetAttachment {
id: number
}
interface UserGetUnreadMsgCount {
}
interface UserGetMessages {
page: number,
page_size: number
}
interface UserGetUserPosts {
/** 用户名 */
username: string,
page: number,
page_size: number
}
interface UserGetStars {
page: number,
page_size: number
}
interface UserGetUserProfile {
username: string
}
interface UserGetBills {
page: number,
page_size: number
}
interface UserReqRecharge {
amount: number
}
interface UserGetRecharge {
id: number
}
interface UserBindUserPhone {
phone: string,
captcha: string
}
interface UserGetCaptcha {
}
interface UserWhisper {
user_id: number,
content: string
}
interface UserChangePassword {
/** 新密码 */
password: string,
/** 旧密码 */
old_password: string
}
interface UserChangeNickname {
/** 昵称 */
nickname: string
}
interface PostGetPost {
id: number
}
interface PostGetPosts {
query: string | null,
type: string,
page: number,
page_size: number
}
interface PostLockPost {
id: number
}
interface PostStickPost {
id: number
}
interface PostGetPostStar {
id: number
}
interface PostPostStar {
id: number
}
interface PostGetPostCollection {
id: number
}
interface PostPostCollection {
id: number
}
interface PostGetTags {
type: "hot" | string,
num: number
}
interface PostGetPostComments {
id: number
}
interface PostCreatePost {
/** 帖子内容列表 */
contents: Partial<Item.PostItemProps>[],
/** 标签列表 */
tags: string[],
/** 艾特用户列表 */
users: string[],
/** 附件价格 */
attachment_price: number
}
interface PostDeletePost {
id: number
}
interface PostCreateComment {
/** 内容ID */
post_id: number,
/** 帖子内容列表 */
contents: Partial<Item.CommentItemProps>[],
/** 艾特用户列表 */
users: string[]
}
interface PostDeleteComment {
id: number
}
interface PostCreateCommentReply {
/** 艾特的用户UID */
at_user_id: number,
/** 回复的评论ID */
comment_id: number,
/** 回复内容 */
content: string
}
interface PostDeleteCommentReply{
id: number
}
declare namespace NetParams {
interface AuthUserLogin {
/** 用户名 */
username: string;
/** 密码 */
password: string;
}
interface AuthUserRegister {
/** 用户名 */
username: string;
/** 密码 */
password: string;
}
type AuthUserInfo = string;
interface AuthUpdateUserPassword {
/** 新密码 */
password: string;
/** 旧密码 */
old_password: string;
}
interface UserGetCollections {
page: number;
page_size: number;
}
interface UserPrecheckAttachment {
id: number;
}
interface UserGetAttachment {
id: number;
}
interface UserGetUnreadMsgCount {}
interface UserGetMessages {
page: number;
page_size: number;
}
interface UserGetUserPosts {
/** 用户名 */
username: string;
page: number;
page_size: number;
}
interface UserGetStars {
page: number;
page_size: number;
}
interface UserGetUserProfile {
username: string;
}
interface UserGetBills {
page: number;
page_size: number;
}
interface UserReqRecharge {
amount: number;
}
interface UserGetRecharge {
id: number;
}
interface UserBindUserPhone {
phone: string;
captcha: string;
}
interface UserGetCaptcha {}
interface UserWhisper {
user_id: number;
content: string;
}
interface UserChangePassword {
/** 新密码 */
password: string;
/** 旧密码 */
old_password: string;
}
interface UserChangeNickname {
/** 昵称 */
nickname: string;
}
interface PostGetPost {
id: number;
}
interface PostGetPosts {
query: string | null;
type: string;
page: number;
page_size: number;
}
interface PostLockPost {
id: number;
}
interface PostStickPost {
id: number;
}
interface PostGetPostStar {
id: number;
}
interface PostPostStar {
id: number;
}
interface PostGetPostCollection {
id: number;
}
interface PostPostCollection {
id: number;
}
interface PostGetTags {
type: 'hot' | string;
num: number;
}
interface PostGetPostComments {
id: number;
}
interface PostCreatePost {
/** 帖子内容列表 */
contents: Partial<Item.PostItemProps>[];
/** 标签列表 */
tags: string[];
/** 艾特用户列表 */
users: string[];
/** 附件价格 */
attachment_price: number;
}
interface PostDeletePost {
id: number;
}
interface PostCreateComment {
/** 内容ID */
post_id: number;
/** 帖子内容列表 */
contents: Partial<Item.CommentItemProps>[];
/** 艾特用户列表 */
users: string[];
}
interface PostDeleteComment {
id: number;
}
interface PostCreateCommentReply {
/** 艾特的用户UID */
at_user_id: number;
/** 回复的评论ID */
comment_id: number;
/** 回复内容 */
content: string;
}
interface PostDeleteCommentReply {
id: number;
}
}

@ -1,158 +1,142 @@
declare module NetReq {
declare namespace NetReq {
interface AuthUserLogin {
token: string;
}
interface AuthUserRegister {
/** 用户UID */
id: number;
/** 用户名 */
username: string;
}
type AuthUserInfo = Item.UserInfo;
interface AuthUpdateUserPassword {}
interface UserGetCollections {
/** 帖子列表 */
list: Item.PostProps[];
/** 页码信息 */
pager: Item.PagerProps;
}
type UserGetSuggestUsers = string[];
type UserGetSuggestTags = string[];
interface UserPrecheckAttachment {
paid: number;
}
type UserGetAttachment = string;
interface UserGetUnreadMsgCount {
count: number;
}
interface UserGetMessages {
/** 消息列表 */
list: Item.MessageProps[];
/** 页码信息 */
pager: Item.PagerProps;
}
interface AuthUserLogin {
token: string
}
interface UserGetUserPosts {
/** 帖子列表 */
list: Item.PostProps[];
/** 页码信息 */
pager: Item.PagerProps;
}
interface AuthUserRegister {
/** 用户UID */
id: number,
/** 用户名 */
username: string
}
type AuthUserInfo = Item.UserInfo
interface UserGetStars {
/** 帖子列表 */
list: Item.PostProps[];
/** 页码信息 */
pager: Item.PagerProps;
}
interface AuthUpdateUserPassword {
}
type UserGetUserProfile = Item.UserInfo;
interface UserGetCollections {
/** 帖子列表 */
list: Item.PostProps[],
/** 页码信息 */
pager: Item.PagerProps
}
interface UserGetBills {
list: Item.BillProps[];
/** 页码信息 */
pager: Item.PagerProps;
}
type UserGetSuggestUsers = string[]
type UserGetSuggestTags = string[]
interface UserPrecheckAttachment {
paid: number
}
interface UserReqRecharge {
id: number;
pay: string;
}
type UserGetAttachment = string
interface UserGetRecharge {
status: string;
}
interface UserGetUnreadMsgCount {
count: number
}
interface UserBindUserPhone {}
interface UserGetMessages {
/** 消息列表 */
list: Item.MessageProps[],
/** 页码信息 */
pager: Item.PagerProps
}
interface UserGetCaptcha {
id: string;
/** 头像图片 base64 */
b64s: string;
}
interface UserGetUserPosts {
/** 帖子列表 */
list: Item.PostProps[],
/** 页码信息 */
pager: Item.PagerProps
}
interface UserChangeNickname {}
interface UserGetStars {
/** 帖子列表 */
list: Item.PostProps[],
/** 页码信息 */
pager: Item.PagerProps
}
interface UserChangePassword {}
type UserGetUserProfile = Item.UserInfo
type PostGetPost = Item.PostProps;
interface UserGetBills {
list: Item.BillProps[],
/** 页码信息 */
pager: Item.PagerProps
}
interface PostGetPosts {
/** 帖子列表 */
list: Item.PostProps[];
/** 页码信息 */
pager: Item.PagerProps;
}
interface UserReqRecharge {
id: number,
pay: string
}
interface PostLockPost {
/** 锁定状态0为未锁定1为锁定 */
lock_status: 0 | 1;
}
interface UserGetRecharge {
status: string
}
interface PostStickPost {
/** 置顶状态0为未置顶1为置顶 */
top_status: 0 | 1;
}
interface UserBindUserPhone {
interface PostGetPostStar {
status: boolean;
}
}
interface PostPostStar {
status: boolean;
}
interface UserGetCaptcha {
id: string,
/** 头像图片 base64 */
b64s: string
}
interface PostGetPostCollection {
status: boolean;
}
interface UserChangeNickname {
}
interface PostPostCollection {
status: boolean;
}
interface UserChangePassword {
type PostGetTags = Item.TagProps[];
}
interface PostGetPostComments {
/** 评论列表 */
list: Item.CommentProps[];
/** 页码信息 */
pager: Item.PagerProps;
}
type PostGetPost = Item.PostProps
type PostCreatePost = Item.PostProps;
interface PostGetPosts {
/** 帖子列表 */
list: Item.PostProps[],
/** 页码信息 */
pager: Item.PagerProps
}
interface PostDeletePost {}
interface PostLockPost {
/** 锁定状态0为未锁定1为锁定 */
lock_status: 0 | 1
}
type PostCreateComment = Item.CommentProps;
interface PostStickPost {
/** 置顶状态0为未置顶1为置顶 */
top_status: 0 | 1
}
interface PostDeleteComment {}
interface PostGetPostStar {
status: boolean
}
interface PostPostStar {
status: boolean
}
interface PostGetPostCollection {
status: boolean
}
interface PostPostCollection {
status: boolean
}
type PostGetTags = Item.TagProps[]
interface PostGetPostComments {
/** 评论列表 */
list: Item.CommentProps[],
/** 页码信息 */
pager: Item.PagerProps
}
type PostCreatePost = Item.PostProps
interface PostDeletePost {
}
type PostCreateComment = Item.CommentProps
interface PostDeleteComment {
}
type PostCreateCommentReply = Item.ReplyProps
interface PostDeleteCommentReply{
}
type PostCreateCommentReply = Item.ReplyProps;
interface PostDeleteCommentReply {}
}

@ -1,290 +1,288 @@
declare module Item {
declare namespace Item {
interface UserInfo {
/** 用户UID */
id: number;
/** 用户名 */
username: string;
/** 用户昵称 */
nickname: string;
/** 用户头像 */
avatar: string;
/** 用户手机号 */
phone?: string;
/** 是否为管理员 */
is_admin: boolean;
/** 用户余额(分) */
balance?: number;
/** 用户状态 */
status?: 0 | 1;
}
interface UserInfo {
/** 用户UID */
id: number,
/** 用户名 */
username: string,
/** 用户昵称 */
nickname: string,
/** 用户头像 */
avatar: string,
/** 用户手机号 */
phone?: string,
/** 是否为管理员 */
is_admin: boolean,
/** 用户余额(分) */
balance?: number,
/** 用户状态 */
status?: 0 | 1
}
/** 评论内容 */
interface CommentItemProps {
/** 内容ID */
id: number;
/** 评论ID */
comment_id: number;
/** 评论者UID */
user_id: number;
/** 类别1为标题2为文字段落3为图片地址4为视频地址5为语音地址6为链接地址 */
type: number;
/** 内容 */
content: string;
/** 排序,越小越靠前 */
sort: number;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
/** 评论数据 */
interface CommentProps {
id: number;
post_id: number;
/** 评论者UID */
user_id: number;
/** 评论者用户信息 */
user: UserInfo;
/** 评论内容 */
interface CommentItemProps {
/** 内容ID */
id: number,
/** 评论ID */
comment_id: number,
/** 评论者UID */
user_id: number,
/** 类别1为标题2为文字段落3为图片地址4为视频地址5为语音地址6为链接地址 */
type: number,
/** 内容 */
content: string,
/** 排序,越小越靠前 */
sort: number,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1
}
contents: CommentItemProps[];
/** 回复列表 */
replies: ReplyProps[];
/** 评论者IP地址 */
ip?: string;
/** 评论者城市地址 */
ip_loc: string;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
/** 评论数据 */
interface CommentProps {
id: number,
post_id: number,
/** 评论者UID */
user_id: number,
/** 评论者用户信息 */
user: UserInfo,
/** 评论内容 */
contents: CommentItemProps[],
/** 回复列表 */
replies: ReplyProps[],
/** 评论者IP地址 */
ip?: string,
/** 评论者城市地址 */
ip_loc: string,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1
}
interface CommentComponentProps extends CommentProps {
/** 文字评论列表 */
texts: CommentItemProps[];
/** 图片评论列表 */
imgs: CommentItemProps[];
}
interface CommentComponentProps extends CommentProps {
/** 文字评论列表 */
texts: CommentItemProps[],
/** 图片评论列表 */
imgs: CommentItemProps[]
}
/** 回复内容 */
interface ReplyProps {
/** 内容ID */
id: number;
/** 评论ID */
comment_id: number;
/** 回复人ID */
user_id: number;
/** 回复人用户数据 */
user: UserInfo;
/** 艾特人ID */
at_user_id: number;
/** 艾特人用户数据 */
at_user: UserInfo;
/** 内容 */
content: string;
/** 回复人IP地址 */
ip?: string;
/** 回复人城市地址 */
ip_loc: string;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
/** 回复内容 */
interface ReplyProps {
/** 内容ID */
id: number,
/** 评论ID */
comment_id: number,
/** 回复人ID */
user_id: number,
/** 回复人用户数据 */
user: UserInfo,
/** 艾特人ID */
at_user_id: number,
/** 艾特人用户数据 */
at_user: UserInfo,
/** 内容 */
content: string,
/** 回复人IP地址 */
ip?: string,
/** 回复人城市地址 */
ip_loc: string,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1
}
/** 帖子内容 */
interface PostItemProps {
/** 内容ID */
id: number,
/** 类型1为标题2为文字段落3为图片地址4为视频地址5为语音地址6为链接地址7为附件资源8为收费资源 */
type: number,
/** POST ID */
post_id: number,
/** 内容 */
content: string,
/** 排序,越小越靠前 */
sort: number,
/** 用户UID */
user_id?: number,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1,
}
/** 帖子 */
interface PostProps {
id: number,
/** 发帖人UID */
user_id: number,
/** 发帖人用户数据 */
user: UserInfo,
/** 附件价格(分) */
attachment_price: number,
/** 发帖时IP地址 */
ip?: string,
/** 发帖时城市地址 */
ip_loc: string,
/** 最新回复时间 */
latest_replied_on: number,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 点赞数 */
upvote_count: number,
/** 评论数 */
comment_count: number,
/** 收藏数 */
collection_count: number,
/** 内容列表 */
contents: PostItemProps[],
/** 标签列表 */
tags: {[key: string]: number} | string,
/** 是否锁定 */
is_lock: number,
/** 是否置顶 */
is_top: number,
/** 是否精华 */
is_essence: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1
}
/** 帖子内容 */
interface PostItemProps {
/** 内容ID */
id: number;
/** 类型1为标题2为文字段落3为图片地址4为视频地址5为语音地址6为链接地址7为附件资源8为收费资源 */
type: number;
/** POST ID */
post_id: number;
/** 内容 */
content: string;
/** 排序,越小越靠前 */
sort: number;
/** 用户UID */
user_id?: number;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
/** 组件用帖子 */
interface PostComponentProps extends PostProps {
/** 文字段落列表 */
texts: PostItemProps[],
/** 图片列表 */
imgs: PostItemProps[],
/** 视频列表 */
videos: PostItemProps[],
/** 链接列表 */
links: PostItemProps[],
/** 附件列表 */
attachments: PostItemProps[],
/** 收费附件列表 */
charge_attachments: PostItemProps[]
}
/** 帖子 */
interface PostProps {
id: number;
/** 发帖人UID */
user_id: number;
/** 发帖人用户数据 */
user: UserInfo;
/** 附件价格(分) */
attachment_price: number;
/** 发帖时IP地址 */
ip?: string;
/** 发帖时城市地址 */
ip_loc: string;
/** 最新回复时间 */
latest_replied_on: number;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 点赞数 */
upvote_count: number;
/** 评论数 */
comment_count: number;
/** 收藏数 */
collection_count: number;
/** 内容列表 */
contents: PostItemProps[];
/** 标签列表 */
tags: { [key: string]: number } | string;
/** 是否锁定 */
is_lock: number;
/** 是否置顶 */
is_top: number;
/** 是否精华 */
is_essence: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
interface MessageProps {
id: number,
/** 类型1为动态2为评论3为回复4为私信99为系统通知 */
type: 1 | 2 | 3 | 4 | 99,
/** 摘要说明 */
breif: string,
/** 详细内容 */
content: string,
/** 是否已读0为未读1为已读 */
is_read: 0 | 1,
/** 发送人UID */
sender_user_id: number,
/** 发送人用户数据 */
sender_user: UserInfo,
/** 接收方UID */
receiver_user_id: number,
/** 帖子ID */
post_id: number,
/** 帖子内容 */
post: PostProps,
/** 评论ID */
comment_id: number,
/** 评论内容 */
comment: CommentProps,
/** 回复ID */
reply_id: number,
/** 回复内容 */
replay: ReplyProps,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1
}
/** 组件用帖子 */
interface PostComponentProps extends PostProps {
/** 文字段落列表 */
texts: PostItemProps[];
/** 图片列表 */
imgs: PostItemProps[];
/** 视频列表 */
videos: PostItemProps[];
/** 链接列表 */
links: PostItemProps[];
/** 附件列表 */
attachments: PostItemProps[];
/** 收费附件列表 */
charge_attachments: PostItemProps[];
}
interface AttachmentProps {
id: number,
/** 类别1为图片2为视频3为其他附件 */
type: 1 | 2 | 3,
/** 发布者用户UID */
user_id: number,
/** 发布者用户数据 */
user: UserInfo,
/** 文件大小 */
file_size: number,
/** 图片宽度 */
img_width?: number,
/** 图片高度 */
img_height?: number,
/** 内容 */
content: string,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1
}
interface MessageProps {
id: number;
/** 类型1为动态2为评论3为回复4为私信99为系统通知 */
type: 1 | 2 | 3 | 4 | 99;
/** 摘要说明 */
breif: string;
/** 详细内容 */
content: string;
/** 是否已读0为未读1为已读 */
is_read: 0 | 1;
/** 发送人UID */
sender_user_id: number;
/** 发送人用户数据 */
sender_user: UserInfo;
/** 接收方UID */
receiver_user_id: number;
/** 帖子ID */
post_id: number;
/** 帖子内容 */
post: PostProps;
/** 评论ID */
comment_id: number;
/** 评论内容 */
comment: CommentProps;
/** 回复ID */
reply_id: number;
/** 回复内容 */
replay: ReplyProps;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
interface TagProps {
id: number,
/** 创建者UID */
user_id: number,
/** 创建者用户数据 */
user: UserInfo,
/** 标签名 */
tag: string,
/** 引用数 */
quote_num: number,
/** 创建时间 */
created_on: number,
/** 修改时间 */
modified_on?: number,
/** 删除时间 */
deleted_on?: number,
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1
}
interface AttachmentProps {
id: number;
/** 类别1为图片2为视频3为其他附件 */
type: 1 | 2 | 3;
/** 发布者用户UID */
user_id: number;
/** 发布者用户数据 */
user: UserInfo;
/** 文件大小 */
file_size: number;
/** 图片宽度 */
img_width?: number;
/** 图片高度 */
img_height?: number;
/** 内容 */
content: string;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
interface PagerProps {
/** 当前页码 */
page: number,
/** 每页条数 */
page_size: number,
/** 总条数 */
total_rows: number
}
interface TagProps {
id: number;
/** 创建者UID */
user_id: number;
/** 创建者用户数据 */
user: UserInfo;
/** 标签名 */
tag: string;
/** 引用数 */
quote_num: number;
/** 创建时间 */
created_on: number;
/** 修改时间 */
modified_on?: number;
/** 删除时间 */
deleted_on?: number;
/** 是否删除0为未删除1为已删除 */
is_del?: 0 | 1;
}
interface BillProps {
id: number,
reason: string,
change_amount: number,
created_on: number
}
interface PagerProps {
/** 当前页码 */
page: number;
/** 每页条数 */
page_size: number;
/** 总条数 */
total_rows: number;
}
}
interface BillProps {
id: number;
reason: string;
change_amount: number;
created_on: number;
}
}

@ -2,8 +2,8 @@
export const parsePostTag = (content: string) => {
const tags: string[] = []
const users: string[] = []
var tagExp = /(#|)([^#@])+?\s+?/g // 这⾥中⽂#和英⽂#都会识别
var atExp = /@([a-zA-Z0-9])+?\s+?/g // 这⾥中⽂#和英⽂#都会识别
const tagExp = /(#|)([^#@])+?\s+?/g // 这⾥中⽂#和英⽂#都会识别
const atExp = /@([a-zA-Z0-9])+?\s+?/g // 这⾥中⽂#和英⽂#都会识别
content = content
.replace(/<[^>]*?>/gi, '')
.replace(/(.*?)<\/[^>]*?>/gi, '')

@ -1,8 +1,8 @@
// 滚动到顶部
export const scrollToTop = (scrollDuration: number) => {
var cosParameter = window.scrollY / 2;
var scrollCount = 0;
var oldTimestamp = performance.now();
const cosParameter = window.scrollY / 2;
let scrollCount = 0;
let oldTimestamp = performance.now();
function step(newTimestamp: number) {
scrollCount +=
Math.PI / (scrollDuration / (newTimestamp - oldTimestamp));

@ -1,19 +1,19 @@
<template>
<div>
<main-nav title="404" />
<div>
<main-nav title="404" />
<n-list class="main-content-wrap wrap404" bordered>
<n-result
status="404"
title="404 资源不存在"
description="再看看其他的吧"
>
<template #footer>
<n-button @click="goHome"></n-button>
</template>
</n-result>
</n-list>
</div>
<n-list class="main-content-wrap wrap404" bordered>
<n-result
status="404"
title="404 资源不存在"
description="再看看其他的吧"
>
<template #footer>
<n-button @click="goHome"></n-button>
</template>
</n-result>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -21,17 +21,17 @@ import { useRouter } from 'vue-router';
const router = useRouter();
const goHome = () => {
router.push({
path: '/',
});
router.push({
path: '/',
});
};
</script>
<style lang="less" scoped>
.wrap404 {
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</style>

@ -1,33 +1,33 @@
<template>
<div>
<main-nav title="收藏" />
<div>
<main-nav title="收藏" />
<n-list class="main-content-wrap" bordered>
<template #footer>
<div class="pagination-wrap" v-if="totalPage > 0">
<n-pagination
:page="page"
@update:page="updatePage"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
/>
</div>
</template>
<n-list class="main-content-wrap" bordered>
<template #footer>
<div v-if="totalPage > 0" class="pagination-wrap">
<n-pagination
:page="page"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
@update:page="updatePage"
/>
</div>
</template>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div v-if="list.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -47,27 +47,27 @@ const pageSize = ref(20);
const totalPage = ref(0);
const loadPosts = () => {
loading.value = true;
getCollections({
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
loading.value = true;
getCollections({
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
};
const updatePage = (p: number) => {
page.value = p;
loadPosts();
page.value = p;
loadPosts();
};
onMounted(() => {
loadPosts();
loadPosts();
});
</script>
</script>

@ -1,37 +1,37 @@
<template>
<div>
<main-nav :title="title" />
<div>
<main-nav :title="title" />
<n-list class="main-content-wrap" bordered>
<template #footer>
<div class="pagination-wrap" v-if="totalPage > 0">
<n-pagination
:page="page"
@update:page="updatePage"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
/>
</div>
</template>
<n-list-item>
<!-- -->
<compose @post-success="onPostSuccess" />
</n-list-item>
<n-list class="main-content-wrap" bordered>
<template #footer>
<div v-if="totalPage > 0" class="pagination-wrap">
<n-pagination
:page="page"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
@update:page="updatePage"
/>
</div>
</template>
<n-list-item>
<!-- -->
<compose @post-success="onPostSuccess" />
</n-list-item>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div v-if="list.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -50,91 +50,91 @@ const page = ref(+(route.query.p as string) || 1);
const pageSize = ref(20);
const totalPage = ref(0);
const title = computed(() => {
let t = '广';
let t = '广';
if (route.query && route.query.q) {
if (route.query.t && route.query.t === 'tag') {
t = '#' + decodeURIComponent(route.query.q as string);
} else {
t = ': ' + decodeURIComponent(route.query.q as string);
}
if (route.query && route.query.q) {
if (route.query.t && route.query.t === 'tag') {
t = '#' + decodeURIComponent(route.query.q as string);
} else {
t = ': ' + decodeURIComponent(route.query.q as string);
}
}
return t;
return t;
});
const loadPosts = () => {
loading.value = true;
getPosts({
query: route.query.q ? decodeURIComponent(route.query.q as string) : null,
type: route.query.t as string,
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
loading.value = true;
getPosts({
query: route.query.q ? decodeURIComponent(route.query.q as string) : null,
type: route.query.t as string,
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
};
const onPostSuccess = (post: Item.PostProps) => {
router.push({
name: 'post',
query: {
id: post.id,
},
});
router.push({
name: 'post',
query: {
id: post.id,
},
});
};
const updatePage = (p: number) => {
router.push({
name: 'home',
query: {
...route.query,
p,
},
});
router.push({
name: 'home',
query: {
...route.query,
p,
},
});
};
onMounted(() => {
loadPosts();
loadPosts();
});
watch(
() => ({
path: route.path,
query: route.query,
refresh: store.state.refresh,
}),
(to, from) => {
if (to.refresh !== from.refresh) {
page.value = +(route.query.p as string) || 1;
setTimeout(() => {
loadPosts();
}, 0);
return;
}
if (from.path !== '/post' && to.path === '/') {
page.value = +(route.query.p as string) || 1;
() => ({
path: route.path,
query: route.query,
refresh: store.state.refresh,
}),
(to, from) => {
if (to.refresh !== from.refresh) {
page.value = +(route.query.p as string) || 1;
setTimeout(() => {
loadPosts();
}, 0);
return;
}
if (from.path !== '/post' && to.path === '/') {
page.value = +(route.query.p as string) || 1;
setTimeout(() => {
loadPosts();
}, 0);
}
setTimeout(() => {
loadPosts();
}, 0);
}
},
);
</script>
<style lang="less" scoped>
.pagination-wrap {
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
}
</style>
</style>

@ -1,34 +1,34 @@
<template>
<div>
<main-nav title="提醒" />
<div>
<main-nav title="提醒" />
<n-list class="main-content-wrap messages-wrap" bordered>
<div v-if="loading" class="skeleton-wrap">
<message-skeleton :num="pageSize" />
</div>
<n-list class="main-content-wrap messages-wrap" bordered>
<div v-if="loading" class="skeleton-wrap">
<message-skeleton :num="pageSize" />
</div>
<div v-else>
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-else>
<div v-if="list.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="m in list" :key="m.id">
<message-item :message="m" />
</n-list-item>
</div>
<n-list-item v-for="m in list" :key="m.id">
<message-item :message="m" />
</n-list-item>
</div>
<template #footer>
<div class="pagination-wrap" v-if="totalPage > 0">
<n-pagination
:page="page"
@update:page="updatePage"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
/>
</div>
</template>
</n-list>
</div>
<template #footer>
<div v-if="totalPage > 0" class="pagination-wrap">
<n-pagination
:page="page"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
@update:page="updatePage"
/>
</div>
</template>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -45,34 +45,34 @@ const totalPage = ref(0);
const list = ref<Item.MessageProps[]>([]);
const loadMessages = () => {
loading.value = true;
getMessages({
page: page.value,
page_size: pageSize.value,
loading.value = true;
getMessages({
page: page.value,
page_size: pageSize.value,
})
.then((res) => {
loading.value = false;
list.value = res.list;
totalPage.value = Math.ceil(res.pager.total_rows / pageSize.value);
})
.then((res) => {
loading.value = false;
list.value = res.list;
totalPage.value = Math.ceil(res.pager.total_rows / pageSize.value);
})
.catch((err) => {
loading.value = false;
});
.catch((err) => {
loading.value = false;
});
};
const updatePage = (p: number) => {
page.value = p;
loadMessages();
page.value = p;
loadMessages();
};
onMounted(() => {
loadMessages();
loadMessages();
});
</script>
<style lang="less" scoped>
.pagination-wrap {
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
}
</style>
</style>

@ -1,48 +1,42 @@
<template>
<div>
<main-nav title="泡泡详情" :back="true" />
<div>
<main-nav title="泡泡详情" :back="true" />
<n-list class="main-content-wrap" bordered>
<n-list-item>
<n-spin :show="loading">
<div class="detail-wrap" v-if="post.id > 0">
<post-detail :post="post" @reload="loadPost" />
</div>
<div class="empty-wrap" v-else>
<n-empty size="large" description="暂无数据" />
</div>
</n-spin>
</n-list-item>
<n-list-item v-if="post.id > 0">
<compose-comment
:lock="post.is_lock"
:post-id="post.id"
@post-success="loadComments(true)"
/>
</n-list-item>
<n-list class="main-content-wrap" bordered>
<n-list-item>
<n-spin :show="loading">
<div v-if="post.id > 0" class="detail-wrap">
<post-detail :post="post" @reload="loadPost" />
</div>
<div v-else class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
</n-spin>
</n-list-item>
<n-list-item v-if="post.id > 0">
<compose-comment
:lock="post.is_lock"
:post-id="post.id"
@post-success="loadComments(true)"
/>
</n-list-item>
<div v-if="post.id > 0">
<div v-if="commentLoading" class="skeleton-wrap">
<post-skeleton :num="5" />
</div>
<div v-else>
<div class="empty-wrap" v-if="comments.length === 0">
<n-empty
size="large"
description="暂无评论,快来抢沙发"
/>
</div>
<div v-if="post.id > 0">
<div v-if="commentLoading" class="skeleton-wrap">
<post-skeleton :num="5" />
</div>
<div v-else>
<div v-if="comments.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无评论,快来抢沙发" />
</div>
<n-list-item v-for="comment in comments" :key="comment.id">
<comment-item
:comment="comment"
@reload="loadComments"
/>
</n-list-item>
</div>
</div>
</n-list>
</div>
<n-list-item v-for="comment in comments" :key="comment.id">
<comment-item :comment="comment" @reload="loadComments" />
</n-list-item>
</div>
</div>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -58,57 +52,57 @@ const comments = ref<Item.CommentProps[]>([]);
const postId = computed(() => +(route.query.id as string));
const loadPost = () => {
post.value = {
id: 0,
} as Item.PostProps;
loading.value = true;
getPost({
id: postId.value,
})
.then((res) => {
loading.value = false;
post.value = res;
post.value = {
id: 0,
} as Item.PostProps;
loading.value = true;
getPost({
id: postId.value,
})
.then((res) => {
loading.value = false;
post.value = res;
// 加载评论
loadComments();
})
.catch((err) => {
loading.value = false;
});
};
const loadComments = (scrollToBottom: boolean = false) => {
if (comments.value.length === 0) {
commentLoading.value = true;
}
getPostComments({
id: post.value.id as number,
// 加载评论
loadComments();
})
.then((res) => {
comments.value = res.list;
commentLoading.value = false;
.catch((err) => {
loading.value = false;
});
};
const loadComments = (scrollToBottom = false) => {
if (comments.value.length === 0) {
commentLoading.value = true;
}
getPostComments({
id: post.value.id as number,
})
.then((res) => {
comments.value = res.list;
commentLoading.value = false;
if (scrollToBottom) {
setTimeout(() => {
window.scrollTo(0, 99999);
}, 50);
}
})
.catch((err) => {
commentLoading.value = false;
});
if (scrollToBottom) {
setTimeout(() => {
window.scrollTo(0, 99999);
}, 50);
}
})
.catch((err) => {
commentLoading.value = false;
});
};
onMounted(() => {
loadPost();
loadPost();
});
watch(postId, () => {
if (postId.value > 0 && route.name === 'post') {
loadPost();
}
if (postId.value > 0 && route.name === 'post') {
loadPost();
}
});
</script>
<style lang="less" scoped>
.detail-wrap {
min-height: 100px;
min-height: 100px;
}
</style>
</style>

@ -1,53 +1,53 @@
<template>
<div>
<main-nav title="主页" />
<div>
<main-nav title="主页" />
<n-list
class="main-content-wrap profile-wrap"
bordered
v-if="store.state.userInfo.id > 0"
>
<!-- -->
<div class="profile-baseinfo">
<div class="avatar">
<n-avatar size="large" :src="store.state.userInfo.avatar" />
</div>
<div class="base-info">
<div class="username">
<strong>{{ store.state.userInfo.nickname }}</strong>
<span> @{{ store.state.userInfo.username }} </span>
</div>
<div class="uid">UID. {{ store.state.userInfo.id }}</div>
</div>
</div>
<template #footer>
<div class="pagination-wrap" v-if="totalPage > 0">
<n-pagination
:page="page"
@update:page="updatePage"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
/>
</div>
</template>
<n-tabs class="profile-tabs-wrap" animated>
<n-tab-pane name="post" tab="泡泡"> </n-tab-pane>
<!-- <n-tab-pane name="comment" tab="评论"> </n-tab-pane> -->
</n-tabs>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<n-list
v-if="store.state.userInfo.id > 0"
class="main-content-wrap profile-wrap"
bordered
>
<!-- -->
<div class="profile-baseinfo">
<div class="avatar">
<n-avatar size="large" :src="store.state.userInfo.avatar" />
</div>
<div class="base-info">
<div class="username">
<strong>{{ store.state.userInfo.nickname }}</strong>
<span> @{{ store.state.userInfo.username }} </span>
</div>
<div class="uid">UID. {{ store.state.userInfo.id }}</div>
</div>
</div>
<template #footer>
<div v-if="totalPage > 0" class="pagination-wrap">
<n-pagination
:page="page"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
@update:page="updatePage"
/>
</div>
</template>
<n-tabs class="profile-tabs-wrap" animated>
<n-tab-pane name="post" tab="泡泡"> </n-tab-pane>
<!-- <n-tab-pane name="comment" tab="评论"> </n-tab-pane> -->
</n-tabs>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div v-if="list.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -66,66 +66,66 @@ const pageSize = ref(20);
const totalPage = ref(0);
const loadPosts = () => {
loading.value = true;
getUserPosts({
username: store.state.userInfo.username,
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
loading.value = true;
getUserPosts({
username: store.state.userInfo.username,
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
};
const updatePage = (p: number) => {
page.value = p;
loadPosts();
page.value = p;
loadPosts();
};
onMounted(() => {
loadPosts();
loadPosts();
});
</script>
<style lang="less" scoped>
.profile-baseinfo {
display: flex;
padding: 16px;
.avatar {
width: 55px;
}
display: flex;
padding: 16px;
.avatar {
width: 55px;
}
.base-info {
position: relative;
width: calc(100% - 55px);
.base-info {
position: relative;
width: calc(100% - 55px);
.username {
line-height: 16px;
font-size: 16px;
}
.username {
line-height: 16px;
font-size: 16px;
}
.uid {
font-size: 14px;
line-height: 14px;
margin-top: 10px;
opacity: 0.75;
}
.uid {
font-size: 14px;
line-height: 14px;
margin-top: 10px;
opacity: 0.75;
}
}
}
.profile-tabs-wrap {
padding: 0 16px;
padding: 0 16px;
}
.pagination-wrap {
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
}
</style>
</style>

File diff suppressed because it is too large Load Diff

@ -1,33 +1,33 @@
<template>
<div>
<main-nav title="点赞" />
<div>
<main-nav title="点赞" />
<n-list class="main-content-wrap" bordered>
<template #footer>
<div class="pagination-wrap" v-if="totalPage > 0">
<n-pagination
:page="page"
@update:page="updatePage"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
/>
</div>
</template>
<n-list class="main-content-wrap" bordered>
<template #footer>
<div v-if="totalPage > 0" class="pagination-wrap">
<n-pagination
:page="page"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
@update:page="updatePage"
/>
</div>
</template>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div v-if="list.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -47,36 +47,36 @@ const pageSize = ref(20);
const totalPage = ref(0);
const loadPosts = () => {
loading.value = true;
getStars({
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
loading.value = true;
getStars({
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
};
const updatePage = (p: number) => {
page.value = p;
loadPosts();
page.value = p;
loadPosts();
};
onMounted(() => {
loadPosts();
loadPosts();
});
</script>
<style lang="less" scoped>
.pagination-wrap {
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
}
</style>
</style>

@ -1,42 +1,42 @@
<template>
<div>
<main-nav title="话题" />
<div>
<main-nav title="话题" />
<n-list class="main-content-wrap tags-wrap" bordered>
<n-tabs type="line" animated @update:value="changeTab">
<n-tab-pane name="hot" tab="热门" />
<n-tab-pane name="new" tab="最新" />
</n-tabs>
<n-spin :show="loading">
<n-space>
<n-tag
class="tag-item"
type="success"
round
v-for="tag in tags"
:key="tag.id"
>
<router-link
class="hash-link"
:to="{
name: 'home',
query: {
q: tag.tag,
t: 'tag',
},
}"
>
#{{ tag.tag }}
</router-link>
<span class="tag-hot">({{ tag.quote_num }})</span>
<template #avatar>
<n-avatar :src="tag.user.avatar" />
</template>
</n-tag>
</n-space>
</n-spin>
</n-list>
</div>
<n-list class="main-content-wrap tags-wrap" bordered>
<n-tabs type="line" animated @update:value="changeTab">
<n-tab-pane name="hot" tab="热门" />
<n-tab-pane name="new" tab="最新" />
</n-tabs>
<n-spin :show="loading">
<n-space>
<n-tag
v-for="tag in tags"
:key="tag.id"
class="tag-item"
type="success"
round
>
<router-link
class="hash-link"
:to="{
name: 'home',
query: {
q: tag.tag,
t: 'tag',
},
}"
>
#{{ tag.tag }}
</router-link>
<span class="tag-hot">({{ tag.quote_num }})</span>
<template #avatar>
<n-avatar :src="tag.user.avatar" />
</template>
</n-tag>
</n-space>
</n-spin>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -48,37 +48,37 @@ const tagType = ref('hot');
const loading = ref(false);
const loadTags = () => {
loading.value = true;
getTags({
type: tagType.value,
num: 50,
loading.value = true;
getTags({
type: tagType.value,
num: 50,
})
.then((res) => {
tags.value = res;
loading.value = false;
})
.then((res) => {
tags.value = res;
loading.value = false;
})
.catch((err) => {
loading.value = false;
});
.catch((err) => {
loading.value = false;
});
};
const changeTab = (tab: string) => {
tagType.value = tab;
loadTags();
tagType.value = tab;
loadTags();
};
onMounted(() => {
loadTags();
loadTags();
});
</script>
<style lang="less" scoped>
.tags-wrap {
padding: 20px;
.tag-item {
.tag-hot {
margin-left: 12px;
font-size: 12px;
opacity: 0.75;
}
padding: 20px;
.tag-item {
.tag-hot {
margin-left: 12px;
font-size: 12px;
opacity: 0.75;
}
}
}
</style>
</style>

@ -1,71 +1,67 @@
<template>
<div>
<main-nav title="用户详情" />
<n-list class="main-content-wrap profile-wrap" bordered>
<!-- -->
<n-spin :show="userLoading">
<div class="profile-baseinfo" v-if="user.id > 0">
<div class="avatar">
<n-avatar size="large" :src="user.avatar" />
</div>
<div class="base-info">
<div class="username">
<strong>{{ user.nickname }}</strong>
<span> @{{ user.username }} </span>
</div>
<div class="uid">UID. {{ user.id }}</div>
</div>
<div class="user-opts">
<n-space vertical>
<n-button
size="small"
secondary
type="primary"
@click="openWhisper"
>
</n-button>
</n-space>
</div>
</div>
<!-- -->
<whisper
:show="showWhisper"
:user="user"
@success="whisperSuccess"
/>
</n-spin>
<template #footer>
<div class="pagination-wrap" v-if="totalPage > 0">
<n-pagination
:page="page"
@update:page="updatePage"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
/>
</div>
</template>
<n-tabs class="profile-tabs-wrap" animated>
<n-tab-pane name="post" tab="泡泡" />
<!-- <n-tab-pane name="comment" tab="评论"> </n-tab-pane> -->
</n-tabs>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
<div>
<main-nav title="用户详情" />
<n-list class="main-content-wrap profile-wrap" bordered>
<!-- -->
<n-spin :show="userLoading">
<div v-if="user.id > 0" class="profile-baseinfo">
<div class="avatar">
<n-avatar size="large" :src="user.avatar" />
</div>
<div class="base-info">
<div class="username">
<strong>{{ user.nickname }}</strong>
<span> @{{ user.username }} </span>
</div>
<div v-else>
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
<div class="uid">UID. {{ user.id }}</div>
</div>
<div class="user-opts">
<n-space vertical>
<n-button
size="small"
secondary
type="primary"
@click="openWhisper"
>
</n-button>
</n-space>
</div>
</div>
<!-- -->
<whisper :show="showWhisper" :user="user" @success="whisperSuccess" />
</n-spin>
<template #footer>
<div v-if="totalPage > 0" class="pagination-wrap">
<n-pagination
:page="page"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
@update:page="updatePage"
/>
</div>
</template>
<n-tabs class="profile-tabs-wrap" animated>
<n-tab-pane name="post" tab="泡泡" />
<!-- <n-tab-pane name="comment" tab="评论"> </n-tab-pane> -->
</n-tabs>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div v-if="list.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="post in list" :key="post.id">
<post-item :post="post" />
</n-list-item>
</div>
</n-list>
</div>
</template>
<script setup lang="ts">
@ -79,10 +75,10 @@ const route = useRoute();
const loading = ref(false);
const user = reactive({
id: 0,
avatar: '',
username: '',
nickname: '',
id: 0,
avatar: '',
username: '',
nickname: '',
});
const userLoading = ref(false);
const showWhisper = ref(false);
@ -93,105 +89,105 @@ const pageSize = ref(20);
const totalPage = ref(0);
const loadPosts = () => {
loading.value = true;
getUserPosts({
username: username.value as string,
page: page.value,
page_size: pageSize.value,
loading.value = true;
getUserPosts({
username: username.value as string,
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
window.scrollTo(0, 0);
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
.catch((err) => {
loading.value = false;
});
};
const loadUser = () => {
userLoading.value = true;
getUserProfile({
username: username.value as string,
userLoading.value = true;
getUserProfile({
username: username.value as string,
})
.then((res) => {
userLoading.value = false;
user.id = res.id;
user.avatar = res.avatar;
user.username = res.username;
user.nickname = res.nickname;
loadPosts();
})
.then((res) => {
userLoading.value = false;
user.id = res.id;
user.avatar = res.avatar;
user.username = res.username;
user.nickname = res.nickname;
loadPosts();
})
.catch((err) => {
userLoading.value = false;
console.log(err);
});
.catch((err) => {
userLoading.value = false;
console.log(err);
});
};
const updatePage = (p: number) => {
page.value = p;
loadPosts();
page.value = p;
loadPosts();
};
const openWhisper = () => {
// window.$message.warning('您尚未获得私信权限');
showWhisper.value = true;
// window.$message.warning('您尚未获得私信权限');
showWhisper.value = true;
};
const whisperSuccess = () => {
showWhisper.value = false;
showWhisper.value = false;
};
watch(
() => ({
path: route.path,
query: route.query,
}),
(to, from) => {
if (from.path === '/user' && to.path === '/user') {
username.value = route.query.username || '';
loadUser();
}
() => ({
path: route.path,
query: route.query,
}),
(to, from) => {
if (from.path === '/user' && to.path === '/user') {
username.value = route.query.username || '';
loadUser();
}
},
);
onMounted(() => {
loadUser();
loadUser();
});
</script>
<style lang="less" scoped>
.profile-tabs-wrap {
padding: 0 16px;
padding: 0 16px;
}
.profile-baseinfo {
display: flex;
padding: 16px;
display: flex;
padding: 16px;
.avatar {
width: 55px;
}
.base-info {
position: relative;
width: calc(100% - 55px);
.avatar {
width: 55px;
.username {
line-height: 16px;
font-size: 16px;
}
.base-info {
position: relative;
width: calc(100% - 55px);
.username {
line-height: 16px;
font-size: 16px;
}
.uid {
font-size: 14px;
line-height: 14px;
margin-top: 10px;
opacity: 0.75;
}
.uid {
font-size: 14px;
line-height: 14px;
margin-top: 10px;
opacity: 0.75;
}
}
}
.pagination-wrap {
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
}
</style>
</style>

@ -1,146 +1,137 @@
<template>
<div>
<main-nav title="钱包" />
<div>
<main-nav title="钱包" />
<n-list class="main-content-wrap" bordered>
<div class="balance-wrap">
<div class="balance-line">
<n-statistic label="账户余额 (元)"
><n-number-animation
:from="0.0"
:to="(store.state.userInfo.balance || 0) / 100"
:duration="500"
:precision="2"
/>
</n-statistic>
<div class="balance-opts">
<n-space vertical>
<n-button
size="small"
secondary
type="primary"
@click="doRecharge"
>
</n-button>
<n-button
size="small"
secondary
type="tertiary"
@click="doWithdraw"
>
</n-button>
</n-space>
</div>
</div>
</div>
<n-list class="main-content-wrap" bordered>
<div class="balance-wrap">
<div class="balance-line">
<n-statistic label="账户余额 (元)"
><n-number-animation
:from="0.0"
:to="(store.state.userInfo.balance || 0) / 100"
:duration="500"
:precision="2"
/>
</n-statistic>
<div class="balance-opts">
<n-space vertical>
<n-button
size="small"
secondary
type="primary"
@click="doRecharge"
>
</n-button>
<n-button
size="small"
secondary
type="tertiary"
@click="doWithdraw"
>
</n-button>
</n-space>
</div>
</div>
</div>
<template #footer>
<div class="pagination-wrap" v-if="totalPage > 0">
<n-pagination
:page="page"
@update:page="updatePage"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
/>
</div>
</template>
<template #footer>
<div v-if="totalPage > 0" class="pagination-wrap">
<n-pagination
:page="page"
:page-slot="!store.state.collapsedRight ? 8 : 5"
:page-count="totalPage"
@update:page="updatePage"
/>
</div>
</template>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div class="empty-wrap" v-if="list.length === 0">
<n-empty size="large" description="暂无数据" />
</div>
<div v-if="loading" class="skeleton-wrap">
<post-skeleton :num="pageSize" />
</div>
<div v-else>
<div v-if="list.length === 0" class="empty-wrap">
<n-empty size="large" description="暂无数据" />
</div>
<n-list-item v-for="bill in list" :key="bill.id">
<div class="bill-line">
<div>NO.{{ bill.id }}</div>
<div>{{ bill.reason }}</div>
<div
:class="{
income: bill.change_amount >= 0,
out: bill.change_amount < 0,
}"
>
{{
(bill.change_amount > 0 ? '+' : '') +
(bill.change_amount / 100).toFixed(2)
}}
</div>
<div>
{{ formatRelativeTime(bill.created_on) }}
</div>
</div>
</n-list-item>
<n-list-item v-for="bill in list" :key="bill.id">
<div class="bill-line">
<div>NO.{{ bill.id }}</div>
<div>{{ bill.reason }}</div>
<div
:class="{
income: bill.change_amount >= 0,
out: bill.change_amount < 0,
}"
>
{{
(bill.change_amount > 0 ? '+' : '') +
(bill.change_amount / 100).toFixed(2)
}}
</div>
</n-list>
<n-modal v-model:show="showRecharge">
<n-card
:bordered="false"
title="请选择充值金额"
role="dialog"
aria-modal="true"
style="width: 100%; max-width: 330px"
<div>
{{ formatRelativeTime(bill.created_on) }}
</div>
</div>
</n-list-item>
</div>
</n-list>
<n-modal v-model:show="showRecharge">
<n-card
:bordered="false"
title="请选择充值金额"
role="dialog"
aria-modal="true"
style="width: 100%; max-width: 330px"
>
<div v-if="rechargeQrcode.length === 0" class="amount-options">
<n-space align="baseline">
<n-button
v-for="amount in openAmounts"
:key="amount"
size="small"
secondary
:type="selectedRechargeAmount === amount ? 'info' : 'default'"
@click.stop="selectedRechargeAmount = amount"
>
<div class="amount-options" v-if="rechargeQrcode.length === 0">
<n-space align="baseline">
<n-button
v-for="amount in openAmounts"
:key="amount"
size="small"
secondary
:type="
selectedRechargeAmount === amount
? 'info'
: 'default'
"
@click.stop="selectedRechargeAmount = amount"
>
{{ amount / 100 }}
</n-button>
</n-space>
</div>
<div
v-if="
selectedRechargeAmount > 0 &&
rechargeQrcode.length === 0
"
style="margin-top: 10px"
>
<n-button
:loading="recharging"
strong
secondary
type="info"
style="width: 100%"
@click="handleRecharge"
>
<template #icon>
<n-icon><logo-alipay /></n-icon>
</template>
</n-button>
</div>
<div class="qrcode-wrap" v-show="rechargeQrcode.length > 0">
<canvas id="qrcode-container"></canvas>
<div class="pay-tips">
使{{
(selectedRechargeAmount / 100).toFixed(2)
}}
</div>
<div class="pay-sub-tips">
<n-badge :value="100" type="info" dot processing />
<span style="margin-left: 6px">
...
</span>
</div>
</div>
</n-card>
</n-modal>
</div>
{{ amount / 100 }}
</n-button>
</n-space>
</div>
<div
v-if="selectedRechargeAmount > 0 && rechargeQrcode.length === 0"
style="margin-top: 10px"
>
<n-button
:loading="recharging"
strong
secondary
type="info"
style="width: 100%"
@click="handleRecharge"
>
<template #icon>
<n-icon><logo-alipay /></n-icon>
</template>
</n-button>
</div>
<div v-show="rechargeQrcode.length > 0" class="qrcode-wrap">
<canvas id="qrcode-container"></canvas>
<div class="pay-tips">
使{{
(selectedRechargeAmount / 100).toFixed(2)
}}
</div>
<div class="pay-sub-tips">
<n-badge :value="100" type="info" dot processing />
<span style="margin-left: 6px"> ... </span>
</div>
</div>
</n-card>
</n-modal>
</div>
</template>
<script setup lang="ts">
@ -170,158 +161,154 @@ const totalPage = ref(0);
const openAmounts = ref([100, 200, 300, 500, 1000, 3000, 5000, 10000, 50000]);
const loadPosts = () => {
loading.value = true;
getBills({
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
loading.value = true;
getBills({
page: page.value,
page_size: pageSize.value,
})
.then((rsp) => {
loading.value = false;
list.value = rsp.list;
totalPage.value = Math.ceil(rsp.pager.total_rows / pageSize.value);
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
window.scrollTo(0, 0);
})
.catch((err) => {
loading.value = false;
});
};
const updatePage = (p: number) => {
page.value = p;
loadPosts();
page.value = p;
loadPosts();
};
const loadWallet = () => {
// 获取最新
const token = localStorage.getItem('PAOPAO_TOKEN') || '';
if (token) {
userInfo(token)
.then((res) => {
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
loadPosts();
})
.catch((err) => {
store.commit('triggerAuth', true);
store.commit('userLogout');
});
} else {
// 获取最新
const token = localStorage.getItem('PAOPAO_TOKEN') || '';
if (token) {
userInfo(token)
.then((res) => {
store.commit('updateUserinfo', res);
store.commit('triggerAuth', false);
loadPosts();
})
.catch((err) => {
store.commit('triggerAuth', true);
store.commit('userLogout');
}
});
} else {
store.commit('triggerAuth', true);
store.commit('userLogout');
}
};
const doRecharge = () => {
showRecharge.value = true;
showRecharge.value = true;
};
const handleRecharge = (amount: any) => {
recharging.value = true;
reqRecharge({
amount: selectedRechargeAmount.value,
})
.then((res) => {
recharging.value = false;
rechargeQrcode.value = res.pay;
recharging.value = true;
reqRecharge({
amount: selectedRechargeAmount.value,
})
.then((res) => {
recharging.value = false;
rechargeQrcode.value = res.pay;
// 生成二维码
QRCode.toCanvas(
document.querySelector('#qrcode-container'),
res.pay,
{
width: 150,
margin: 2,
}
);
// 生成二维码
QRCode.toCanvas(document.querySelector('#qrcode-container'), res.pay, {
width: 150,
margin: 2,
});
const s = setInterval(() => {
getRecharge({
id: res.id,
})
.then((res) => {
if (res.status === 'TRADE_SUCCESS') {
clearInterval(s);
window.$message.success('');
const s = setInterval(() => {
getRecharge({
id: res.id,
})
.then((res) => {
if (res.status === 'TRADE_SUCCESS') {
clearInterval(s);
window.$message.success('');
showRecharge.value = false;
rechargeQrcode.value = '';
showRecharge.value = false;
rechargeQrcode.value = '';
loadWallet();
}
})
.catch((err) => {
console.log(err);
});
}, 2000);
})
.catch((err) => {
recharging.value = false;
});
loadWallet();
}
})
.catch((err) => {
console.log(err);
});
}, 2000);
})
.catch((err) => {
recharging.value = false;
});
};
const doWithdraw = () => {
if (store.state.userInfo.balance == 0) {
window.$message.warning('');
} else {
window.$message.warning('');
}
if (store.state.userInfo.balance == 0) {
window.$message.warning('');
} else {
window.$message.warning('');
}
};
onMounted(() => {
loadWallet();
loadWallet();
});
</script>
<style lang="less" scoped>
.balance-wrap {
padding: 16px;
padding: 16px;
.balance-line {
display: flex;
justify-content: space-between;
.balance-line {
display: flex;
justify-content: space-between;
.balance-opts {
display: flex;
flex-direction: column;
}
.balance-opts {
display: flex;
flex-direction: column;
}
}
}
.bill-line {
padding: 16px;
display: flex;
justify-content: space-between;
.income,
.out {
font-weight: bold;
}
padding: 16px;
display: flex;
justify-content: space-between;
.income,
.out {
font-weight: bold;
}
.income {
color: #18a058;
}
.income {
color: #18a058;
}
}
.pagination-wrap {
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
padding: 10px;
display: flex;
justify-content: center;
overflow: hidden;
}
.qrcode-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.pay-tips {
margin-top: 16px;
}
.pay-sub-tips {
margin-top: 4px;
font-size: 12px;
opacity: 0.75;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.pay-tips {
margin-top: 16px;
}
.pay-sub-tips {
margin-top: 4px;
font-size: 12px;
opacity: 0.75;
display: flex;
align-items: center;
}
}
}
.dark {
.income {
color: #63e2b7;
}
.income {
color: #63e2b7;
}
}
</style>
</style>

Loading…
Cancel
Save