Files
yxe-mp/packageUser/self-report/records.vue
T
weicw-dshsa 2ff8d42207 fix(自主填报): 修正重提跳转按填报类型、补 wx:key 并清理废弃 API 调用
一、填报记录「重新提交」跳错表单(用户反馈)

records.vue 原先按 activeType(当前 tab)决定跳驻店还是跑店表单,
但列表请求无守卫:切 tab 会连发请求且响应顺序不保证,
旧响应回来会盖掉当前 tab 的数据,导致 tab 与实际数据错位。
此时点「重新提交」会拿 A 表的 id 去打开 B 表的表单
(两表 id 各自从 1 开始,撞号常见,故不报错、只静默进错页面)。

改法:把填报类型钉在记录上,而不是靠 tab 状态猜
- fetchList 拉到数据后给每条记录打 reportType
- 新增 recordType(item):优先取记录自带类型,异常数据回退当前 tab
- 归一化、筛选、卡片标题/副标题、展开详情形态、重新提交跳转全部改走它
- 列表请求加自增 listRequestId 守卫,丢弃过期响应(loading 同理)

二、visit.vue 缺 wx:key 告警

小程序 wx:key 只接受简单属性路径,模板字面量写法
(:key="`store-${idx}`")会被编译器静默丢弃,四处循环因此无 key。
按项目既有 _rowKey 模式改:
- 巡店列表 :key="item._rowKey",新增 buildStoreKey/ensureStoreKeys
  (重提与草稿的旧数据无该字段,加载时补齐;提交走显式白名单,不外泄)
- 照片 :key="photo",两个 picker :key="item.id"

三、getSystemInfoSync 废弃告警

该调用在 AppNavbar 的 render(getSafeTop)里,每渲染一次告警一次。
改为挂载时取一次并缓存,优先用新接口 uni.getWindowInfo,
取不到再回退旧接口;select.vue 同源调用一并处理。

注:uview 组件库内部 u-overlay/u-popup 的 mode 传 null 告警
(u-popup 的 position computed 无 else 分支)未改,
uni_modules 已被 gitignore 且会被依赖重装覆盖,属组件库问题。
2026-10-12 13:47:03 +08:00

