Files
yxe-mp/pages/audit/index.vue
T
weicw-dshsa aff1e52e3e feat(自主填报): 拆分驻店/跑店填报并新增跑店填报页
「自主填报」原本只有一种表单。本次拆成两种类型:

1. 新增选择填报类型页 packageUser/self-report/select
   - 按设计稿:hero 页头 + 两张类型卡(驻店填报 / 跑店填报)+ 确认选择
   - 入口(首页、我的)由直接进 create 改为先进 select

2. 原自主填报页改为「驻店填报」
   - 仅改导航标题,表单内容不变

3. 新增跑店填报页 packageUser/self-report/visit
   - 复制现有跑店/巡店任务(pages/task/detail 的 cateId=4 分支)的填报页并改造:
     保留巡店列表与下方内容,去掉列表上方的任务卡片/总进度/任务详情;
     打卡、摆台照、销量、小票、门店评分一并不保留
   - 巡店列表内「连锁筛选」「门店筛选」是两个并列的独立输入框,
     各自弹各自的选择器(不串联),样式与驻店填报逐项对齐;
     换连锁会清空已选门店
   - 每条巡店可新增/删除,独立获取定位 + 打卡拍照(最多 9 张)
   - 定位不强制(巡店任务里是强制的,这里放开)
   - 底部「保存草稿」+「提交填报」,草稿交互与驻店填报一致:
     进页面询问是否加载、提交成功后清除;storage key 与驻店分开
   - 驳回后重新提交复用同一页面(mode=resubmit)

4. 填报记录页加「驻店 / 跑店」类型切换
   - 两表分表查询,各自分页;跑店卡片展示门店数、巡店列表(门店/定位/照片)
   - 连锁与门店关键词筛选落到巡店列表的每一行

5. 药审核加「跑店填报」类型
   - 新增 AuditVisitReportItem,展开时按 id 拉详情(列表不下发 store_list)
   - 跑店没有销量小计,审核通过需手填奖金,故补奖金输入弹层

顺带重构:
- 门店解析(扁平列表 / 连锁嵌套两种结构的兼容、门店明细不入页面 data
  的性能处理)抽到 util/store-picker,create 与 visit 共用一份,
  避免两页各自维护导致解析口径不一致
2026-10-11 22:50:15 +08:00

