aff1e52e3e
「自主填报」原本只有一种表单。本次拆成两种类型:
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 共用一份,
避免两页各自维护导致解析口径不一致
1063 lines
31 KiB
Vue
1063 lines
31 KiB
Vue
<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="activeType === '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="activeType === '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="activeType === '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: [],
|
||
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() {
|
||
this.loading = true
|
||
try {
|
||
const req = this.activeType === 'visit' ? visitReportUserListReq : selfReportUserListReq
|
||
const {data} = await req({})
|
||
this.records = this.unwrapList(data)
|
||
} catch (error) {
|
||
this.records = []
|
||
uni.showToast({
|
||
title: '记录加载失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
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
|
||
}
|
||
},
|
||
normalizeRecord(item = {}) {
|
||
if (this.activeType === '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,
|
||
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,
|
||
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.activeType === 'visit') {
|
||
return (item.storeRows || []).some(store => this.matchesSelectedChain('', store.chainName, selectedId))
|
||
}
|
||
return this.matchesSelectedChain(item.chainIdNormalized, item.chainName, selectedId)
|
||
},
|
||
matchesItemKeyword(item, keyword) {
|
||
if (this.activeType === '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
|
||
const url = this.activeType === '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.activeType === '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.activeType === '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>
|