particularly complete forum function, add point order export and user delete

This commit is contained in:
2024-03-22 01:47:55 +08:00
parent 87df8d5966
commit 932b83ddcd
52 changed files with 2806 additions and 132 deletions

View File

@@ -3,7 +3,7 @@ import { isDarkMode } from '@/Utils'
import { useAccount } from '@/api/account'
import { QueryGetAPI } from '@/api/query'
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
import { AVATAR_URL, BASE_API, EVENT_API_URL } from '@/data/constants'
import { AVATAR_URL, BASE_API_URL, EVENT_API_URL } from '@/data/constants'
import { Grid28Filled, List16Filled } from '@vicons/fluent'
import { format } from 'date-fns'
import { saveAs } from 'file-saver'
@@ -253,7 +253,7 @@ function objectsToCSV(arr: any[]) {
:color="{
color: selectedType == EventType.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
textColor: 'white',
borderColor: isDarkMode() ? 'white' : '#00000000',
borderColor: isDarkMode ? 'white' : '#00000000',
}"
>
{{ item.price }}

View File

@@ -0,0 +1,249 @@
<script setup lang="ts">
import { useAccount } from '@/api/account'
import { QueryPostAPI } from '@/api/query'
import { useForumStore } from '@/store/useForumStore'
import {
DataTableColumns,
NAlert,
NButton,
NCard,
NCheckbox,
NDataTable,
NDescriptions,
NDescriptionsItem,
NDivider,
NFlex,
NInput,
NInputGroup,
NInputGroupLabel,
NModal,
NSelect,
NSpin,
NTabPane,
NTabs,
NTag,
NTime,
useMessage,
} from 'naive-ui'
import { h, ref } from 'vue'
import { ForumModel, ForumUserModel } from '@/api/models/forum'
import { FORUM_API_URL } from '@/data/constants'
// @ts-ignore
import Agreement from '@/document/EnableForumAgreement.md'
import { UserBasicInfo } from '@/api/api-models'
const useForum = useForumStore()
const accountInfo = useAccount()
const message = useMessage()
const managedForums = ref((await useForum.GetManagedForums()) ?? [])
const currentForum = ref((await useForum.GetForumInfo(accountInfo.value.id)) ?? ({} as ForumModel))
const selectedForum = ref(accountInfo.value.id)
const readedAgreement = ref(false)
const showAgreement = ref(false)
const create_Name = ref('')
const create_Description = ref('')
const paginationSetting = { defaultPageSize: 20, showSizePicker: true, pageSizes: [20, 50, 100] }
async function createForum() {
if (!readedAgreement.value) {
message.warning('请先阅读并同意服务协议')
return
}
if (!create_Name.value) {
message.warning('请输入名称')
return
}
try {
const data = await QueryPostAPI<ForumModel>(FORUM_API_URL + 'create', {
name: create_Name.value,
})
if (data.code == 200) {
message.success('创建成功')
currentForum.value = data.data
} else {
message.error('创建失败:' + data.message)
console.error(data.message)
}
} catch (err) {
console.error(err)
message.error('创建失败:' + err)
}
}
async function SwitchForum(owner: number) {
currentForum.value = (await useForum.GetForumInfo(owner)) ?? ({} as ForumModel)
}
const defaultColumns: DataTableColumns<ForumUserModel> = [
{
title: '名称',
key: 'name',
},
{
title: 'B站账号',
key: 'biliId',
render(row) {
return h(NTag, { type: row.isBiliAuthed ? 'success' : 'warning' }, () => (row.isBiliAuthed ? '已绑定' : '未绑定'))
},
},
]
const applyingColumns: DataTableColumns<ForumUserModel> = [
...defaultColumns,
{
title: '操作',
key: 'action',
render(row) {
return h(
NButton,
{
text: true,
type: 'success',
onClick: () =>
useForum.ConfirmApply(currentForum.value.owner.id, row.id).then((success) => {
if (success) message.success('操作成功')
currentForum.value.applying = currentForum.value.applying.filter((u) => u.id != row.id)
}),
},
{ default: () => '通过申请' },
)
},
},
]
const memberColumns: DataTableColumns<ForumUserModel> = [
...defaultColumns,
{
title: '操作',
key: 'action',
render(row) {
return h(
NButton,
{
text: true,
type: 'success',
onClick: () =>
useForum.ConfirmApply(currentForum.value.owner.id, row.id).then((success) => {
if (success) message.success('操作成功')
currentForum.value.applying = currentForum.value.applying.filter((u) => u.id != row.id)
}),
},
{ default: () => '通过申请' },
)
},
},
]
const banColumns: DataTableColumns<ForumUserModel> = [
...defaultColumns,
{
title: '操作',
key: 'action',
render(row) {
return h(
NButton,
{
text: true,
type: 'success',
onClick: () =>
useForum.ConfirmApply(currentForum.value.owner.id, row.id).then((success) => {
if (success) message.success('操作成功')
currentForum.value.applying = currentForum.value.applying.filter((u) => u.id != row.id)
}),
},
{ default: () => '通过申请' },
)
},
},
]
</script>
<template>
<NCard v-if="!currentForum.name" size="small" title="啊哦">
<NAlert type="error"> 你尚未创建粉丝讨论区 </NAlert>
<NDivider />
<NFlex justify="center">
<NFlex vertical>
<NButton @click="createForum" size="large" type="primary"> 创建粉丝讨论区 </NButton>
<NInputGroup>
<NInputGroupLabel> 名称 </NInputGroupLabel>
<NInput v-model:value="create_Name" placeholder="就是名称" maxlength="20" minlength="1" show-count />
</NInputGroup>
<NInput
v-model:value="create_Description"
placeholder="(可选) 公告/描述"
maxlength="200"
show-count
type="textarea"
/>
<NCheckbox v-model:checked="readedAgreement">
已阅读并同意 <NButton @click="showAgreement = true" text type="info"> 服务协议 </NButton>
</NCheckbox>
</NFlex>
</NFlex>
</NCard>
<template v-else>
<NSpin :show="useForum.isLoading">
<NSelect
v-model:value="selectedForum"
:options="
managedForums.map((f) => ({
label: (f.owner.id == accountInfo.id ? '[我的] ' : '') + f.name + ` (${f.owner.name})`,
value: f.owner.id,
}))
"
@update:value="(v) => SwitchForum(v)"
>
<template #header>
<NButton @click="SwitchForum(accountInfo.id)" size="small" type="primary"> 我的粉丝讨论区 </NButton>
</template>
</NSelect>
<NDivider />
<NTabs animated v-bind:key="selectedForum" type="segment">
<NTabPane tab="信息" name="info">
<NDescriptions bordered size="small">
<NDescriptionsItem label="名称"> {{ currentForum.name }} </NDescriptionsItem>
<NDescriptionsItem label="公告"> {{ currentForum.description ?? '无' }} </NDescriptionsItem>
<NDescriptionsItem label="创建者"> {{ currentForum.owner.name }} </NDescriptionsItem>
<NDescriptionsItem label="创建时间"> <NTime :time="currentForum.createAt" /> </NDescriptionsItem>
<NDescriptionsItem label="帖子数量"> {{ currentForum.topicCount }} </NDescriptionsItem>
<NDescriptionsItem v-if="currentForum.settings.requireApply" label="成员数量">
{{ currentForum.members?.length ?? 0 }}
</NDescriptionsItem>
<NDescriptionsItem label="管理员数量"> {{ currentForum.admins?.length ?? 0 }} </NDescriptionsItem>
</NDescriptions>
<NDivider> 设置 </NDivider>
</NTabPane>
<NTabPane tab="成员" name="member">
<NDivider> 申请 </NDivider>
<NDataTable
:columns="applyingColumns"
:data="currentForum.applying"
:pagination="paginationSetting"
/>
<template v-if="currentForum.settings.requireApply">
<NDivider> 成员 </NDivider>
<NDataTable
:columns="memberColumns"
:data="currentForum.members.sort((a, b) => (a.isAdmin ? 1 : 0) - (b.isAdmin ? 1 : 0))"
:pagination="paginationSetting"
/>
</template>
<NDivider> 封禁用户 </NDivider>
<NDataTable
:columns="banColumns"
:data="currentForum.blackList"
:pagination="paginationSetting"
/>
</NTabPane>
</NTabs>
</NSpin>
</template>
<NModal
v-model:show="showAgreement"
preset="card"
title="开通粉丝讨论区用户协议"
style="width: 600px; max-width: 90vw"
>
<Agreement />
</NModal>
</template>

