Files
yxe-mp/packageUser/self-report/records.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

1063 lines
31 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="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>