feat: 更新组件和API模型,优化用户体验

- 在api-models.ts中将goodsId字段更改为goods,以更好地表示商品信息
- 在多个组件中添加NEllipsis组件以优化文本显示
- 在AddressDisplay.vue中调整模板格式,提升可读性
- 在PointOrderCard.vue中更新订单信息的显示逻辑
- 在PointOrderManage.vue中添加批量更新订单状态的功能
This commit is contained in:
2025-05-05 04:10:16 +08:00
parent f90f2057bb
commit 010309ce16
8 changed files with 394 additions and 200 deletions

View File

@@ -17,3 +17,12 @@
- `open_live/`: 直播相关视图,包括点歌系统 - `open_live/`: 直播相关视图,包括点歌系统
- `obs/`: OBS相关视图组件 - `obs/`: OBS相关视图组件
- `public/`: 公共静态资源 - `public/`: 公共静态资源
## 项目使用的库
- @vueuse/core: 提供了一系列的 Vue 3 的实用函数
- @vicons/fluent: 图标
- naive-ui: 组件库
- pinia: 状态管理
- vue-router: 路由
- vue-echarts: 图表

View File

@@ -772,7 +772,7 @@ export interface ResponsePointOrder2OwnerModel {
type: GoodsTypes type: GoodsTypes
customer: BiliAuthModel customer: BiliAuthModel
address?: AddressInfo address?: AddressInfo
goodsId: number goods: ResponsePointGoodModel
count: number count: number
createAt: number createAt: number
updateAt: number updateAt: number

2
src/components.d.ts vendored
View File

@@ -22,6 +22,7 @@ declare module 'vue' {
NAvatar: typeof import('naive-ui')['NAvatar'] NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard'] NCard: typeof import('naive-ui')['NCard']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty'] NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex'] NFlex: typeof import('naive-ui')['NFlex']
NFormItemGi: typeof import('naive-ui')['NFormItemGi'] NFormItemGi: typeof import('naive-ui')['NFormItemGi']
@@ -29,6 +30,7 @@ declare module 'vue' {
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage'] NImage: typeof import('naive-ui')['NImage']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NModal: typeof import('naive-ui')['NModal']
NPopconfirm: typeof import('naive-ui')['NPopconfirm'] NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']

View File

@@ -15,33 +15,72 @@ const { height } = useElementSize(elementRef.value)
</script> </script>
<template> <template>
<NText v-if="!address" depth="3" italic> 未知 </NText> <NText
<NFlex v-else ref="elementRef"> v-if="!address"
<NFlex vertical :size="5"> depth="3"
italic
>
未知
</NText>
<NFlex
v-else
ref="elementRef"
>
<NFlex
vertical
:size="5"
>
<NText v-if="size != 'small'"> <NText v-if="size != 'small'">
{{ address.province }} {{ address.province }}
<NText depth="3"> </NText> <NText depth="3">
</NText>
{{ address.city }} {{ address.city }}
<NText depth="3"> </NText> <NText depth="3">
</NText>
{{ address.district }} {{ address.district }}
<NText depth="3"> </NText> <NText depth="3">
</NText>
{{ address.street }} {{ address.street }}
</NText> </NText>
<NText depth="3"> <NText depth="3">
<NFlex align="center"> <NFlex align="center">
<NTag size="tiny" type="info" :bordered="false"> 详细地址 </NTag> <NTag
size="tiny"
type="info"
:bordered="false"
>
详细地址
</NTag>
<NEllipsis :style="{ maxWidth: size == 'small' ? '120px' : '1000px' }">
{{ address.address }} {{ address.address }}
</NEllipsis>
</NFlex> </NFlex>
</NText> </NText>
<NText v-if="size != 'small'" depth="3"> <NText
v-if="size != 'small'"
depth="3"
>
<NFlex align="center"> <NFlex align="center">
<NTag size="tiny" type="info" :bordered="false"> 收货人 </NTag> <NTag
size="tiny"
type="info"
:bordered="false"
>
收货人
</NTag>
<span> {{ address.phone }} {{ address.name }} </span> <span> {{ address.phone }} {{ address.name }} </span>
</NFlex> </NFlex>
</NText> </NText>
</NFlex> </NFlex>
<NFlex style="flex: 1" justify="end" align="center"> <NFlex
<slot name="actions"></slot> style="flex: 1"
justify="end"
align="center"
>
<slot name="actions" />
</NFlex> </NFlex>
</NFlex> </NFlex>
</template> </template>

View File

@@ -7,6 +7,7 @@ import { NCard, NEllipsis, NEmpty, NFlex, NIcon, NImage, NTag, NText } from 'nai
const props = defineProps<{ const props = defineProps<{
goods: ResponsePointGoodModel | undefined; goods: ResponsePointGoodModel | undefined;
contentStyle?: string | undefined; contentStyle?: string | undefined;
size?: 'small' | 'default';
}>(); }>();
// 默认封面图片 // 默认封面图片

View File

@@ -2,9 +2,8 @@
import { import {
GoodsTypes, GoodsTypes,
PointOrderStatus, PointOrderStatus,
ResponsePointGoodModel,
ResponsePointOrder2OwnerModel, ResponsePointOrder2OwnerModel,
ResponsePointOrder2UserModel, ResponsePointOrder2UserModel
} from '@/api/api-models' } from '@/api/api-models'
import { QueryPostAPI } from '@/api/query' import { QueryPostAPI } from '@/api/query'
import { POINT_API_URL } from '@/data/constants' import { POINT_API_URL } from '@/data/constants'
@@ -12,10 +11,13 @@ import { Info24Filled } from '@vicons/fluent'
import { import {
DataTableColumns, DataTableColumns,
DataTableRowKey, DataTableRowKey,
NAlert,
NAutoComplete, NAutoComplete,
NButton, NButton,
NCard,
NDataTable, NDataTable,
NDivider, NDivider,
NEllipsis,
NEmpty, NEmpty,
NFlex, NFlex,
NIcon, NIcon,
@@ -24,6 +26,7 @@ import {
NInputGroupLabel, NInputGroupLabel,
NModal, NModal,
NScrollbar, NScrollbar,
NSpace,
NStep, NStep,
NSteps, NSteps,
NTag, NTag,
@@ -32,9 +35,6 @@ import {
NTooltip, NTooltip,
useDialog, useDialog,
useMessage, useMessage,
NCard,
NSpace,
NAlert,
} from 'naive-ui' } from 'naive-ui'
import { computed, h, onMounted, ref, watch } from 'vue' import { computed, h, onMounted, ref, watch } from 'vue'
import AddressDisplay from './AddressDisplay.vue' import AddressDisplay from './AddressDisplay.vue'
@@ -45,10 +45,10 @@ type OrderType = ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel
const props = defineProps<{ const props = defineProps<{
order: ResponsePointOrder2UserModel[] | ResponsePointOrder2OwnerModel[] order: ResponsePointOrder2UserModel[] | ResponsePointOrder2OwnerModel[]
type: 'user' | 'owner' type: 'user' | 'owner'
goods?: ResponsePointGoodModel[]
loading?: boolean loading?: boolean
}>() }>()
const message = useMessage() const message = useMessage()
const dialog = useDialog() const dialog = useDialog()
const emit = defineEmits(['selectedItem']) const emit = defineEmits(['selectedItem'])
@@ -71,11 +71,7 @@ const orderAsOwner = computed(() => props.order as ResponsePointOrder2OwnerModel
const currentGoods = computed(() => { const currentGoods = computed(() => {
if (!orderDetail.value) return null if (!orderDetail.value) return null
if (props.type === 'user') { return orderDetail.value.goods
return (orderDetail.value as ResponsePointOrder2UserModel).goods
} else {
return props.goods?.find((g) => g.id === (orderDetail.value as ResponsePointOrder2OwnerModel).goodsId)
}
}) })
const expressOptions = computed(() => { const expressOptions = computed(() => {
@@ -143,6 +139,7 @@ const orderColumn: DataTableColumns<OrderType> = [
}, },
{ {
title: '订单号', title: '订单号',
minWidth: 70,
key: 'id', key: 'id',
}, },
{ {
@@ -173,11 +170,9 @@ const orderColumn: DataTableColumns<OrderType> = [
{ {
title: '礼物名', title: '礼物名',
key: 'giftName', key: 'giftName',
minWidth: 150,
render: (row: OrderType) => { render: (row: OrderType) => {
if (row.instanceOf === 'user') { return row.goods?.name
return (row as ResponsePointOrder2UserModel).goods.name
}
return props.goods?.find((g) => g.id === row.goodsId)?.name || '未知礼物'
}, },
}, },
{ {
@@ -249,9 +244,7 @@ const orderColumn: DataTableColumns<OrderType> = [
key: 'address', key: 'address',
minWidth: 250, minWidth: 250,
render: (row: OrderType) => { render: (row: OrderType) => {
const goodsCollectUrl = row.instanceOf === 'user' const goodsCollectUrl = row.goods.collectUrl
? (row as ResponsePointOrder2UserModel).goods.collectUrl
: props.goods?.find((g) => g.id === row.goodsId)?.collectUrl
if (row.type === GoodsTypes.Physical) { if (row.type === GoodsTypes.Physical) {
return goodsCollectUrl return goodsCollectUrl
@@ -262,7 +255,7 @@ const orderColumn: DataTableColumns<OrderType> = [
text: true, text: true,
type: 'info' type: 'info'
}, () => h(NText, { italic: true }, () => '通过站外链接收集')) }, () => h(NText, { italic: true }, () => '通过站外链接收集'))
: h(AddressDisplay, { address: row.address }) : h(AddressDisplay, { address: row.address, size: 'small' })
} else { } else {
return h(NText, { depth: 3, italic: true }, () => '无需发货') return h(NText, { depth: 3, italic: true }, () => '无需发货')
} }
@@ -277,7 +270,7 @@ const orderColumn: DataTableColumns<OrderType> = [
if (row.trackingNumber) { if (row.trackingNumber) {
return h(NFlex, { align: 'center', gap: 8 }, () => [ return h(NFlex, { align: 'center', gap: 8 }, () => [
h(NTag, { size: 'tiny', bordered: false }, () => row.expressCompany), h(NTag, { size: 'tiny', bordered: false }, () => row.expressCompany),
h(NText, { depth: 3 }, () => row.trackingNumber), h(NEllipsis, { style: { maxWidth: '100px' } }, () => h(NText, { depth: 3 }, () => row.trackingNumber)),
]) ])
} }
return h(NText, { depth: 3, italic: true }, () => '尚未发货') return h(NText, { depth: 3, italic: true }, () => '尚未发货')
@@ -728,7 +721,10 @@ onMounted(() => {
size="small" size="small"
class="address-info-card" class="address-info-card"
> >
<AddressDisplay :address="orderDetail.address" /> <AddressDisplay
:address="orderDetail.address"
size="default"
/>
</NCard> </NCard>
</template> </template>

View File

@@ -2,7 +2,7 @@
import { QueryGetAPI } from '@/api/query' import { QueryGetAPI } from '@/api/query'
import { BILI_AUTH_API_URL, CURRENT_HOST } from '@/data/constants' import { BILI_AUTH_API_URL, CURRENT_HOST } from '@/data/constants'
import { useBiliAuth } from '@/store/useBiliAuth' import { useBiliAuth } from '@/store/useBiliAuth'
import { useStorage } from '@vueuse/core' import { useStorage, useBreakpoints as useVueUseBreakpoints, breakpointsTailwind } from '@vueuse/core'
import { import {
NAlert, NAlert,
NButton, NButton,
@@ -17,7 +17,7 @@ import {
NStep, NStep,
NSteps, NSteps,
NText, NText,
useMessage useMessage,
} from 'naive-ui' } from 'naive-ui'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
@@ -30,6 +30,8 @@ type AuthStartModel = {
} }
const message = useMessage() const message = useMessage()
const breakpoints = useVueUseBreakpoints(breakpointsTailwind)
const isSmallScreen = breakpoints.smaller('sm')
const guidKey = useStorage('Bili.Auth.Key', uuidv4()) const guidKey = useStorage('Bili.Auth.Key', uuidv4())
const currentToken = useStorage<string>('Bili.Auth.Selected', null) const currentToken = useStorage<string>('Bili.Auth.Selected', null)
@@ -97,11 +99,15 @@ function checkTimeLeft() {
} }
} }
function copyCode() { function copyCode() {
if (navigator.clipboard) { const textToCopy = currentStep.value === 2
navigator.clipboard.writeText(startModel.value?.code ?? '') ? `${CURRENT_HOST}bili-user?auth=${currentToken.value}`
message.success('已复制认证码到剪切板') : startModel.value?.code ?? ''
if (navigator.clipboard && textToCopy) {
navigator.clipboard.writeText(textToCopy)
message.success(currentStep.value === 2 ? '已复制登陆链接到剪切板' : '已复制认证码到剪切板')
} else { } else {
message.warning('当前环境不支持自动复制, 请手动选择并复制') message.warning('无法复制内容, 请手动选择并复制')
} }
} }
@@ -119,20 +125,25 @@ onMounted(async () => {
<NFlex <NFlex
justify="center" justify="center"
align="center" align="center"
style="height: 100vh" style="min-height: 100vh; padding: 20px; box-sizing: border-box"
> >
<NCard <NCard
embedded embedded
style="margin: 20px; max-width: 1100px" style="width: 100%; max-width: 1000px"
> >
<template #header> <template #header>
<NText style="font-size: 1.2em; font-weight: bold">
Bilibili 身份验证 Bilibili 身份验证
</NText>
</template> </template>
<NFlex :wrap="false"> <NFlex
:wrap="false"
:vertical="isSmallScreen"
>
<NSteps <NSteps
:current="currentStep + 1" :current="currentStep + 1"
vertical vertical
style="max-width: 300px" style="min-width: 200px; max-width: 300px; margin-bottom: 20px"
> >
<NStep <NStep
title="准备认证" title="准备认证"
@@ -147,30 +158,45 @@ onMounted(async () => {
description="现在就已经通过了认证!" description="现在就已经通过了认证!"
/> />
</NSteps> </NSteps>
<div style="flex-grow: 1; padding-left: 20px; border-left: 1px solid var(--n-border-color); min-width: 0;">
<template v-if="currentStep == 1"> <template v-if="currentStep == 1">
<NSpace <NFlex
vertical vertical
justify="center" justify="center"
align="center" align="center"
style="width: 100%" style="width: 100%; height: 100%; padding-top: 20px; min-height: 250px;"
> >
<template v-if="!timeOut"> <template v-if="!timeOut">
<NSpin /> <NSpin size="large" />
<span> 剩余 <NCountdown :duration="timeLeft" /> </span> <NText style="margin-top: 15px; font-size: 1.1em;">
<NInputGroup> 剩余时间<NCountdown :duration="timeLeft" />
</NText>
<NText
depth="3"
style="margin-top: 20px;"
>
请复制下方的认证码并前往指定直播间发送
</NText>
<NInputGroup style="margin-top: 10px; max-width: 300px;">
<NInput <NInput
:value="startModel?.code" :value="startModel?.code"
:allow-input="() => false" readonly
placeholder="认证码"
style="text-align: center; font-size: 1.2em; letter-spacing: 2px;"
/> />
<NButton @click="copyCode"> <NButton
复制认证码 type="primary"
@click="copyCode"
>
复制
</NButton> </NButton>
</NInputGroup> </NInputGroup>
<NButton <NButton
type="primary" type="info"
tag="a" tag="a"
:href="'https://live.bilibili.com/' + startModel?.targetRoomId" :href="'https://live.bilibili.com/' + startModel?.targetRoomId"
target="_blank" target="_blank"
style="margin-top: 20px"
> >
前往直播间 前往直播间
</NButton> </NButton>
@@ -178,10 +204,13 @@ onMounted(async () => {
<NAlert <NAlert
v-else v-else
type="error" type="error"
title="认证超时"
style="width: 100%; max-width: 400px;"
> >
认证超时 <NFlex justify="center">
<NButton <NButton
type="error" type="error"
style="margin-top: 10px"
@click=" @click="
() => { () => {
currentStep = 0 currentStep = 0
@@ -189,17 +218,17 @@ onMounted(async () => {
} }
" "
> >
重新开始 重新开始认证
</NButton> </NButton>
</NFlex>
</NAlert> </NAlert>
</NSpace> </NFlex>
</template> </template>
<template v-else-if="currentStep == 0"> <template v-else-if="currentStep == 0">
<NSpace <NSpace
vertical vertical
justify="center" align="stretch"
align="center" style="width: 100%; padding-top: 10px"
style="width: 100%"
> >
<NAlert type="info"> <NAlert type="info">
<NText> <NText>
@@ -220,12 +249,10 @@ onMounted(async () => {
在指定的直播间直播间内发送给出的验证码 在指定的直播间直播间内发送给出的验证码
</NText> </NText>
</NAlert> </NAlert>
<NText <NFlex
depth="3" justify="center"
style="font-size: 15px" style="margin-top: 20px"
> >
准备好了吗?
</NText>
<NButton <NButton
size="large" size="large"
type="primary" type="primary"
@@ -233,36 +260,49 @@ onMounted(async () => {
> >
开始认证 开始认证
</NButton> </NButton>
</NFlex>
</NSpace> </NSpace>
</template> </template>
<template v-else-if="currentStep == 2"> <template v-else-if="currentStep == 2">
<NFlex <NSpace
justify="center"
align="center"
vertical vertical
style="width: 100%" align="stretch"
style="width: 100%; padding-top: 10px"
>
<NAlert
type="success"
title="验证成功!"
style="margin-bottom: 15px"
> >
<NAlert type="success">
你已完成验证! 请妥善保存你的登陆链接, 请勿让其他人获取. 丢失后可以再次通过认证流程获得. 你已完成验证! 请妥善保存你的登陆链接, 请勿让其他人获取. 丢失后可以再次通过认证流程获得.
<br> <br>
要在其他地方登陆, 或者需要重新登陆的话把这个链接复制到浏览器地址栏打开即可 要在其他地方登陆, 或者需要重新登陆的话把这个链接复制到浏览器地址栏打开即可
</NAlert> </NAlert>
<NText> 你的登陆链接为: </NText> <NText strong>
<NInputGroup> 你的登陆链接为:
</NText>
<NInput <NInput
:value="`${CURRENT_HOST}bili-user?auth=${currentToken}`" :value="`${CURRENT_HOST}bili-user?auth=${currentToken}`"
type="textarea" type="textarea"
:allow-input="() => false" readonly
style="margin-top: 5px"
/> />
<NFlex
justify="end"
style="margin-top: 10px"
>
<NButton <NButton
type="info" type="primary"
style="height: 100%"
@click="copyCode" @click="copyCode"
> >
复制登陆链接 复制登陆链接
</NButton> </NButton>
</NInputGroup> </NFlex>
<NFlex> <NFlex
justify="center"
style="margin-top: 20px"
:wrap="true"
>
<NButton <NButton
type="primary" type="primary"
@click="$router.push({ name: 'bili-user' })" @click="$router.push({ name: 'bili-user' })"
@@ -271,6 +311,7 @@ onMounted(async () => {
</NButton> </NButton>
<NPopconfirm <NPopconfirm
positive-text="继续" positive-text="继续"
negative-text="取消"
@positive-click=" @positive-click="
() => { () => {
currentStep = 0 currentStep = 0
@@ -288,8 +329,9 @@ onMounted(async () => {
这将会登出当前已认证的账号, 请先在认证其他账号前保存你的登陆链接 这将会登出当前已认证的账号, 请先在认证其他账号前保存你的登陆链接
</NPopconfirm> </NPopconfirm>
</NFlex> </NFlex>
</NFlex> </NSpace>
</template> </template>
</div>
</NFlex> </NFlex>
</NCard> </NCard>
</NFlex> </NFlex>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAccount } from '@/api/account' import { useAccount } from '@/api/account'
import { GoodsTypes, PointOrderStatus, ResponsePointGoodModel, ResponsePointOrder2OwnerModel } from '@/api/api-models' import { GoodsTypes, PointOrderStatus, ResponsePointGoodModel, ResponsePointOrder2OwnerModel, ResponsePointUserModel } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query' import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import PointOrderCard from '@/components/manage/PointOrderCard.vue' import PointOrderCard from '@/components/manage/PointOrderCard.vue'
import { POINT_API_URL } from '@/data/constants' import { POINT_API_URL } from '@/data/constants'
@@ -17,12 +17,16 @@ import {
NDivider, NDivider,
NEmpty, NEmpty,
NFlex, NFlex,
NModal,
NPopconfirm, NPopconfirm,
NSelect, NSelect,
NSpace,
NSpin, NSpin,
NText,
useMessage, useMessage,
} from 'naive-ui' } from 'naive-ui'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import PointUserDetailCard from './PointUserDetailCard.vue'
// 订单筛选设置类型定义 // 订单筛选设置类型定义
type OrderFilterSettings = { type OrderFilterSettings = {
@@ -62,6 +66,8 @@ const filteredOrders = computed(() => {
const isLoading = ref(false) const isLoading = ref(false)
const selectedItem = ref<DataTableRowKey[]>() const selectedItem = ref<DataTableRowKey[]>()
const targetStatus = ref<PointOrderStatus>()
const showStatusModal = ref(false)
// 获取所有订单 // 获取所有订单
async function getOrders() { async function getOrders() {
@@ -104,6 +110,54 @@ async function deleteOrder() {
} }
} }
// 打开状态更新模态框
function openStatusUpdateModal() {
if (!selectedItem.value?.length) {
message.warning('请选择要更新的订单')
return
}
showStatusModal.value = true
}
// 批量更新订单状态
async function batchUpdateOrderStatus() {
if (!selectedItem.value?.length) {
message.warning('请选择要更新的订单')
return
}
if (targetStatus.value === undefined) {
message.warning('请选择目标状态')
return
}
try {
const requestData = {
orderIds: selectedItem.value,
status: targetStatus.value
}
const data = await QueryPostAPI<number[]>(POINT_API_URL + 'batch-update-order-status', requestData)
if (data.code == 200) {
message.success('更新成功')
// 更新本地订单状态
orders.value.forEach(order => {
if (data.data.includes(order.id)) {
order.status = targetStatus.value as PointOrderStatus
order.updateAt = Date.now()
}
})
targetStatus.value = undefined
showStatusModal.value = false
} else {
message.error('更新失败: ' + data.message)
}
} catch (err) {
message.error('更新失败: ' + err)
console.log(err)
}
}
// 订单状态文本映射 // 订单状态文本映射
const statusText = { const statusText = {
[PointOrderStatus.Completed]: '已完成', [PointOrderStatus.Completed]: '已完成',
@@ -116,7 +170,7 @@ function exportData() {
try { try {
const text = objectsToCSV( const text = objectsToCSV(
filteredOrders.value.map((s) => { filteredOrders.value.map((s) => {
const gift = props.goods.find((g) => g.id == s.goodsId) const gift = s.goods
return { return {
订单号: s.id, 订单号: s.id,
订单类型: s.type == GoodsTypes.Physical ? '实体' : '虚拟', 订单类型: s.type == GoodsTypes.Physical ? '实体' : '虚拟',
@@ -237,8 +291,7 @@ onMounted(async () => {
/> />
<NSelect <NSelect
v-model:value="filterSettings.customer" v-model:value="filterSettings.customer"
:options=" :options="new List(orders)
new List(orders)
.DistinctBy((s) => s.customer.userId) .DistinctBy((s) => s.customer.userId)
.Select((s) => ({ label: s.customer.name, value: s.customer.userId })) .Select((s) => ({ label: s.customer.name, value: s.customer.userId }))
.ToArray() .ToArray()
@@ -247,15 +300,17 @@ onMounted(async () => {
clearable clearable
style="min-width: 120px; max-width: 150px" style="min-width: 120px; max-width: 150px"
/> />
<NCheckbox <NCheckbox v-model:checked="filterSettings.onlyRequireShippingInfo">
v-model:checked="filterSettings.onlyRequireShippingInfo"
>
仅包含未填写快递单号的订单 仅包含未填写快递单号的订单
</NCheckbox> </NCheckbox>
</NFlex> </NFlex>
</NCard> </NCard>
<NDivider title-placement="left"> <NDivider title-placement="left">
<NFlex
:gap="8"
:wrap="false"
>
<NPopconfirm @positive-click="deleteOrder"> <NPopconfirm @positive-click="deleteOrder">
<template #trigger> <template #trigger>
<NButton <NButton
@@ -268,15 +323,65 @@ onMounted(async () => {
</template> </template>
确定删除吗? 确定删除吗?
</NPopconfirm> </NPopconfirm>
<NPopconfirm @positive-click="openStatusUpdateModal">
<template #trigger>
<NButton
size="tiny"
type="info"
:disabled="!selectedItem?.length"
>
批量更新状态
</NButton>
</template>
确定要更新选中订单的状态吗?
</NPopconfirm>
</NFlex>
</NDivider> </NDivider>
<!-- 订单列表 --> <!-- 订单列表 -->
<PointOrderCard <PointOrderCard
:order="filteredOrders" :order="filteredOrders"
:goods="goods"
type="owner" type="owner"
@selected-item="(items) => (selectedItem = items)" @selected-item="(items) => (selectedItem = items)"
/> />
<!-- 状态选择模态框 -->
<NModal
v-model:show="showStatusModal"
title="选择目标状态"
preset="card"
style="max-width: 400px"
>
<NSpace vertical>
<NText>请选择您想要将订单更新为的状态</NText>
<NSelect
v-model:value="targetStatus"
:options="[
{ label: '已完成', value: PointOrderStatus.Completed },
{ label: '等待发货', value: PointOrderStatus.Pending },
{ label: '已发货', value: PointOrderStatus.Shipped },
]"
placeholder="选择状态"
style="width: 100%"
/>
<NFlex
justify="end"
:gap="12"
>
<NButton @click="showStatusModal = false">
取消
</NButton>
<NButton
type="primary"
:disabled="targetStatus === undefined"
@click="batchUpdateOrderStatus"
>
确认更新
</NButton>
</NFlex>
</NSpace>
</NModal>
</template> </template>
</NSpin> </NSpin>
</template> </template>