1561 lines
53 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="audit-page">
<view class="page-hero">
<AppNavbar title="药审核" show-back/>
<view class="intro-card">
<view class="card-title">
<view class="title-bar"></view>
<text>药审核</text>
</view>
</view>
</view>
<view class="card subsection">
<view class="type-tabs">
<view
v-for="tab in typeTabs"
:key="tab.value"
:class="['tab', activeType === tab.value ? 'active' : '']"
@tap="changeType(tab.value)"
>
{{ tab.label }}
</view>
</view>
<view class="status-tabs">
<view
v-for="tab in displayStatusTabs"
:key="tab.value"
:class="['tab', activeStatus === tab.value ? 'active' : '']"
@tap="changeStatus(tab.value)"
>
{{ tab.label }}
</view>
</view>
</view>
<view class="card block">
<view class="section-title">筛选查询</view>
<view class="filter-row">
<view v-if="!isStoreVisitType" class="filter-chip" @tap="openChainPicker">{{ chainLabel }}</view>
<input v-model="filterForm.userGroup" class="group-input" type="text" confirm-type="search" placeholder="用户分组" placeholder-class="keyword-placeholder" @confirm="handleQuery" />
<view class="filter-chip" @tap="openDatePicker('start')">{{ filterForm.startDate || '开始日期' }}</view>
<view class="filter-chip" @tap="openDatePicker('end')">{{ filterForm.endDate || '结束日期' }}</view>
</view>
<view class="keyword-row">
<input
v-if="!isStoreVisitType"
v-model="filterForm.merchantKeyword"
class="keyword-input"
type="text"
confirm-type="search"
placeholder="请输入门店关键词"
placeholder-class="keyword-placeholder"
@confirm="handleQuery"
/>
<view class="filter-btn primary" @tap="handleQuery">查询</view>
<view class="filter-btn" @tap="handleReset">重置</view>
</view>
</view>
<!-- <view class="card block">-->
<!-- <view class="section-title">快捷操作</view>-->
<!-- <view class="action-row">-->
<!-- <view class="primary-btn action-btn" @tap="handleAction('审核')">审核</view>-->
<!-- <view class="primary-btn action-btn" @tap="handleAction('抄送')">抄送</view>-->
<!-- <view class="warn-btn action-btn" @tap="handleAction('驳回')">驳回</view>-->
<!-- <view class="danger-btn action-btn" @tap="handleAction('删除')">删除</view>-->
<!-- </view>-->
<!-- <view class="tip">点击任意按钮会立即给出处理反馈。</view>-->
<!-- </view>-->
<view class="block">
<view class="section-title">记录列表</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="currentTasks.length === 0" class="empty">暂无记录</view>
<view v-else>
<template v-if="activeType === 'movement'">
<AuditMovementItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@preview="preview"
@approve="handleAction(2, $event)"
@reject="handleAction(3, $event)"
@copied="fetchList"
/>
</template>
<template v-else-if="activeType === 'storeVisit'">
<AuditStoreVisitItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@preview="preview"
@approve="handleAction(2, $event)"
@reject="handleAction(3, $event)"
@copied="fetchList"
/>
</template>
<template v-else-if="activeType === 'selfReport'">
<AuditSelfReportItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@preview="preview"
@approve="handleSelfReportApprove"
@reject="handleSelfReportReject"
@updated="handleSelfReportUpdated"
/>
</template>
<template v-else-if="activeType === 'visitReport'">
<AuditVisitReportItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@preview="preview"
@approve="handleVisitReportApprove"
@reject="handleVisitReportReject"
/>
</template>
<template v-else>
<AuditShareItem
v-for="item in currentTasks"
:key="item.id"
:item="item"
@approve="handleShareApprove"
@reject="handleActionShare(2, $event)"
@update="handleShareUpdate"
@preview="preview"
@copied="fetchList"
/>
</template>
<view class="load-more-tip">
{{ loadingMore ? '加载更多中...' : (hasMore ? '上滑加载更多' : '没有更多了') }}
</view>
</view>
</view>
<view v-if="showRejectPopup" class="income-popup-mask" @tap="closeRejectPopup"></view>
<view v-if="showRejectPopup" class="income-popup card" @tap.stop>
<view class="income-popup-head">
<view class="section-title">{{ isSelfReportLikeRejectType(pendingRejectType) ? '填写驳回原因(可选)' : '填写拒绝原因' }}</view>
<view class="close-btn" @tap="closeRejectPopup">关闭</view>
</view>
<view class="income-form">
<view class="income-label">{{ isSelfReportLikeRejectType(pendingRejectType) ? '驳回原因' : '拒绝原因' }}</view>
<input
v-model="rejectReason"
class="income-input"
type="text"
:placeholder="isSelfReportLikeRejectType(pendingRejectType) ? '可不填写' : '请输入拒绝原因'"
/>
</view>
<view class="primary-btn income-submit" @tap="submitRejectReason">提交</view>
</view>
<!-- 跑店填报没有销量小计,奖金只能审核时手填 -->
<view v-if="showRewardPopup" class="income-popup-mask" @tap="closeRewardPopup"></view>
<view v-if="showRewardPopup" class="income-popup card" @tap.stop>
<view class="income-popup-head">
<view class="section-title">输入奖金金额</view>
<view class="close-btn" @tap="closeRewardPopup">关闭</view>
</view>
<view class="income-form">
<view class="income-label">奖金金额</view>
<input
v-model="rewardAmount"
class="income-input"
type="digit"
placeholder="请输入奖金金额,可填0"
/>
</view>
<view class="primary-btn income-submit" @tap="submitRewardAmount">提交</view>
</view>
<u-picker
:show="showChainPicker"
:columns="[chainOptions]"
:loading="chainLoading"
:defaultIndex="chainPickerIndex"
keyName="text"
@confirm="onChainConfirm"
@cancel="closeChainPicker"
@close="closeChainPicker"
/>
<u-datetime-picker
:show="showStartPicker"
:value="startPickerValue"
mode="date"
@change="e => onDateChange('start', e)"
@confirm="e => onDateConfirm('start', e)"
@cancel="closeDatePicker('start')"
@close="closeDatePicker('start')"
/>
<u-datetime-picker
:show="showEndPicker"
:value="endPickerValue"
mode="date"
@change="e => onDateChange('end', e)"
@confirm="e => onDateConfirm('end', e)"
@cancel="closeDatePicker('end')"
@close="closeDatePicker('end')"
/>
</view>
</template>
<script>
import {auditUserTaskPageReq, changeUserTaskReq, listUserTaskReq} from "@/api/task";
import dayjs from 'dayjs'
import {shareAuditPageReq, shareChangeReq, sharePageReq} from "@/api/share";
import {selfReportAuditPageReq, selfReportChangeReq, selfReportPageReq} from "@/api/self-report";
import {visitReportAuditPageReq, visitReportChangeReq, visitReportPageReq} from "@/api/visit-report";
import AppNavbar from "@/components/AppNavbar.vue";
import AuditMovementItem from "./components/AuditMovementItem.vue";
import AuditStoreVisitItem from "./components/AuditStoreVisitItem.vue";
import AuditShareItem from "./components/AuditShareItem.vue";
import AuditSelfReportItem from "./components/AuditSelfReportItem.vue";
import AuditVisitReportItem from "./components/AuditVisitReportItem.vue";
import {dictDataListByDictReq} from "@/api/common";
export default {
components: {AppNavbar, AuditMovementItem, AuditStoreVisitItem, AuditShareItem, AuditSelfReportItem, AuditVisitReportItem},
data() {
return {
typeTabs: [
{label: '药动销', value: 'movement'},
{label: '跑店/巡店', value: 'storeVisit'},
{label: '驻店填报', value: 'selfReport'},
{label: '跑店填报', value: 'visitReport'},
{label: '药分享', value: 'exchange'}
],
statusTabs: [
{label: '待审核', value: 0},
{label: '已通过', value: 1},
{label: '已驳回', value: 2},
{label: '抄送', value: 3}
],
activeType: 'movement',
activeStatus: 0,
movementTasks: [],
storeVisitTasks: [],
selfReportTasks: [],
visitReportTasks: [],
exchangeTasks: [],
loading: false,
chainLoading: false,
showChainPicker: false,
showStartPicker: false,
showEndPicker: false,
chainOptions: [{id: '', text: '全部连锁'}],
chainMerchantMap: {},
filterForm: {
chainId: '',
userGroup: '',
merchantKeyword: '',
startDate: '',
endDate: ''
},
showRejectPopup: false,
rejectReason: '',
pendingReject: null,
pendingRejectType: 'audit',
// 跑店填报审核通过时的奖金输入
showRewardPopup: false,
rewardAmount: '',
pendingRewardItem: null,
pageNo: 1,
pageSize: 5,
hasMore: true,
loadingMore: false
}
},
computed: {
currentTasks() {
if (this.activeType === 'movement') return this.movementTasks
if (this.activeType === 'storeVisit') return this.storeVisitTasks
if (this.activeType === 'selfReport') return this.selfReportTasks
if (this.activeType === 'visitReport') return this.visitReportTasks
return this.exchangeTasks
},
displayStatusTabs() {
// 驻店/跑店填报都由审核端驳回,没有「抄送」态
if (this.isSelfReportLikeType) {
return this.statusTabs.filter(item => item.value !== 3)
}
return this.statusTabs
},
isStoreVisitType() {
return this.activeType === 'storeVisit'
},
// 跑店填报的连锁在 store_list JSON 里,没有 chain_id 可匹配,只能按连锁名模糊查
useChainNameFilter() {
return this.activeType === 'visitReport'
},
isSelfReportLikeType() {
return this.activeType === 'selfReport' || this.activeType === 'visitReport'
},
chainLabel() {
const selected = this.chainOptions.find(item => item.id === this.filterForm.chainId)
return (selected && selected.text) || '连锁筛选'
},
chainPickerIndex() {
const idx = this.chainOptions.findIndex(item => item.id === this.filterForm.chainId)
return [idx > -1 ? idx : 0]
},
startPickerValue() {
return this.filterForm.startDate ? dayjs(this.filterForm.startDate).valueOf() : Date.now()
},
endPickerValue() {
return this.filterForm.endDate ? dayjs(this.filterForm.endDate).valueOf() : Date.now()
}
},
onShow() {
this.initPage()
},
onReachBottom() {
this.loadMore()
},
methods: {
async initPage() {
await this.fetchChainList()
await this.fetchList()
},
getPageSize(type = this.activeType) {
return this.isSelfReportLikeType || type === 'selfReport' || type === 'visitReport' ? 10 : this.pageSize
},
getTaskListByType(type = this.activeType) {
if (type === 'movement') return this.movementTasks
if (type === 'storeVisit') return this.storeVisitTasks
if (type === 'selfReport') return this.selfReportTasks
if (type === 'visitReport') return this.visitReportTasks
return this.exchangeTasks
},
setTaskListByType(type = this.activeType, list = []) {
if (type === 'movement') {
this.movementTasks = list
return
}
if (type === 'storeVisit') {
this.storeVisitTasks = list
return
}
if (type === 'selfReport') {
this.selfReportTasks = list
return
}
if (type === 'visitReport') {
this.visitReportTasks = list
return
}
this.exchangeTasks = list
},
resetPagination() {
this.pageNo = 1
this.hasMore = true
},
async loadMore() {
if (this.loading || this.loadingMore || !this.hasMore) return
await this.fetchList({append: true})
},
buildUserTaskFilterParams(options = {}) {
const enableChainFilter = options.enableChainFilter !== false
const enableMerchantFilter = options.enableMerchantFilter !== false
const params = {
page: options.page || this.pageNo,
limit: this.pageSize
}
const chainId = this.getSelectedChainApiValue(this.filterForm.chainId)
const keyword = (this.filterForm.merchantKeyword || '').trim()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
const chainIdText = String(chainId || '')
const isVirtualChain = chainIdText.startsWith('chain_')
if (enableChainFilter && chainId !== '' && chainId !== null && chainId !== undefined && !isVirtualChain) {
params.chainId = chainId
}
if (enableMerchantFilter && keyword) {
params.merchantName = keyword
params.keyword = keyword
}
const userGroup = (this.filterForm.userGroup || '').trim()
if (userGroup) params.userGroup = userGroup
if (startDate) {
params.createTimeStart = `${startDate} 00:00:00`
params.dateStart = startDate
}
if (endDate) {
params.createTimeEnd = `${endDate} 23:59:59`
params.dateEnd = endDate
}
return params
},
extractPageResult(data) {
if (Array.isArray(data)) {
return {
list: data,
total: data.length,
pages: 1,
current: 1,
size: data.length
}
}
const safeData = data && typeof data === 'object' ? data : {}
const list = Array.isArray(safeData.records)
? safeData.records
: (Array.isArray(safeData.list) ? safeData.list : [])
const total = Number(safeData.total ?? safeData.count)
const pages = Number(safeData.pages)
const current = Number(safeData.current ?? safeData.page ?? safeData.pageNum)
const size = Number(safeData.size ?? safeData.limit ?? safeData.pageSize)
return {
list,
total: Number.isFinite(total) ? total : null,
pages: Number.isFinite(pages) ? pages : null,
current: Number.isFinite(current) ? current : null,
size: Number.isFinite(size) ? size : this.getPageSize()
}
},
updatePagination(pageResult, requestPage, loadedCount, pageSize = this.getPageSize()) {
this.pageNo = requestPage
if (Number.isFinite(pageResult.total) && pageResult.total !== null) {
this.hasMore = requestPage * pageSize < pageResult.total
return
}
if (Number.isFinite(pageResult.pages) && pageResult.pages !== null) {
this.hasMore = requestPage < pageResult.pages
return
}
this.hasMore = loadedCount >= pageSize
},
async fetchChainList() {
this.chainLoading = true
try {
const {data} = await dictDataListByDictReq({
dictId: 175
})
const list = Array.isArray(data) ? data : this.unwrapList(data)
const chainOptions = [{id: '', text: '全部连锁'}]
list
.map((item, index) => this.normalizeChainDictOption(item, index))
.filter(item => item.id !== '' && item.id !== null && item.id !== undefined)
.forEach(item => {
chainOptions.push(item)
})
this.chainOptions = chainOptions
this.chainMerchantMap = {}
} catch (error) {
this.chainOptions = [{id: '', text: '全部连锁'}]
this.chainMerchantMap = {}
} finally {
this.chainLoading = false
}
},
buildShareFilterParams() {
const params = {}
const userGroup = (this.filterForm.userGroup || '').trim()
if (userGroup) params.userGroup = userGroup
const chainId = this.getSelectedChainApiValue(this.filterForm.chainId)
const keyword = (this.filterForm.merchantKeyword || '').trim()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
const chainIdText = String(chainId || '')
const isVirtualChain = chainIdText.startsWith('chain_')
if (chainId !== '' && chainId !== null && chainId !== undefined && !isVirtualChain) {
params.chainId = chainId
}
if (keyword) {
params.merchantName = keyword
params.keyword = keyword
}
if (startDate) {
params.createTimeStart = `${startDate} 00:00:00`
params.dateStart = startDate
}
if (endDate) {
params.createTimeEnd = `${endDate} 23:59:59`
params.dateEnd = endDate
}
return params
},
buildSelfReportFilterParams() {
const params = {}
const userGroup = (this.filterForm.userGroup || '').trim()
if (userGroup) params.userGroup = userGroup
const chainId = this.getSelectedChainApiValue(this.filterForm.chainId)
const keyword = (this.filterForm.merchantKeyword || '').trim()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
const chainIdText = String(chainId || '')
const isVirtualChain = chainIdText.startsWith('chain_')
if (chainId !== '' && chainId !== null && chainId !== undefined && !isVirtualChain) {
params.chainId = chainId
}
if (keyword) {
params.merchantName = keyword
params.keywords = keyword
}
if (startDate) {
params.dateStart = startDate
}
if (endDate) {
params.dateEnd = endDate
}
return params
},
// 跑店填报表里没有 chain_id/merchant_id,只能按 store_list 文本模糊匹配
buildVisitReportFilterParams() {
const params = {}
const userGroup = (this.filterForm.userGroup || '').trim()
if (userGroup) params.userGroup = userGroup
const keyword = (this.filterForm.merchantKeyword || '').trim()
const chainText = this.selectedChainText()
const startDate = this.filterForm.startDate
const endDate = this.filterForm.endDate
if (chainText) params.chainName = chainText
if (keyword) params.merchantName = keyword
if (startDate) params.dateStart = startDate
if (endDate) params.dateEnd = endDate
return params
},
// 下拉里选的是连锁的 id,新表按名字查,需要回查成文本
selectedChainText() {
const selectedId = this.filterForm.chainId
if (selectedId === '' || selectedId === null || selectedId === undefined) return ''
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
return option ? (option.text || '') : ''
},
filterExchangeList(list = []) {
const keyword = (this.filterForm.merchantKeyword || '').trim().toLowerCase()
const startTs = this.filterForm.startDate ? dayjs(`${this.filterForm.startDate} 00:00:00`).valueOf() : null
const endTs = this.filterForm.endDate ? dayjs(`${this.filterForm.endDate} 23:59:59`).valueOf() : null
return list.filter(item => {
if (this.filterForm.chainId && !this.matchesSelectedChain(item.chainIdNormalized, item.chainName, this.filterForm.chainId)) {
return false
}
if (keyword) {
const merchantText = String(item.merchantName || item.merchant_name || '').toLowerCase()
if (!merchantText.includes(keyword)) return false
}
if ((startTs || endTs) && !Number.isFinite(item.activityTimestamp)) {
return false
}
if (startTs && item.activityTimestamp < startTs) {
return false
}
if (endTs && item.activityTimestamp > endTs) {
return false
}
return true
})
},
filterMovementList(list = [], options = {}) {
const enableChainFilter = options.enableChainFilter !== false
const enableMerchantFilter = options.enableMerchantFilter !== false
const keyword = enableMerchantFilter ? (this.filterForm.merchantKeyword || '').trim().toLowerCase() : ''
const startTs = this.filterForm.startDate ? dayjs(`${this.filterForm.startDate} 00:00:00`).valueOf() : null
const endTs = this.filterForm.endDate ? dayjs(`${this.filterForm.endDate} 23:59:59`).valueOf() : null
return list.filter(item => {
if (enableChainFilter && this.filterForm.chainId && !this.matchesSelectedChain(item.chainIdNormalized, item.chainName, this.filterForm.chainId)) {
return false
}
if (keyword) {
const merchantText = String(item.merchantName || '').toLowerCase()
if (!merchantText.includes(keyword)) return false
}
if ((startTs || endTs) && !Number.isFinite(item.submitTimestamp)) {
return false
}
if (startTs && item.submitTimestamp < startTs) {
return false
}
if (endTs && item.submitTimestamp > endTs) {
return false
}
return true
})
},
handleQuery() {
if (!this.validateDateRange()) return
this.fetchList()
},
async handleReset() {
this.filterForm = {
chainId: '',
userGroup: '',
merchantKeyword: '',
startDate: '',
endDate: ''
}
await this.fetchList()
},
openChainPicker() {
this.showChainPicker = true
},
closeChainPicker() {
this.showChainPicker = false
},
onChainConfirm(e) {
const selected = e.value && e.value.length ? e.value[0] : null
this.filterForm.chainId = selected ? (selected.id ?? '') : ''
this.showChainPicker = false
this.handleQuery()
},
openDatePicker(type) {
if (type === 'start') {
this.showStartPicker = true
return
}
this.showEndPicker = true
},
closeDatePicker(type) {
if (type === 'start') {
this.showStartPicker = false
return
}
this.showEndPicker = false
},
setDateValue(type, raw) {
const value = dayjs(raw).format('YYYY-MM-DD')
if (type === 'start') {
this.filterForm.startDate = value
return
}
this.filterForm.endDate = value
},
onDateChange(type, event) {
const raw = event && event.value !== undefined ? event.value : Date.now()
this.setDateValue(type, raw)
},
onDateConfirm(type, event) {
const raw = event && event.value !== undefined ? event.value : Date.now()
const prevStartDate = this.filterForm.startDate
const prevEndDate = this.filterForm.endDate
this.setDateValue(type, raw)
if (!this.validateDateRange()) {
this.filterForm.startDate = prevStartDate
this.filterForm.endDate = prevEndDate
return
}
this.closeDatePicker(type)
this.fetchList()
},
validateDateRange() {
const {startDate, endDate} = this.filterForm
if (!startDate || !endDate) return true
const startTs = dayjs(`${startDate} 00:00:00`).valueOf()
const endTs = dayjs(`${endDate} 23:59:59`).valueOf()
if (startTs <= endTs) return true
uni.showToast({
title: '开始日期不能晚于结束日期',
icon: 'none'
})
return false
},
changeType(value) {
if (value === 'storeVisit') {
this.filterForm.chainId = ''
this.filterForm.merchantKeyword = ''
}
// 跑店填报没有 chain_id 列,连锁只能在已加载的选项里按名字查;
// 切到该类型时若选中的是虚拟连锁(chain_ 前缀),清掉避免查出空列表
if (value === 'visitReport') {
const chainIdText = String(this.getSelectedChainApiValue(this.filterForm.chainId) || '')
if (!chainIdText || chainIdText.startsWith('chain_')) {
this.filterForm.chainId = ''
}
}
if ((value === 'selfReport' || value === 'visitReport') && this.activeStatus === 3) {
this.activeStatus = 0
}
this.activeType = value
this.fetchList()
},
changeStatus(value) {
this.activeStatus = value
this.fetchList()
},
async reloadFromFirstPage() {
this.resetPagination()
await this.fetchList()
},
async fetchList(options = {}) {
const append = !!options.append
const requestPage = append ? (this.pageNo + 1) : 1
const pageSize = this.getPageSize()
if (append) {
if (this.loading || this.loadingMore || !this.hasMore) return
this.loadingMore = true
} else {
this.resetPagination()
this.setTaskListByType(this.activeType, [])
this.loading = true
}
try {
const isCopyQuery = this.activeStatus === 3
if (this.activeType === 'movement') {
const params = {
status: isCopyQuery ? 3 : (this.activeStatus + 1),
auditLite: 1
}
if (isCopyQuery) params.isCopy = 1
params.cateIdList = '1,3'
Object.assign(params, this.buildUserTaskFilterParams({
page: requestPage
}))
const {data} = await auditUserTaskPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeTask(item, '药动销'))
const nextList = this.filterMovementList(normalized)
const merged = append ? this.getTaskListByType('movement').concat(nextList) : nextList
this.setTaskListByType('movement', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
} else if (this.activeType === 'storeVisit') {
const params = {
status: isCopyQuery ? 3 : (this.activeStatus + 1),
cateId: 4,
auditLite: 1
}
if (isCopyQuery) params.isCopy = 1
Object.assign(params, this.buildUserTaskFilterParams({
page: requestPage,
enableChainFilter: false,
enableMerchantFilter: false
}))
const {data} = await auditUserTaskPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeTask(item, '跑店/巡店'))
const nextList = this.filterMovementList(normalized, {
enableChainFilter: false,
enableMerchantFilter: false
})
const merged = append ? this.getTaskListByType('storeVisit').concat(nextList) : nextList
this.setTaskListByType('storeVisit', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
} else if (this.activeType === 'selfReport') {
if (isCopyQuery) {
this.setTaskListByType('selfReport', [])
this.hasMore = false
} else {
const params = {
status: this.activeStatus,
page: requestPage,
limit: pageSize,
auditLite: 1
}
Object.assign(params, this.buildSelfReportFilterParams())
const {data} = await selfReportAuditPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeSelfReport(item))
const nextList = this.filterMovementList(normalized)
const merged = append ? this.getTaskListByType('selfReport').concat(nextList) : nextList
this.setTaskListByType('selfReport', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
}
} else if (this.activeType === 'visitReport') {
if (isCopyQuery) {
this.setTaskListByType('visitReport', [])
this.hasMore = false
} else {
const params = {
status: this.activeStatus,
page: requestPage,
limit: pageSize,
auditLite: 1
}
Object.assign(params, this.buildVisitReportFilterParams())
const {data} = await visitReportAuditPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeVisitReport(item))
const nextList = this.filterMovementList(normalized, {
enableChainFilter: false,
enableMerchantFilter: false
})
const merged = append ? this.getTaskListByType('visitReport').concat(nextList) : nextList
this.setTaskListByType('visitReport', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
}
} else {
const params = {
status: isCopyQuery ? 2 : this.activeStatus,
page: requestPage,
limit: pageSize,
auditLite: 1
}
if (isCopyQuery) params.isCopy = 1
Object.assign(params, this.buildShareFilterParams())
const {data} = await shareAuditPageReq(params)
const pageResult = this.extractPageResult(data)
const list = pageResult.list
const normalized = list.map(item => this.normalizeOrder(item, '药兑换'))
const nextList = this.filterExchangeList(normalized)
const merged = append ? this.getTaskListByType('exchange').concat(nextList) : nextList
this.setTaskListByType('exchange', merged)
this.updatePagination(pageResult, requestPage, list.length, pageSize)
}
} catch (err) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
console.log(err)
} finally {
if (append) {
this.loadingMore = false
} else {
this.loading = false
}
}
},
normalizeTask(item, typeLabel) {
const imageList = Array.isArray(item.imageList) ? item.imageList : this.parseJson(item.image, [])
const chainInfo = this.resolveMovementChain(item)
const task = item && item.task ? item.task : {}
const merchant = item && item.task && item.task.merchant ? item.task.merchant : {}
const user = item && item.user ? item.user : {}
const merchantName = merchant.name || merchant.merchantName || item.merchantName || item.merchant_name || ''
const merchantId = merchant.id ?? merchant.merchantId ?? merchant.merchant_id ?? item.merchantId ?? item.merchant_id ?? ''
const submitRaw = item.submitTime || item.date || item.createdAt || item.createTime || item.create_time || ''
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
return {
id: item.id,
taskId: item.taskId,
userId: item.userId,
name: item.taskTitle || task.title || '任务',
typeLabel,
taskTitle: item.taskTitle || task.title || '',
taskReward: item.taskReward !== undefined ? item.taskReward : task.reward,
taskTarget: (task.cate && task.cate.title) || task.target || '',
owner,
date: item.date || dayjs(item.date).format('YYYY-MM-DD HH:mm:ss') || item.createdAt || '-',
status: item.status,
imageList: Array.isArray(imageList) ? imageList : [],
merchantName: item.merchantName || merchantName,
merchantId,
chainName: item.chainName || chainInfo.chainName,
chainIdNormalized: chainInfo.chainId,
submitTimestamp: this.parseDateToTimestamp(submitRaw),
submitTime: this.formatDateTime(submitRaw)
}
},
normalizeOrder(item, typeLabel) {
const orderNo = item.orderNo || item.order_no || item.no || item.id
const detailListRaw = item.detailList || item.list || item.items || item.orderItems || []
const detailList = this.parseJson(detailListRaw, [])
const receiptRaw = item.image || item.imageList || item.receiptPhoto || item.receiptPhotos || []
const chainInfo = this.resolveShareChain(item)
const activityRaw = item.date || item.shareDate || item.createTime || item.createdAt || item.create_time || ''
const user = item && item.user ? item.user : {}
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
return {
id: item.id || item.orderId || item.order_id,
name: orderNo ? `订单:${orderNo}` : '订单',
typeLabel,
owner,
date: this.formatDateTime(activityRaw, '-'),
detailList: Array.isArray(detailList) ? detailList : [],
status: item.status,
amount: item.amount,
income: item.income,
image: item.image,
extraIncome: item.extraIncome,
merchantName: item.merchantName || '',
merchantId: item.merchantId || item.merchant_id || '',
chainName: item.chainName || chainInfo.chainName,
chainIdNormalized: chainInfo.chainId,
activityTimestamp: this.parseDateToTimestamp(activityRaw),
orderNo,
remark: item.remark || '',
userId: item.userId,
}
},
normalizeSelfReport(item) {
const clockPhotos = this.parseJson(item.clockPhotos, [])
const boothPhotos = this.parseJson(item.boothPhotos, [])
const salesGroups = this.parseJson(item.salesGroups, [])
const receiptPhoto = this.parseJson(item.receiptPhoto, [])
const summaryRaw = this.parseJson(item.summary, '')
const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw)
const activityDate = item.date || ''
const user = item && item.user ? item.user : {}
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
return {
id: item.id,
userId: item.userId,
merchantId: item.merchantId,
merchantName: item.merchantName || '',
chainName: item.chainName || '-',
chainIdNormalized: item.chainId || '',
owner,
date: activityDate || '-',
status: item.status,
reward: item.reward,
rating: item.rating,
location: item.location || '',
summary,
rejectReason: item.rejectReason || '',
clockPhotos: Array.isArray(clockPhotos) ? clockPhotos : [],
boothPhotos: this.normalizeReceiptPhotos(boothPhotos),
receiptPhotos: this.normalizeReceiptPhotos(receiptPhoto),
salesGroups: Array.isArray(salesGroups) ? salesGroups : [],
submitTimestamp: this.parseDateToTimestamp(activityDate)
}
},
// 跑店填报:卡片主体是巡店列表,列表接口不带 storeList(大字段),展开时再拉详情
normalizeVisitReport(item) {
const createRaw = item.createTime || item.createdAt || ''
const summaryRaw = this.parseJson(item.summary, '')
const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw)
const user = item && item.user ? item.user : {}
const owner = item.owner || `${user.realName || user.nickname || '-'},${user.phone || ''}`
return {
id: item.id,
userId: item.userId,
storeCount: Number(item.storeCount) || 0,
storeList: item.storeList || '',
owner,
status: item.status,
reward: item.reward,
location: item.location || '',
summary,
rejectReason: item.rejectReason || '',
submitTimestamp: this.parseDateToTimestamp(createRaw)
}
},
formatAmount(value) {
const num = Number(value || 0)
return num.toFixed(2)
},
async handleAction(status, item) {
if (Number(status) === 3) {
this.openRejectPopup(item, 'audit')
return
}
await this.submitAuditAction(status, item)
},
async submitAuditAction(status, item, auditReason) {
const data = {
id: item.id,
taskId: item.taskId,
userId: item.userId,
status
}
if (Number(status) === 3) {
data.auditReason = auditReason
}
await changeUserTaskReq(data)
uni.showToast({
title: '操作成功',
})
await this.fetchList()
},
buildSharePayload(item, overrides = {}) {
const base = {...item}
const payload = {
...base,
...overrides
}
delete payload.owner
delete payload.name
delete payload.typeLabel
delete payload.receiptPhotos
delete payload.chainIdNormalized
delete payload.activityTimestamp
if (!Object.prototype.hasOwnProperty.call(overrides, 'detailList')) {
delete payload.detailList
}
return payload
},
sanitizeDetailList(list) {
const items = Array.isArray(list) ? list : []
return items.map(detail => ({
goodsCode: detail.goodsCode || detail.code || '',
goodsName: detail.goodsName || detail.name || detail.title || '',
num: detail.num || detail.qty || '',
spec: detail.spec || detail.specification || '',
amount: detail.amount || '',
sourceCode: detail.sourceCode || detail.source_code || ''
}))
},
async handleActionShare(status, item) {
const data = this.buildSharePayload(item, {status})
await shareChangeReq(data)
uni.showToast({
title: '操作成功',
})
await this.fetchList()
},
async handleShareApprove({item, income}) {
if (!income || Number(income) <= 0) {
uni.showToast({
title: '请确认后再通过',
icon: 'none'
})
return
}
await shareChangeReq({
id: item.id,
status: 1,
})
uni.showToast({
title: '操作成功',
})
await this.fetchList()
},
async handleSelfReportApprove(item) {
await selfReportChangeReq({
id: item.id,
status: 1,
reward: item.reward
})
uni.showToast({
title: '操作成功'
})
await this.reloadFromFirstPage()
},
async handleSelfReportReject(item) {
this.openRejectPopup(item, 'selfReport')
},
async handleSelfReportUpdated() {
await this.reloadFromFirstPage()
},
// 跑店填报与驻店填报共用同一套审核动作,仅接口不同。
// 跑店没有销量小计,奖金得审核端手填,所以先弹输入框。
async handleVisitReportApprove(item) {
this.pendingRewardItem = item
this.rewardAmount = item.reward === null || item.reward === undefined ? '' : `${item.reward}`
this.showRewardPopup = true
},
closeRewardPopup() {
this.showRewardPopup = false
this.rewardAmount = ''
this.pendingRewardItem = null
},
async submitRewardAmount() {
const item = this.pendingRewardItem
if (!item) {
this.closeRewardPopup()
return
}
const raw = String(this.rewardAmount === null || this.rewardAmount === undefined ? '' : this.rewardAmount).trim()
if (!raw) {
uni.showToast({title: '请输入奖金金额', icon: 'none'})
return
}
const reward = Number(raw)
if (!Number.isFinite(reward) || reward < 0) {
uni.showToast({title: '奖金金额不能小于0', icon: 'none'})
return
}
await visitReportChangeReq({
id: item.id,
status: 1,
reward
})
uni.showToast({
title: '操作成功'
})
this.closeRewardPopup()
await this.reloadFromFirstPage()
},
async handleVisitReportReject(item) {
this.openRejectPopup(item, 'visitReport')
},
async handleShareUpdate({item, payload, localDetailList}) {
const data = this.buildSharePayload(item, payload)
await shareChangeReq(data)
uni.showToast({
title: '修改成功',
})
// this.exchangeTasks = this.exchangeTasks.map(task => {
// if (task.id !== item.id) return task
// const orderNo = payload.orderNo || task.orderNo || ''
// return {
// ...task,
// ...payload,
// orderNo,
// name: orderNo ? `订单:${orderNo}` : task.name,
// detailList: Array.isArray(localDetailList) ? localDetailList : task.detailList
// }
// })
await this.fetchList()
},
openRejectPopup(item, type = 'audit') {
this.pendingReject = item
this.pendingRejectType = type
this.rejectReason = ''
this.showRejectPopup = true
},
closeRejectPopup() {
this.showRejectPopup = false
this.rejectReason = ''
this.pendingReject = null
this.pendingRejectType = 'audit'
},
async submitRejectReason() {
const reason = (this.rejectReason || '').trim()
if (!this.isSelfReportLikeRejectType(this.pendingRejectType) && !reason) {
uni.showToast({
title: '请输入拒绝原因',
icon: 'none'
})
return
}
if (!this.pendingReject) {
this.closeRejectPopup()
return
}
if (this.isSelfReportLikeRejectType(this.pendingRejectType)) {
const req = this.pendingRejectType === 'visitReport' ? visitReportChangeReq : selfReportChangeReq
await req({
id: this.pendingReject.id,
status: 2,
rejectReason: reason
})
uni.showToast({
title: '操作成功'
})
await this.reloadFromFirstPage()
this.closeRejectPopup()
return
}
await this.submitAuditAction(3, this.pendingReject, reason)
this.closeRejectPopup()
},
// 驻店/跑店填报的驳回都是「驳回原因可不填」
isSelfReportLikeRejectType(type) {
return type === 'selfReport' || type === 'visitReport'
},
preview(url, list) {
uni.previewImage({
current: url,
urls: list
})
},
parseJson(value, fallback) {
if (value === null || value === undefined || value === '') return fallback
if (typeof value === 'string') {
try {
return JSON.parse(value)
} catch (err) {
return value
}
}
return value
},
parseDateToTimestamp(value) {
if (value === null || value === undefined || value === '') return NaN
const raw = String(value).trim()
if (!raw) return NaN
if (/^-?\d+(\.\d+)?$/.test(raw)) {
const num = Number(raw)
if (!Number.isFinite(num)) return NaN
return Math.abs(num) < 1e12 ? num * 1000 : num
}
const parsed = dayjs(raw)
return parsed.isValid() ? parsed.valueOf() : NaN
},
formatDateTime(value, fallback = '') {
if (value === null || value === undefined || value === '') return fallback
const raw = String(value).trim()
if (!raw) return fallback
if (/^-?\d+(\.\d+)?$/.test(raw)) {
const num = Number(raw)
if (!Number.isFinite(num)) return fallback
const parsedNumber = dayjs(Math.abs(num) < 1e12 ? num * 1000 : num)
return parsedNumber.isValid() ? parsedNumber.format('YYYY-MM-DD HH:mm:ss') : fallback
}
const parsed = dayjs(raw)
if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss')
return raw.replace('T', ' ')
},
resolveShareChain(item) {
const rawChainId = item.chainId ?? item.chain_id ?? item.chainCode ?? item.chain_code ?? ''
const merchantId = item.merchantId ?? item.merchant_id ?? ''
const directName = item.chainName || item.chain_name || item.chain || item.chainAbb || ''
let chainId = rawChainId
let chainName = directName
const merchantChain = merchantId !== '' && merchantId !== null && merchantId !== undefined
? this.chainMerchantMap[String(merchantId)]
: null
if (!chainId && merchantChain) {
chainId = merchantChain.chainId
}
if (!chainName && merchantChain) {
chainName = merchantChain.chainName
}
if (!chainName && chainId !== '' && chainId !== null && chainId !== undefined) {
const selected = this.findChainOption(chainId, chainName)
chainName = selected ? selected.text : ''
}
return {
chainId: chainId ?? '',
chainName: chainName || '-'
}
},
resolveMovementChain(item) {
const task = item && item.task ? item.task : {}
const merchant = task && task.merchant ? task.merchant : {}
const rawChainId = item.chainId
?? item.chain_id
?? task.chainId
?? task.chain_id
?? merchant.chainId
?? merchant.chain_id
?? merchant.chainCode
?? merchant.chain_code
?? ''
const merchantId = item.merchantId
?? item.merchant_id
?? task.merchantId
?? task.merchant_id
?? merchant.id
?? merchant.merchantId
?? merchant.merchant_id
?? ''
const directName = item.chainName
|| item.chain_name
|| task.chainName
|| task.chain_name
|| merchant.chainName
|| merchant.chain_name
|| merchant.chain
|| merchant.chainAbb
|| ''
let chainId = rawChainId
let chainName = directName
const merchantChain = merchantId !== '' && merchantId !== null && merchantId !== undefined
? this.chainMerchantMap[String(merchantId)]
: null
if (!chainId && merchantChain) {
chainId = merchantChain.chainId
}
if (!chainName && merchantChain) {
chainName = merchantChain.chainName
}
if (!chainName && chainId !== '' && chainId !== null && chainId !== undefined) {
const selected = this.findChainOption(chainId, chainName)
chainName = selected ? selected.text : ''
}
return {
chainId: chainId ?? '',
chainName: chainName || '-'
}
},
normalizeChainDictOption(item, index) {
const text = item.dictDataName || item.label || item.name || item.title || item.text || `连锁${index + 1}`
const code = item.dictDataCode ?? item.code ?? item.value ?? ''
const value = item.dictDataValue ?? item.value ?? item.code ?? ''
const rawId = item.id ?? code ?? value ?? `chain_${index}`
return {
id: rawId,
text,
code,
value
}
},
findChainOption(chainId, chainName = '') {
const chainIdText = String(chainId ?? '').trim()
const chainNameText = String(chainName || '').trim()
return this.chainOptions.find(option => {
const candidates = [
option.id,
option.code,
option.value,
option.text
]
.map(value => String(value ?? '').trim())
.filter(Boolean)
if (chainIdText && candidates.includes(chainIdText)) return true
return !!chainNameText && candidates.includes(chainNameText)
}) || null
},
matchesSelectedChain(chainId, chainName, selectedId) {
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
if (!option) {
return String(chainId ?? '').trim() === String(selectedId).trim()
}
const selectedValues = [
option.id,
option.code,
option.value,
option.text
]
.map(value => String(value ?? '').trim())
.filter(Boolean)
const itemValues = [
chainId,
chainName
]
.map(value => String(value ?? '').trim())
.filter(Boolean)
return itemValues.some(value => selectedValues.includes(value))
},
getSelectedChainApiValue(selectedId) {
if (selectedId === '' || selectedId === null || selectedId === undefined) return ''
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
if (!option) return selectedId
return option.code || option.value || option.id
},
normalizeMerchantChain(item, index) {
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'name', 'title']) || `连锁${index + 1}`
const rawChainId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? ''
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
? `chain_${index}`
: rawChainId
const childCandidates = ['children', 'merchantList', 'merchants', 'shopList', 'list', 'records']
let rawChildren = []
for (let i = 0; i < childCandidates.length; i++) {
const list = item[childCandidates[i]]
if (Array.isArray(list) && list.length) {
rawChildren = list
break
}
}
const merchants = rawChildren
.map(child => this.normalizeMerchant(child))
.filter(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.id).trim() !== '')
return {
id: chainId,
text: chainName,
merchants
}
},
normalizeMerchant(item) {
const id = item.id ?? item.merchantId ?? item.merchant_id
const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
return {
id,
text
}
},
pickFirstText(source, keys = []) {
if (!source || typeof source !== 'object') return ''
for (let i = 0; i < keys.length; i++) {
const value = source[keys[i]]
if (value !== undefined && value !== null && String(value).trim()) {
return String(value).trim()
}
}
return ''
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList']
for (let i = 0; i < keys.length; i++) {
const value = data[keys[i]]
if (Array.isArray(value)) return value
if (value && typeof value === 'object') {
const nested = this.unwrapList(value)
if (nested.length) return nested
}
}
return [data]
},
resolveMediaUrl(media) {
if (!media) return ''
if (typeof media === 'string') return media
return media.url || media.path || media.image || ''
},
normalizeReceiptPhotos(value) {
if (!value) return []
const list = Array.isArray(value) ? value : [value]
return list.map(item => this.resolveMediaUrl(item)).filter(Boolean)
}
}
}
</script>
<style lang="scss" scoped>
.audit-page {
min-height: 100vh;
//padding: 0 20rpx 40rpx;
background: #f5f5f5;
}
.page-hero {
//padding: 30rpx 6rpx 18rpx;
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat top center / 100% auto;
}
.page-navbar {
padding: 0 6rpx;
}
.intro-card {
margin: 18rpx 20rpx 0;
padding: 20rpx 18rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
}
.card-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 700;
color: #1f2937;
}
.title-bar {
width: 6rpx;
height: 30rpx;
border-radius: 999rpx;
background: #2fc6a7;
}
.intro-text {
margin-top: 12rpx;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
}
.subsection {
margin: 18rpx 20rpx 0;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.type-tabs,
.status-tabs {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
}
.tab {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: #f5f8ff;
color: var(--text-sub);
font-size: 24rpx;
border: 1rpx solid #e1e8f5;
}
.tab.active {
color: #ffffff;
background: var(--brand-primary);
border-color: var(--brand-primary);
}
.block {
margin: 18rpx 20rpx 0;
}
.filter-row {
margin-top: 14rpx;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.filter-chip {
padding: 8rpx 18rpx;
border-radius: 999rpx;
border: 1rpx solid #2fc6a7;
color: #2fc6a7;
font-size: 22rpx;
background: #f3fffb;
}
.group-input {
flex: 1;
min-width: 180rpx;
height: 52rpx;
padding: 0 18rpx;
border-radius: 999rpx;
border: 1rpx solid #2fc6a7;
background: #f3fffb;
color: #374151;
font-size: 22rpx;
}
.keyword-row {
margin-top: 14rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.keyword-input {
flex: 1;
height: 64rpx;
border-radius: 14rpx;
border: 1rpx solid #d1d5db;
background: #ffffff;
padding: 0 16rpx;
font-size: 24rpx;
color: #374151;
}
.keyword-placeholder {
color: #9ca3af;
}
.filter-btn {
min-width: 96rpx;
height: 64rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid #d1d5db;
color: #6b7280;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
}
.filter-btn.primary {
border-color: #2fc6a7;
background: #2fc6a7;
color: #ffffff;
}
.action-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14rpx;
}
.action-btn {
font-size: 30rpx;
}
.danger-btn {
background: linear-gradient(135deg, #f5576c, #f093fb);
color: #ffffff;
border-radius: 999rpx;
padding: 18rpx 32rpx;
text-align: center;
}
.tip {
color: var(--text-sub);
font-size: 24rpx;
margin-top: 12rpx;
}
.empty {
color: var(--text-sub);
text-align: center;
padding: 16rpx 0;
}
.load-more-tip {
color: #9ca3af;
text-align: center;
font-size: 22rpx;
padding: 24rpx 0 32rpx;
}
</style>