1084 lines
32 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="records-page">
<AppNavbar title="填报记录" showBack/>
<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 statusTabs"
: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 class="filter-chip" @tap="openChainPicker">{{ chainLabel }}</view>
<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-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="block">
<view class="section-title">记录列表</view>
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="filteredRecords.length === 0" class="empty">暂无记录</view>
<view v-else>
<view v-for="item in filteredRecords" :key="item.id" class="record-card">
<view class="record-head">
<view>
<view class="record-title">{{ cardTitle(item) }}</view>
<view class="record-sub" v-if="cardSubTitle(item)">{{ cardSubTitle(item) }}</view>
</view>
<view :class="['status-badge', statusClass(item.status)]">{{ statusText(item.status) }}</view>
</view>
<view class="record-grid">
<view v-if="item.reportType === 'store'" class="record-kv">
<text class="record-label">活动时间</text>
<text class="record-value">{{ item.activityTime || '-' }}</text>
</view>
<view v-else class="record-kv">
<text class="record-label">巡店门店数</text>
<text class="record-value">{{ item.storeCount || 0 }}家</text>
</view>
<view v-if="item.reportType === 'store'" class="record-kv">
<text class="record-label">评分</text>
<text class="record-value">{{ item.rating || '-' }}星</text>
</view>
<view class="record-kv">
<text class="record-label">提交时间</text>
<text class="record-value">{{ item.submitTime || '-' }}</text>
</view>
</view>
<view class="summary-line">小结:{{ item.summary || '-' }}</view>
<view v-if="isRejected(item.status) && item.rejectReason" class="reject-line">
驳回原因:{{ item.rejectReason }}
</view>
<view class="record-toggle" @tap="toggleExpanded(item.id)">
<text>{{ isExpanded(item.id) ? '收起详情' : '展开详情' }}</text>
<u-icon :name="isExpanded(item.id) ? 'arrow-up' : 'arrow-down'" size="20" color="#2fc6a7"/>
</view>
<view v-if="isExpanded(item.id)" class="record-detail">
<!-- 跑店填报:巡店列表(驻店填报没有门店列表,走下面的固定区块) -->
<block v-if="item.reportType === 'visit'">
<view v-if="!item.storeRows.length" class="detail-empty">暂无巡店记录</view>
<view v-for="(store, sIdx) in item.storeRows" :key="`store-${sIdx}`" class="store-block">
<view class="store-block-head">
<text class="store-block-title">巡店{{ sIdx + 1 }}</text>
<text class="store-block-time">{{ store.visitTime || '-' }}</text>
</view>
<view class="store-block-line">门店:{{ store.merchantName || '-' }}</view>
<view class="store-block-line">连锁:{{ store.chainName || '-' }}</view>
<view class="store-block-line">
定位:{{ store.location || '-' }}
<text v-if="store.lat && store.lng">({{ store.lat }}, {{ store.lng }})</text>
</view>
<view v-if="store.photos.length" class="media-images">
<image
v-for="(img, imgIdx) in store.photos"
:key="`store-${sIdx}-photo-${imgIdx}`"
class="media-image"
:src="img"
mode="aspectFill"
@tap="preview(img, store.photos)"
/>
</view>
<view v-else class="detail-empty">暂无巡店照片</view>
</view>
</block>
<block v-else>
<view class="record-section">
<view class="record-section-title">上下班打卡</view>
<view v-if="item.clockImages.length" class="media-images">
<image
v-for="(img, idx) in item.clockImages"
:key="`clock-${idx}`"
class="media-image"
:src="img"
mode="aspectFill"
@tap="preview(img, item.clockImages)"
/>
</view>
<view v-else class="detail-empty">暂无打卡图片</view>
</view>
<view class="record-section">
<view class="record-section-title">门头摆台照</view>
<view v-if="item.boothImages.length" class="media-images">
<image
v-for="(img, idx) in item.boothImages"
:key="`booth-${idx}`"
class="media-image"
:src="img"
mode="aspectFill"
@tap="preview(img, item.boothImages)"
/>
</view>
<view v-else class="detail-empty">暂无门头摆台照</view>
</view>
<view class="record-section">
<view class="record-section-title">小票上传</view>
<view v-if="item.receiptImages.length" class="media-images">
<image
v-for="(img, idx) in item.receiptImages"
:key="`receipt-${idx}`"
class="media-image"
:src="img"
mode="aspectFill"
@tap="preview(img, item.receiptImages)"
/>
</view>
<view v-else class="detail-empty">暂无小票</view>
</view>
<view class="record-section">
<view class="record-section-title">销量填写</view>
<view v-if="item.salesRows.length" class="sales-list">
<view class="sales-item" v-for="(detail, idx) in item.salesRows" :key="`sales-${idx}`">
<view class="sales-head">
<view class="sales-name">{{ detail.name }}</view>
<view class="sales-qty">{{ detail.qty }}{{ detail.unit }}</view>
</view>
<view class="sales-meta">
<text>{{ detail.groupTitle }}</text>
<text>{{ detail.makerAbb || '-' }}</text>
<text>{{ detail.spec || '-' }}</text>
</view>
</view>
</view>
<view v-else class="detail-empty">暂无销量数据</view>
</view>
</block>
</view>
<view v-if="isRejected(item.status)" class="record-actions">
<view class="resubmit-btn" @tap="handleResubmit(item)">重新提交</view>
</view>
</view>
</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"
@confirm="e => onDateConfirm('start', e)"
@cancel="closeDatePicker('start')"
@close="closeDatePicker('start')"
/>
<u-datetime-picker
:show="showEndPicker"
:value="endPickerValue"
mode="date"
@confirm="e => onDateConfirm('end', e)"
@cancel="closeDatePicker('end')"
@close="closeDatePicker('end')"
/>
<Login ref="Login" @done="handleLoginDone"/>
</view>
</template>
<script>
import dayjs from "dayjs";
import AppNavbar from "@/components/AppNavbar.vue";
import Login from "@/components/Login.vue";
import {dictDataListByDictReq} from "@/api/common";
import {selfReportUserListReq} from "@/api/self-report";
import {visitReportUserListReq} from "@/api/visit-report";
import {getUserInfo} from "@/util/user";
export default {
components: {AppNavbar, Login},
data() {
return {
// 驻店填报 = yxe_self_report,跑店填报 = yxe_visit_report(两张表,分别拉取)
typeTabs: [
{label: '驻店填报', value: 'store'},
{label: '跑店填报', value: 'visit'}
],
activeType: 'store',
statusTabs: [
{label: '全部', value: 'all'},
{label: '待审核', value: 0},
{label: '已通过', value: 1},
{label: '已驳回', value: 2}
],
activeStatus: 'all',
records: [],
// 列表请求自增序号:切 tab 会连发请求且响应顺序不保证,只认最后一次,丢弃过期响应
listRequestId: 0,
loading: false,
chainLoading: false,
showChainPicker: false,
showStartPicker: false,
showEndPicker: false,
chainOptions: [{id: '', text: '全部连锁'}],
filterForm: {
chainId: '',
merchantKeyword: '',
startDate: '',
endDate: ''
},
expandedMap: {}
}
},
computed: {
normalizedRecords() {
return (Array.isArray(this.records) ? this.records : []).map(item => this.normalizeRecord(item))
},
filteredRecords() {
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 this.normalizedRecords.filter(item => {
if (this.activeStatus !== 'all' && Number(item.status) !== Number(this.activeStatus)) {
return false
}
if (this.filterForm.chainId && !this.matchesItemChain(item)) {
return false
}
if (keyword && !this.matchesItemKeyword(item, 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
})
},
chainLabel() {
const selected = this.chainOptions.find(item => String(item.id) === String(this.filterForm.chainId))
return (selected && selected.text) || '全部连锁'
},
chainPickerIndex() {
const idx = this.chainOptions.findIndex(item => String(item.id) === String(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()
}
},
async onShow() {
if (!this.ensureLogin()) return
await this.initPage()
},
async onPullDownRefresh() {
try {
if (this.ensureLogin()) {
await this.initPage()
}
} finally {
uni.stopPullDownRefresh()
}
},
methods: {
ensureLogin() {
if (getUserInfo().token) return true
this.$refs.Login.open()
return false
},
async handleLoginDone() {
await this.initPage()
},
async initPage() {
this.expandedMap = {}
await Promise.all([
this.fetchChainList(),
this.fetchList()
])
},
async fetchList() {
const requestId = ++this.listRequestId
const type = this.activeType
this.loading = true
try {
const req = type === 'visit' ? visitReportUserListReq : selfReportUserListReq
const {data} = await req({})
// 响应顺序不保证:切 tab 后返回的旧响应直接丢弃,避免另一类型的数据盖进当前 tab
if (requestId !== this.listRequestId) return
this.records = this.unwrapList(data).map(item => {
if (!item || typeof item !== 'object') return item
// 类型钉在记录上:后续归一化/筛选/重新提交都以它为准,不再依赖当前 tab
return Object.assign({}, item, {reportType: type})
})
} catch (error) {
if (requestId !== this.listRequestId) return
this.records = []
uni.showToast({
title: '记录加载失败',
icon: 'none'
})
} finally {
if (requestId === this.listRequestId) {
this.loading = false
}
}
},
async fetchChainList() {
this.chainLoading = true
try {
const {data} = await dictDataListByDictReq({dictId: 175})
const list = Array.isArray(data) ? data : this.unwrapList(data)
const options = [{id: '', text: '全部连锁'}]
list
.map((item, index) => this.normalizeChainDictOption(item, index))
.filter(item => item.id !== '' && item.id !== null && item.id !== undefined)
.forEach(item => options.push(item))
this.chainOptions = options
} catch (error) {
this.chainOptions = [{id: '', text: '全部连锁'}]
} finally {
this.chainLoading = false
}
},
// 记录所属填报类型:优先取记录自带的 reportType,历史/异常数据回退当前 tab
recordType(item) {
const type = item && item.reportType
return type === 'visit' || type === 'store' ? type : this.activeType
},
normalizeRecord(item = {}) {
if (this.recordType(item) === 'visit') {
return this.normalizeVisitRecord(item)
}
const createRaw = item.createTime || item.createdAt || item.date || ''
const clockPhotos = this.parseMaybeJson(item.clockPhotos, [])
const boothPhotos = this.parseMaybeJson(item.boothPhotos, [])
const receiptPhoto = this.parseMaybeJson(item.receiptPhoto, [])
const salesGroups = this.parseMaybeJson(item.salesGroups, [])
const summaryRaw = this.parseMaybeJson(item.summary, item.summary || '')
const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw || '')
return {
id: item.id,
reportType: 'store',
merchantId: item.merchantId || item.merchant_id || '',
merchantName: item.merchantName || item.merchant_name || '',
chainName: item.chainName || item.chain_name || '-',
chainIdNormalized: item.chainId || item.chain_id || '',
activityTime: item.activityTime || item.activity_time || '',
date: item.date || '',
status: item.status,
rejectReason: item.rejectReason || item.reject_reason || '',
reward: item.reward,
rating: item.rating,
summary,
submitTime: this.formatDateTime(createRaw, createRaw || '-'),
submitTimestamp: this.parseDateToTimestamp(createRaw),
clockImages: this.normalizeMediaList(clockPhotos),
boothImages: this.normalizeMediaList(boothPhotos),
receiptImages: this.normalizeMediaList(receiptPhoto),
salesRows: this.buildSalesRows(salesGroups),
storeCount: 0,
storeRows: []
}
},
// 跑店填报没有单门店/活动时间/打卡/销量,卡片主体是巡店列表
normalizeVisitRecord(item = {}) {
const createRaw = item.createTime || item.createdAt || ''
const summaryRaw = this.parseMaybeJson(item.summary, item.summary || '')
const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw || '')
const storeList = this.parseMaybeJson(item.storeList, [])
const storeRows = (Array.isArray(storeList) ? storeList : []).map(store => ({
merchantName: store && (store.merchantName || store.storeName) ? (store.merchantName || store.storeName) : '',
chainName: store && store.chainName ? store.chainName : '',
visitTime: store && (store.visitTime || store.time) ? (store.visitTime || store.time) : '',
location: store && store.location ? store.location : '',
lat: store && store.lat !== undefined ? store.lat : '',
lng: store && store.lng !== undefined ? store.lng : '',
photos: this.normalizeMediaList(store && store.photos)
}))
return {
id: item.id,
reportType: 'visit',
merchantId: '',
merchantName: '',
chainName: '-',
chainIdNormalized: '',
activityTime: '',
date: '',
status: item.status,
rejectReason: item.rejectReason || item.reject_reason || '',
reward: item.reward,
rating: '',
summary,
submitTime: this.formatDateTime(createRaw, createRaw || '-'),
submitTimestamp: this.parseDateToTimestamp(createRaw),
clockImages: [],
boothImages: [],
receiptImages: [],
salesRows: [],
// 门店数与主门店关键词都从列表内容推导,避免列表页再下发冗余字段
storeCount: Number(item.storeCount) || storeRows.length,
storeRows
}
},
buildSalesRows(groups) {
const rows = []
;(Array.isArray(groups) ? groups : []).forEach((group, gIdx) => {
const items = Array.isArray(group && group.items) ? group.items : []
items.forEach((detail, iIdx) => {
rows.push({
key: `${gIdx}-${iIdx}`,
groupTitle: group && group.title ? group.title : `分组${gIdx + 1}`,
name: detail && detail.name ? detail.name : '-',
makerAbb: detail && detail.makerAbb ? detail.makerAbb : '',
spec: detail && detail.spec ? detail.spec : '',
qty: detail && detail.qty !== undefined && detail.qty !== null ? detail.qty : 0,
unit: detail && detail.unit ? detail.unit : ''
})
})
})
return rows
},
normalizeMediaList(value) {
const list = Array.isArray(value) ? value : []
return list.map(item => this.resolveMediaUrl(item)).filter(Boolean)
},
resolveMediaUrl(media) {
if (!media) return ''
if (typeof media === 'string') return media
return media.url || media.path || media.image || ''
},
parseMaybeJson(value, fallback) {
if (value === null || value === undefined || value === '') return fallback
if (typeof value === 'string') {
try {
return JSON.parse(value)
} catch (error) {
return fallback
}
}
return value
},
unwrapList(data) {
if (Array.isArray(data)) return data
if (!data || typeof data !== 'object') return []
const keys = ['list', 'records', 'rows', 'items', 'data', 'result']
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 []
},
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
}
},
// 跑店记录没有单一门店/连锁,判定要落到巡店列表的每一行
matchesItemChain(item) {
const selectedId = this.filterForm.chainId
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
if (this.recordType(item) === 'visit') {
return (item.storeRows || []).some(store => this.matchesSelectedChain('', store.chainName, selectedId))
}
return this.matchesSelectedChain(item.chainIdNormalized, item.chainName, selectedId)
},
matchesItemKeyword(item, keyword) {
if (this.recordType(item) === 'visit') {
return (item.storeRows || []).some(store => String(store.merchantName || '').toLowerCase().includes(keyword))
}
return String(item.merchantName || '').toLowerCase().includes(keyword)
},
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))
},
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', ' ')
},
formatPrice(value) {
const num = Number(value || 0)
return Number.isFinite(num) ? num.toFixed(2) : '0.00'
},
statusText(status) {
if (Number(status) === 0) return '待审核'
if (Number(status) === 1) return '已通过'
if (Number(status) === 2) return '已驳回'
return '-'
},
statusClass(status) {
if (Number(status) === 1) return 'success'
if (Number(status) === 2) return 'danger'
return 'pending'
},
isRejected(status) {
return Number(status) === 2
},
handleResubmit(item) {
if (!item || item.id === undefined || item.id === null) return
// 按记录自带的类型跳转,不用当前 tab —— tab 与实际数据错位时也不会进错表单
const url = this.recordType(item) === 'visit'
? `/packageUser/self-report/visit?id=${item.id}&mode=resubmit`
: `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
uni.navigateTo({url})
},
cardTitle(item) {
if (this.recordType(item) === 'visit') {
const names = (item.storeRows || [])
.map(store => store.merchantName)
.filter(Boolean)
if (names.length) {
return names.length > 2 ? `${names.slice(0, 2).join('、')} 等${names.length}家` : names.join('、')
}
return '跑店填报'
}
return item.merchantName || '-'
},
cardSubTitle(item) {
if (this.recordType(item) === 'visit') return ''
return item.chainName && item.chainName !== '-' ? `连锁:${item.chainName}` : ''
},
changeType(value) {
if (this.activeType === value) return
this.activeType = value
this.expandedMap = {}
this.records = []
this.fetchList()
},
changeStatus(value) {
this.activeStatus = value
},
handleQuery() {
this.validateDateRange()
},
async handleReset() {
this.activeStatus = 'all'
this.filterForm = {
chainId: '',
merchantKeyword: '',
startDate: '',
endDate: ''
}
this.expandedMap = {}
},
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
},
openDatePicker(type) {
if (type === 'start') {
this.showStartPicker = true
return
}
this.showEndPicker = true
},
closeDatePicker(type) {
if (type === 'start') {
this.showStartPicker = false
return
}
this.showEndPicker = false
},
onDateConfirm(type, event) {
const value = dayjs(event && event.value !== undefined ? event.value : Date.now()).format('YYYY-MM-DD')
const prevStartDate = this.filterForm.startDate
const prevEndDate = this.filterForm.endDate
if (type === 'start') {
this.filterForm.startDate = value
} else {
this.filterForm.endDate = value
}
if (!this.validateDateRange()) {
this.filterForm.startDate = prevStartDate
this.filterForm.endDate = prevEndDate
return
}
this.closeDatePicker(type)
},
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
},
toggleExpanded(id) {
this.$set(this.expandedMap, id, !this.expandedMap[id])
},
isExpanded(id) {
return !!this.expandedMap[id]
},
preview(url, list) {
uni.previewImage({
current: url,
urls: Array.isArray(list) ? list : [url]
})
}
}
}
</script>
<style lang="scss" scoped>
.records-page {
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 40rpx;
}
.subsection {
margin: 18rpx 20rpx 0;
}
.block {
margin: 18rpx 20rpx 0;
}
.type-tabs {
display: flex;
gap: 12rpx;
padding-bottom: 16rpx;
margin-bottom: 16rpx;
border-bottom: 1rpx solid #eef2f7;
}
.status-tabs {
display: flex;
gap: 12rpx;
flex-wrap: wrap;
}
.tab {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: #f5f8ff;
color: #6b7280;
font-size: 24rpx;
border: 1rpx solid #e1e8f5;
}
.tab.active {
color: #ffffff;
background: #2fc6a7;
border-color: #2fc6a7;
}
.section-title {
font-size: 30rpx;
font-weight: 700;
color: #111827;
}
.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;
}
.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;
}
.empty {
color: #6b7280;
text-align: center;
padding: 40rpx 0;
font-size: 26rpx;
}
.record-card {
margin-top: 16rpx;
padding: 22rpx;
border-radius: 22rpx;
background: #ffffff;
border: 1rpx solid #eef2f7;
box-shadow: 0 8rpx 20rpx rgba(37, 99, 235, 0.06);
}
.record-head {
display: flex;
justify-content: space-between;
gap: 18rpx;
}
.record-title {
color: #111827;
font-size: 30rpx;
font-weight: 700;
}
.record-sub {
margin-top: 8rpx;
color: #6b7280;
font-size: 24rpx;
}
.status-badge {
align-self: flex-start;
padding: 8rpx 16rpx;
border-radius: 999rpx;
font-size: 22rpx;
white-space: nowrap;
}
.status-badge.pending {
color: #b45309;
background: #fff7ed;
}
.status-badge.success {
color: #047857;
background: #ecfdf5;
}
.status-badge.danger {
color: #dc2626;
background: #fef2f2;
}
.record-grid {
margin-top: 18rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14rpx;
}
.record-kv {
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.record-label {
color: #9ca3af;
font-size: 22rpx;
}
.record-value {
color: #1f2937;
font-size: 24rpx;
word-break: break-all;
}
.text-red {
color: #dc2626;
}
.summary-line {
margin-top: 16rpx;
color: #4b5563;
font-size: 24rpx;
line-height: 1.5;
word-break: break-word;
}
.reject-line {
margin-top: 10rpx;
color: #dc2626;
font-size: 24rpx;
line-height: 1.5;
word-break: break-word;
}
.record-actions {
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #eef2f7;
display: flex;
justify-content: flex-end;
}
.resubmit-btn {
min-width: 200rpx;
min-height: 72rpx;
padding: 0 32rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #2fc6a7, #1db896);
color: #ffffff;
font-size: 26rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10rpx 20rpx rgba(47, 198, 167, 0.22);
}
.record-toggle {
margin-top: 16rpx;
display: inline-flex;
align-items: center;
gap: 8rpx;
color: #2fc6a7;
font-size: 24rpx;
}
.record-detail {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #eef2f7;
}
.record-section {
margin-top: 16rpx;
}
.record-section:first-child {
margin-top: 0;
}
.record-section-title {
margin-bottom: 12rpx;
color: #111827;
font-size: 26rpx;
font-weight: 700;
}
.media-images {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.media-image {
width: 150rpx;
height: 150rpx;
border-radius: 14rpx;
background: #f3f4f6;
}
.detail-empty {
color: #9ca3af;
font-size: 24rpx;
}
.store-block {
padding: 18rpx;
margin-bottom: 16rpx;
border-radius: 16rpx;
background: #f9fafb;
border: 1rpx solid #eef2f7;
}
.store-block:last-child {
margin-bottom: 0;
}
.store-block-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.store-block-title {
font-size: 26rpx;
font-weight: 700;
color: #111827;
}
.store-block-time {
font-size: 22rpx;
color: #9ca3af;
}
.store-block-line {
margin-bottom: 8rpx;
font-size: 24rpx;
color: #4b5563;
line-height: 1.5;
word-break: break-all;
}
.store-block .media-images {
margin-top: 10rpx;
}
.sales-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.sales-item {
padding: 16rpx;
border-radius: 16rpx;
background: #f9fafb;
}
.sales-head {
display: flex;
justify-content: space-between;
gap: 16rpx;
}
.sales-name {
color: #111827;
font-size: 26rpx;
font-weight: 700;
word-break: break-word;
}
.sales-qty {
color: #dc2626;
font-size: 24rpx;
white-space: nowrap;
}
.sales-meta {
margin-top: 8rpx;
display: flex;
flex-wrap: wrap;
gap: 10rpx;
color: #6b7280;
font-size: 22rpx;
}
</style>