View File

@@ -2,6 +2,7 @@
import { useAccount } from '@/api/account'
import { ResponseLiveInfoModel } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
import LiveInfoContainer from '@/components/LiveInfoContainer.vue'
import { LIVE_API_URL } from '@/data/constants'
import { NAlert, NButton, NDivider, NList, NListItem, NPagination, NSpace, useMessage } from 'naive-ui'
@@ -43,7 +44,6 @@ function OnClickCover(live: ResponseLiveInfoModel) {
<template>
<NSpace vertical>
<NAlert type="warning"> 测试功能, 尚不稳定 </NAlert>
<NAlert type="error" title="2024.2.26">
近期逸站对开放平台直播弹幕流进行了极为严格的限制, 目前本站服务器只能连接个位数的直播间, 这使得在不使用
<NButton tag="a" href="https://www.yuque.com/megghy/dez70g/vfvcyv3024xvaa1p" target="_blank" type="primary" text>
@@ -57,6 +57,8 @@ function OnClickCover(live: ResponseLiveInfoModel) {
</NButton>
, 否则只能记录直播的时间而不包含弹幕
</NAlert>
<EventFetcherStatusCard />
</NSpace>
<NDivider />
<NAlert v-if="accountInfo?.isBiliVerified != true" type="info"> 尚未进行Bilibili认证 </NAlert>

View File

@@ -160,7 +160,10 @@ onMounted(() => {
<template>
<NSpace align="center">
<NAlert type="info" style="max-width: 200px">
<NAlert
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.QuestionBox) ? 'success' : 'warning'"
style="max-width: 200px"
>
启用提问箱
<NDivider vertical />
<NSwitch

View File

@@ -262,7 +262,10 @@ onMounted(() => {
<template>
<NSpace align="center">
<NAlert type="info" style="max-width: 200px">
<NAlert
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Schedule) ? 'success' : 'warning'"
style="max-width: 200px"
>
启用日程表
<NDivider vertical />
<NSwitch

View File

@@ -571,7 +571,10 @@ onMounted(async () => {
<template>
<NSpace align="center">
<NAlert type="info" style="max-width: 200px">
<NAlert
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongList) ? 'success' : 'warning'"
style="max-width: 200px"
>
启用歌单
<NDivider vertical />
<NSwitch

View File

@@ -127,7 +127,11 @@ function createTable() {
</script>
<template>
<NAlert type="info" v-if="accountInfo.id">
<NAlert
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.VideoCollect) ? 'success' : 'warning'"
v-if="accountInfo.id"
style="max-width: 300px"
>
在个人主页展示进行中的征集表
<NSwitch
:value="accountInfo.settings.enableFunctions.includes(FunctionTypes.VideoCollect)"

View File

@@ -337,7 +337,14 @@ onMounted(() => {})
<template>
<NFlex>
<NAlert type="info" style="min-width: 400px">
<NAlert
:type="
accountInfo.settings.enableFunctions.includes(FunctionTypes.Point) && accountInfo.eventFetcherState.online
? 'success'
: 'warning'
"
style="min-width: 400px"
>
启用
<NButton text type="primary" tag="a" href="https://www.yuque.com/megghy/dez70g/ohulp2torghlqqn8" target="_blank">
积分系统
@@ -369,6 +376,9 @@ onMounted(() => {})
<NTabPane name="goods" tab="礼物">
<NFlex>
<NButton type="primary" @click="onModalOpen"> 添加礼物 </NButton>
<NButton @click="$router.push({ name: 'user-goods', params: { id: accountInfo?.name } })" secondary>
前往展示页
</NButton>
</NFlex>
<NDivider />
<NEmpty v-if="goods.filter((g) => g.status != GoodsStatus.Discontinued).length == 0" description="暂无礼物" />

View File

@@ -1,21 +1,47 @@
<script setup lang="ts">
import { ResponsePointGoodModel, ResponsePointOrder2OwnerModel } from '@/api/api-models'
import { useAccount } from '@/api/account'
import { GoodsTypes, PointOrderStatus, ResponsePointGoodModel, ResponsePointOrder2OwnerModel } from '@/api/api-models'
import { QueryGetAPI } from '@/api/query'
import PointOrderCard from '@/components/manage/PointOrderCard.vue'
import { POINT_API_URL } from '@/data/constants'
import { NEmpty, useMessage } from 'naive-ui'
import { onMounted, ref } from 'vue'
import { objectsToCSV } from '@/Utils'
import { useStorage } from '@vueuse/core'
import { format } from 'date-fns'
import { saveAs } from 'file-saver'
import { NButton, NCard, NCheckbox, NDivider, NEmpty, NFlex, NSelect, NSpin, useMessage } from 'naive-ui'
import { computed, onMounted, ref } from 'vue'
type OrderFilterSettings = {
type?: GoodsTypes
status?: PointOrderStatus
onlyRequireShippingInfo: boolean
}
const props = defineProps<{
goods: ResponsePointGoodModel[]
}>()
const defaultSettings = {
onlyRequireShippingInfo: false,
} as OrderFilterSettings
const filterSettings = useStorage<OrderFilterSettings>('Setting.Point.OrderFilter', defaultSettings)
const message = useMessage()
const accountInfo = useAccount()
const orders = ref<ResponsePointOrder2OwnerModel[]>([])
const filteredOrders = computed(() => {
return orders.value.filter((o) => {
if (filterSettings.value.type != undefined && o.type !== filterSettings.value.type) return false
if (filterSettings.value.status != undefined && o.status !== filterSettings.value.status) return false
if (filterSettings.value.onlyRequireShippingInfo && o.trackingNumber) return false
return true
})
})
const isLoading = ref(false)
async function getOrders() {
try {
isLoading.value = true
const data = await QueryGetAPI<ResponsePointOrder2OwnerModel[]>(POINT_API_URL + 'get-orders')
if (data.code == 200) {
return data.data
@@ -25,9 +51,53 @@ async function getOrders() {
} catch (err) {
console.log(err)
message.error('获取订单失败: ' + err)
} finally {
isLoading.value = false
}
return []
}
const statusText = {
[PointOrderStatus.Completed]: '已完成',
[PointOrderStatus.Pending]: '等待发货',
[PointOrderStatus.Shipped]: '已发货',
}
function exportData() {
const text = objectsToCSV(
filteredOrders.value.map((s) => {
const gift = props.goods.find((g) => g.id == s.goodsId)
return {
订单号: s.id,
订单类型: s.type == GoodsTypes.Physical ? '实体' : '虚拟',
订单状态: statusText[s.status],
用户名: s.customer.name ?? '未知',
用户UID: s.customer.userId,
联系人: s.address?.name,
联系电话: s.address?.phone,
地址: s.address
? `${s.address?.province}${s.address?.city}${s.address?.district}${s.address?.street}街道${s.address?.address}`
: '无',
礼物名: gift?.name ?? '已删除',
礼物数量: s.count,
礼物单价: gift?.price,
礼物总价: s.point,
快递公司: s.expressCompany,
快递单号: s.trackingNumber,
创建时间: format(s.createAt, 'yyyy-MM-dd HH:mm:ss'),
更新时间: s.updateAt ? format(s.updateAt, 'yyyy-MM-dd HH:mm:ss') : '未更新',
}
}),
)
const BOM = new Uint8Array([0xef, 0xbb, 0xbf])
const utf8encoder = new TextEncoder()
const utf8array = utf8encoder.encode(text)
saveAs(
new Blob([BOM, utf8array], { type: 'text/csv;charset=utf-8;' }),
`积分订单_${format(Date.now(), 'yyyy-MM-dd HH:mm:ss')}_${accountInfo.value?.name}_.csv`,
)
}
async function refresh() {
orders.value = await getOrders()
}
onMounted(async () => {
orders.value = await getOrders()
@@ -35,6 +105,48 @@ onMounted(async () => {
</script>
<template>
<NEmpty v-if="orders.length == 0" description="暂无订单"></NEmpty>
<PointOrderCard v-else :order="orders" :goods="goods" type="owner" />
<NSpin :show="isLoading">
<NEmpty v-if="filteredOrders.length == 0" description="暂无订单"></NEmpty>
<template v-else>
<br />
<NFlex>
<NButton @click="refresh">刷新</NButton>
<NButton @click="exportData" secondary type="info">导出数据</NButton>
</NFlex>
<NDivider />
<NCard size="small" title="筛选订单">
<template #header-extra>
<NButton @click="filterSettings = JSON.parse(JSON.stringify(defaultSettings))" size="small" type="warning">
重置
</NButton>
</template>
<NFlex align="center">
<NSelect
v-model:value="filterSettings.type"
:options="[
{ label: '实体', value: GoodsTypes.Physical },
{ label: '虚拟', value: GoodsTypes.Virtual },
]"
clearable
placeholder="订单类型"
style="width: 150px"
/>
<NSelect
v-model:value="filterSettings.status"
:options="[
{ label: '已完成', value: PointOrderStatus.Completed },
{ label: '等待发货', value: PointOrderStatus.Pending },
{ label: '已发货', value: PointOrderStatus.Shipped },
]"
placeholder="订单状态"
clearable
style="width: 150px"
/>
<NCheckbox v-model:checked="filterSettings.onlyRequireShippingInfo" label="仅包含未填写快递单号的订单" />
</NFlex>
</NCard>
<NDivider />
<PointOrderCard :order="filteredOrders" :goods="goods" type="owner" />
</template>
</NSpin>
</template>

View File

@@ -111,6 +111,14 @@ async function updateGift() {
</script>
<template>
<NAlert v-if="!accountInfo.eventFetcherState.online" type="warning">
由于你尚未部署
<NButton text type="primary" tag="a" href="https://www.yuque.com/megghy/dez70g/vfvcyv3024xvaa1p" target="_blank">
VtsuruEventFetcher
</NButton>
, 以下选项设置了也没用
</NAlert>
<br />
<NAlert type="info"> 积分总是最多保留两位小数, 四舍五入 </NAlert>
<NDivider> 常用 </NDivider>
<NSpin :show="isLoading">

View File

@@ -133,6 +133,22 @@ const column: DataTableColumns<ResponsePointUserModel> = [
},
{ default: () => '详情' },
),
h(
NPopconfirm,
{ onPositiveClick: () => deleteUser(row) },
{
default: '确定要删除这个用户吗?记录将无法恢复',
trigger: () =>
h(
NButton,
{
type: 'error',
size: 'small',
},
{ default: () => '删除' },
),
},
),
])
},
},
@@ -176,7 +192,9 @@ async function givePoint() {
if (data.code == 200) {
message.success('添加成功')
showGivePointModal.value = false
await refresh()
setTimeout(() => {
refresh()
}, 1500)
addPointCount.value = 0
addPointReason.value = undefined
@@ -186,6 +204,37 @@ async function givePoint() {
}
} catch (err) {
message.error('添加失败: ' + err)
} finally {
isLoading.value = false
}
}
async function deleteUser(user: ResponsePointUserModel) {
isLoading.value = true
try {
const data = await QueryGetAPI(
POINT_API_URL + 'delete-user',
user.isAuthed
? {
authId: user.info.id,
}
: user.info.userId
? {
uId: user.info.userId,
}
: {
uId: user.info.openId,
},
)
if (data.code == 200) {
message.success('已删除')
users.value = users.value.filter((u) => u != user)
} else {
message.error('删除失败: ' + data.message)
}
} catch (err) {
message.error('删除失败: ' + err)
} finally {
isLoading.value = false
}
}
@@ -215,6 +264,7 @@ onMounted(async () => {
<NCheckbox v-model:checked="settings.onlyAuthed"> 只显示已认证用户 </NCheckbox>
</NFlex>
</NCard>
<NDivider />
<template v-if="filteredUsers.length == 0">
<NDivider />
<NEmpty :description="settings.onlyAuthed ? '没有已认证的用户' : '没有用户'" />