Files
yxe-mp/packageUser/self-report/create.vue
T
2026-09-29 15:17:13 +08:00

2081 lines
65 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="self-report-page">
<AppNavbar title="自主填报" showBack/>
<view class="sr-merchant-row" @tap="openChainPicker">
<view class="sr-merchant-label">连锁筛选</view>
<view class="sr-merchant-right">
<text :class="form.chainId ? 'sr-merchant-text' : 'sr-merchant-placeholder'">
{{ selectedChainText }}
</text>
<u-icon name="arrow-right" size="20" color="#9ca3af"/>
</view>
</view>
<view class="sr-merchant-row" @tap="openMerchantPicker">
<view class="sr-merchant-label">门店筛选</view>
<view class="sr-merchant-right">
<text :class="form.merchantId ? 'sr-merchant-text' : 'sr-merchant-placeholder'">
{{ selectedMerchantText }}
</text>
<u-icon name="arrow-right" size="20" color="#9ca3af"/>
</view>
</view>
<view class="sr-merchant-row" @tap="openActivityTimePicker">
<view class="sr-merchant-label">活动日期</view>
<view class="sr-merchant-right">
<text :class="form.date ? 'sr-merchant-text' : 'sr-merchant-placeholder'">
{{ form.date || '请选择活动日期' }}
</text>
<u-icon name="calendar" size="20" color="#9ca3af"/>
</view>
</view>
<view class="card block">
<view class="section-title">上班打卡(水印拍照)</view>
<view class="clock-grid">
<view class="clock-card primary">
<view class="clock-thumb" @tap="handleClockAction(0)">
<image v-if="clockPhotos[0] && clockPhotos[0].url" :src="clockPhotos[0].url" mode="aspectFill" class="clock-image"/>
<view v-else class="clock-placeholder">
<u-icon name="photo" size="40" color="#52627a"/>
</view>
<view v-if="clockPhotos[0] && clockPhotos[0].url" class="clock-check">
<u-icon name="checkmark-circle-fill" size="22" color="#22c55e"/>
</view>
<view class="clock-info">
<view class="clock-time">{{ (clockPhotos[0] && clockPhotos[0].time) || '--:--' }}</view>
<view class="clock-date">{{ (clockPhotos[0] && clockPhotos[0].date) || '--' }}</view>
</view>
</view>
</view>
</view>
</view>
<view class="card block">
<view class="section-title-row">
<view class="section-title">门头摆台照</view>
<view class="section-extra">最多{{ boothLimit }}张</view>
</view>
<view class="photo-grid">
<view v-for="(item, idx) in boothPhotos" :key="`booth-${idx}`" class="photo-item">
<image :src="item" mode="aspectFill" class="photo-image" @tap="previewImages(boothPhotos, item)"/>
<view class="photo-remove" @tap="removeBooth(idx)">x</view>
</view>
<view v-if="boothPhotos.length < boothLimit" class="photo-item add" @tap="uploadBooth">
<u-icon name="photo" size="32" color="#c6ccd8"/>
</view>
</view>
</view>
<view class="card block">
<view class="section-title-row">
<view class="section-title">销量填写</view>
<view class="section-extra">已添加{{ totalSalesItemCount }}项</view>
</view>
<view class="sales-actions">
<view class="sales-add-btn" @tap="openProductPicker">
<u-icon name="plus" size="18" color="#ffffff"/>
<text>新增商品</text>
</view>
</view>
<view v-if="salesItems.length === 0" class="audit-empty-tip">请点击“新增商品”逐个添加销量商品</view>
<view v-else class="selected-sales-list">
<view class="selected-sales-item" v-for="(item, idx) in salesItems" :key="item._key">
<view class="selected-sales-head">
<view>
<view class="selected-sales-name">{{ item.name }}</view>
<view class="selected-sales-group">{{ item.groupTitle }}</view>
</view>
<view class="selected-sales-remove" @tap="removeSalesItem(idx)">删除</view>
</view>
<view class="selected-sales-meta">
<view class="selected-sales-meta-item" v-if="item.makerAbb">
<text class="meta-label">厂家</text>
<text class="meta-value">{{ item.makerAbb }}</text>
</view>
<view class="selected-sales-meta-item">
<text class="meta-label">编码</text>
<text class="meta-value">{{ item.goodsCode || '-' }}</text>
</view>
</view>
<view class="sales-controls">
<view class="spec-wrap">
<picker class="spec-picker" mode="selector" :range="item.specOptions" @change="onSpecChange($event, item)">
<view class="spec-pill">
<text>{{ item.spec || '规格' }}</text>
</view>
</picker>
<!-- <view v-if="hasTaskIncome(item)" class="sales-income">奖金金额:¥{{ formatTaskIncome(item) }}</view>-->
</view>
<view class="qty-box">
<input v-model="item.qty" class="qty-input" type="number" placeholder="0"/>
<text class="qty-unit">{{ item.unit || '盒' }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- <view class="card block reward-summary-card">-->
<!-- <text class="reward-summary-label">奖金合计</text>-->
<!-- <text class="reward-summary-value">{{ totalRewardDisplay }}</text>-->
<!-- </view>-->
<view class="card block">
<view class="section-title-row">
<view class="section-title tight">
小票上传
<text class="section-sub">(必须原图)</text>
</view>
<view class="section-extra">最多{{ receiptLimit }}张</view>
</view>
<view class="photo-grid">
<view v-for="(item, idx) in receiptPhotos" :key="`receipt-${idx}`" class="photo-item">
<image :src="item" mode="aspectFill" class="photo-image" @tap="previewImages(receiptPhotos, item)"/>
<view class="photo-remove" @tap="removeReceipt(idx)">x</view>
</view>
<view v-if="receiptPhotos.length < receiptLimit" class="photo-item add" @tap="uploadReceipt">
<u-icon name="arrow-upward" size="30" color="#c6ccd8"/>
</view>
</view>
</view>
<view class="card block">
<view class="section-title">活动小结</view>
<textarea v-model="form.summary" class="summary-input" placeholder="请填写活动情况"/>
</view>
<view class="card block">
<view class="section-title">下班打卡(水印拍照)</view>
<view class="clock-grid">
<view class="clock-card warm">
<view class="clock-thumb" @tap="handleClockAction(1)">
<image v-if="clockPhotos[1] && clockPhotos[1].url" :src="clockPhotos[1].url" mode="aspectFill" class="clock-image"/>
<view v-else class="clock-placeholder">
<u-icon name="photo" size="40" color="#52627a"/>
</view>
<view v-if="clockPhotos[1] && clockPhotos[1].url" class="clock-check">
<u-icon name="checkmark-circle-fill" size="22" color="#22c55e"/>
</view>
<view class="clock-info">
<view class="clock-time">{{ (clockPhotos[1] && clockPhotos[1].time) || '--:--' }}</view>
<view class="clock-date">{{ (clockPhotos[1] && clockPhotos[1].date) || '--' }}</view>
</view>
</view>
</view>
</view>
</view>
<view class="card block">
<view class="section-title-row">
<view class="section-title warm">门店评分</view>
<view class="section-extra">点击星星评分</view>
</view>
<view class="rating-row">
<u-rate
:count="5"
:size="34"
active-color="#ff8a4c"
inactive-color="#e4e8ef"
:value="form.rating"
@input="form.rating = $event"
/>
</view>
</view>
<view class="form-actions">
<view class="draft-btn" @tap="saveDraft">保存草稿</view>
<view class="primary-btn submit" @tap="submit">{{ submitLoading ? '提交中...' : '提交销量' }}</view>
</view>
<view class="form-actions-placeholder"/>
<u-popup :show="showChainPicker" mode="bottom" round="24rpx" @close="closeChainPicker">
<view class="merchant-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeChainPicker">取消</view>
<text class="picker-title">选择连锁</text>
<view class="picker-action primary" @tap="confirmChainPicker">确定</view>
</view>
<view v-if="merchantLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<picker-view
v-else
class="merchant-picker-view"
:value="chainPickerValue"
:indicator-style="merchantIndicatorStyle"
@change="onChainPickerChange"
>
<picker-view-column>
<view v-for="(item, idx) in merchantChainOptions" :key="`chain-${idx}`" class="merchant-picker-item">
{{ item.text }}
</view>
</picker-view-column>
</picker-view>
</view>
</u-popup>
<u-popup :show="showMerchantPicker" mode="bottom" round="24rpx" @close="closeMerchantPicker">
<view class="merchant-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeMerchantPicker">取消</view>
<text class="picker-title">选择门店</text>
<view class="picker-action primary" @tap="confirmMerchantPicker">确定</view>
</view>
<view class="merchant-picker-search">
<view class="merchant-search-item">
<u-input v-model="merchantStoreSearchKeyword" placeholder="搜索门店" shape="circle" :custom-style="{border:'3px solid #ff8a4c !important'}"/>
</view>
</view>
<view v-if="merchantLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<picker-view
v-else
class="merchant-picker-view"
:value="merchantPickerValue"
:indicator-style="merchantIndicatorStyle"
@change="onMerchantPickerChange"
>
<picker-view-column>
<view v-for="(item, idx) in merchantStoreOptions" :key="`merchant-${idx}`" class="merchant-picker-item">
{{ item.text }}
</view>
</picker-view-column>
</picker-view>
</view>
</u-popup>
<u-popup :show="showProductPicker" mode="bottom" round="24rpx" @close="closeProductPicker">
<view class="product-picker">
<view class="merchant-picker-head">
<view class="picker-action" @tap="closeProductPicker">取消</view>
<text class="picker-title">选择商品</text>
<view class="picker-action primary" @tap="closeProductPicker">完成</view>
</view>
<view class="merchant-picker-search">
<u-input v-model="salesKeyword" placeholder="搜索商品名称/厂家/规格" shape="circle" :custom-style="{border:'3px solid #ff8a4c !important'}"/>
</view>
<view v-if="productLoading" class="merchant-picker-loading">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<view v-else class="product-panel">
<scroll-view class="category-sidebar" scroll-y>
<view
v-for="group in filteredProductGroups"
:key="group.key"
class="category-item"
:class="currentProductGroupKey === group.key ? 'category-item-active' : ''"
@tap="selectProductGroup(group.key)"
>
{{ group.title }}
</view>
</scroll-view>
<scroll-view class="product-panel-main" scroll-y>
<view v-if="filteredProductGroups.length === 0" class="audit-empty-tip">暂无可选商品</view>
<view v-else-if="displayedProductOptions.length === 0" class="audit-empty-tip">该分类暂无可选商品</view>
<view v-else class="product-picker-list">
<view
class="product-picker-item"
v-for="item in displayedProductOptions"
:key="item._key"
@tap="selectProduct(item)"
>
<image
v-if="item.cover"
:src="item.cover"
mode="aspectFill"
class="product-picker-cover"
/>
<view class="product-picker-main">
<view class="product-picker-name">{{ item.name }}</view>
<view class="product-picker-sub">{{ item.makerAbb || '-' }} / {{ item.spec || '规格' }}</view>
<!-- <view v-if="hasTaskIncome(item)" class="product-picker-income">奖金金额:¥{{ formatTaskIncome(item) }}</view>-->
</view>
<view class="product-picker-tag">{{ isProductSelected(item) ? '已添加' : '添加' }}</view>
</view>
</view>
</scroll-view>
</view>
</view>
</u-popup>
<u-datetime-picker
:show="showActivityTimePicker"
:value="activityTimePickerValue"
mode="date"
@confirm="onActivityTimeConfirm"
@cancel="closeActivityTimePicker"
@close="closeActivityTimePicker"
/>
<Login ref="Login"/>
</view>
</template>
<script>
import dayjs from "dayjs";
import {Base64} from "js-base64";
import Login from "@/components/Login.vue";
import AppNavbar from "@/components/AppNavbar.vue";
import {merchantGroupByChainReq} from "@/api/shop";
import {productGroupByCateReq} from "@/api/product";
import {selfReportAddReq} from "@/api/self-report";
import {getUserInfo} from "@/util/user";
import {chooseImg, getLocationOnly} from "@/util";
export default {
components: {Login, AppNavbar},
data() {
return {
isLoggedIn: false,
hasInitialized: false,
merchantLoading: false,
productLoading: false,
showChainPicker: false,
showMerchantPicker: false,
showActivityTimePicker: false,
merchantStoreSearchKeyword: '',
chainPickerValue: [0],
merchantPickerValue: [0],
merchantChainList: [],
form: this.buildForm(),
clockPhotos: [
{key: 'in', label: '上班', time: '', date: '', url: '', lat: '', lng: '', location: ''},
{key: 'out', label: '下班', time: '', date: '', url: '', lat: '', lng: '', location: ''}
],
boothPhotos: [],
boothLimit: 6,
receiptPhotos: [],
receiptLimit: 9,
productGroups: [],
productLoadedChainName: '',
salesItems: [],
salesKeyword: '',
showProductPicker: false,
activeProductGroupKey: '',
currentAddress: '',
lat: null,
lng: null,
currentProvince: '',
currentCity: '',
draftPrompting: false,
submitLoading: false
}
},
computed: {
selectedChainText() {
if (!this.form.chainId) return this.merchantLoading ? '连锁加载中...' : '请选择连锁'
return this.form.chainName || '请选择连锁'
},
selectedMerchantText() {
if (!this.form.chainId) return '请先选择连锁'
if (!this.form.merchantId) return '请选择门店'
return this.form.merchantName || '请选择门店'
},
currentAddressText() {
return this.currentAddress || '暂未获取定位'
},
activityTimePickerValue() {
const value = this.form && this.form.date
const parsed = dayjs(value)
return parsed.isValid() ? parsed.valueOf() : Date.now()
},
merchantChainOptions() {
const list = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
return list.length ? list : [{id: '', text: '暂无连锁', merchants: []}]
},
merchantStoreOptions() {
const currentChain = this.merchantChainOptions.find(chain => String(chain.id) === String(this.form.chainId)) || {}
const merchants = Array.isArray(currentChain.merchants) ? currentChain.merchants : []
const keyword = (this.merchantStoreSearchKeyword || '').trim()
const filteredMerchants = this.filterMerchantStores(merchants)
return filteredMerchants.length ? filteredMerchants : [{id: '', text: keyword ? '无匹配门店' : '无门店'}]
},
merchantIndicatorStyle() {
return 'height: 64px;'
},
filteredProductGroups() {
const keyword = (this.salesKeyword || '').trim().toLowerCase()
const groups = Array.isArray(this.productGroups) ? this.productGroups : []
if (!keyword) return groups
return groups
.map(group => {
const items = Array.isArray(group.items) ? group.items : []
const nextItems = items.filter(item => {
const name = String(item.name || '').toLowerCase()
const maker = String(item.makerAbb || '').toLowerCase()
const spec = String(item.spec || '').toLowerCase()
return name.includes(keyword) || maker.includes(keyword) || spec.includes(keyword)
})
return Object.assign({}, group, {items: nextItems})
})
.filter(group => Array.isArray(group.items) && group.items.length)
},
currentProductGroupKey() {
const groups = Array.isArray(this.filteredProductGroups) ? this.filteredProductGroups : []
if (!groups.length) return ''
const hasActive = groups.some(group => group.key === this.activeProductGroupKey)
return hasActive ? this.activeProductGroupKey : groups[0].key
},
displayedProductOptions() {
const groups = Array.isArray(this.filteredProductGroups) ? this.filteredProductGroups : []
const current = groups.find(group => group.key === this.currentProductGroupKey)
return current && Array.isArray(current.items) ? current.items : []
},
totalSalesItemCount() {
return Array.isArray(this.salesItems) ? this.salesItems.length : 0
},
totalRewardAmount() {
return (Array.isArray(this.salesItems) ? this.salesItems : []).reduce((sum, item) => {
return sum + (this.parseSalesQty(item && item.qty) * this.getTaskIncomeNumber(item))
}, 0)
},
totalRewardDisplay() {
return `¥${this.formatMoney(this.totalRewardAmount)}`
}
},
watch: {
merchantStoreSearchKeyword() {
this.merchantPickerValue = [0]
}
},
async onShow() {
this.isLoggedIn = !!getUserInfo().token
if (!this.hasInitialized) {
await this.init()
this.hasInitialized = true
await this.checkDraftOnEntry()
}
},
methods: {
buildForm() {
return {
chainId: '',
chainName: '',
date: '',
merchantId: '',
merchantName: '',
province: '',
city: '',
lat: '',
lng: '',
summary: '',
rating: 5
}
},
async init() {
this.isLoggedIn = !!getUserInfo().token
this.form = this.buildForm()
this.clockPhotos = [
{key: 'in', label: '上班', time: '', date: '', url: '', lat: '', lng: '', location: ''},
{key: 'out', label: '下班', time: '', date: '', url: '', lat: '', lng: '', location: ''}
]
this.boothPhotos = []
this.receiptPhotos = []
this.productGroups = []
this.salesItems = []
this.salesKeyword = ''
this.showProductPicker = false
this.showChainPicker = false
this.activeProductGroupKey = ''
this.currentAddress = ''
this.lat = null
this.lng = null
this.currentProvince = ''
this.currentCity = ''
await this.getMerchantList()
await this.getProductGroups()
},
getDraftStorageKey() {
const user = getUserInfo() || {}
const uid = user.uid || user.id || user.userId || 'guest'
return `yxe_self_report_create_draft_${uid}`
},
cloneValue(value) {
if (value === undefined || value === null) {
return value
}
return JSON.parse(JSON.stringify(value))
},
normalizeClockPhotoItem(item, fallbackKey, fallbackLabel) {
if (!item || typeof item !== 'object') {
return {
key: fallbackKey,
label: fallbackLabel,
time: '',
date: '',
url: '',
lat: '',
lng: '',
location: ''
}
}
return {
key: item.key || fallbackKey,
label: item.label || fallbackLabel,
time: item.time || '',
date: item.date || '',
url: item.url || '',
lat: item.lat || '',
lng: item.lng || '',
location: item.location || ''
}
},
normalizeDraftClockPhotos(list) {
const source = Array.isArray(list) ? list : []
const inClock = this.normalizeClockPhotoItem(source[0], 'in', '上班')
const outClock = this.normalizeClockPhotoItem(source[1], 'out', '下班')
return [inClock, outClock]
},
hasDraftContent() {
const clockPhotos = Array.isArray(this.clockPhotos) ? this.clockPhotos : []
const hasClock = clockPhotos.some(item => item && item.url)
const hasBooth = Array.isArray(this.boothPhotos) && this.boothPhotos.length > 0
const hasReceipt = Array.isArray(this.receiptPhotos) && this.receiptPhotos.length > 0
const hasSales = Array.isArray(this.salesItems) && this.salesItems.length > 0
const hasSummary = String(this.form && this.form.summary ? this.form.summary : '').trim().length > 0
const hasMerchant = !!(this.form && (this.form.chainId || this.form.merchantId))
const hasActivityDate = !!(this.form && this.form.date)
const hasRating = Number(this.form && this.form.rating) !== 5
return hasClock || hasBooth || hasReceipt || hasSales || hasSummary || hasMerchant || hasActivityDate || hasRating
},
buildDraftPayload() {
return {
version: 1,
savedAt: Date.now(),
form: this.cloneValue(this.form),
clockPhotos: this.cloneValue(this.clockPhotos),
boothPhotos: this.cloneValue(this.boothPhotos),
receiptPhotos: this.cloneValue(this.receiptPhotos),
salesItems: this.cloneValue(this.salesItems),
currentAddress: this.currentAddress,
lat: this.lat,
lng: this.lng,
currentProvince: this.currentProvince,
currentCity: this.currentCity
}
},
async saveDraft() {
if (!this.hasDraftContent()) {
uni.removeStorageSync(this.getDraftStorageKey())
uni.showToast({
title: '请先填写内容',
icon: 'none'
})
return
}
uni.setStorageSync(this.getDraftStorageKey(), this.buildDraftPayload())
uni.showToast({
title: '已保存',
icon: 'success'
})
},
clearDraft() {
uni.removeStorageSync(this.getDraftStorageKey())
},
async checkDraftOnEntry() {
if (this.draftPrompting) return
const draft = uni.getStorageSync(this.getDraftStorageKey())
if (!draft || typeof draft !== 'object') return
const hasDraftContent = this.hasDraftPayloadContent(draft)
if (!hasDraftContent) {
this.clearDraft()
return
}
this.draftPrompting = true
try {
const confirm = await new Promise(resolve => {
uni.showModal({
title: '提示',
content: '您有保存数据,是否立即加载',
confirmText: '是',
cancelText: '否',
success: res => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
})
})
if (confirm) {
await this.restoreDraft(draft)
} else {
this.clearDraft()
}
} finally {
this.draftPrompting = false
}
},
hasDraftPayloadContent(draft) {
if (!draft || typeof draft !== 'object') {
return false
}
const form = draft.form || {}
const clockPhotos = Array.isArray(draft.clockPhotos) ? draft.clockPhotos : []
const boothPhotos = Array.isArray(draft.boothPhotos) ? draft.boothPhotos : []
const receiptPhotos = Array.isArray(draft.receiptPhotos) ? draft.receiptPhotos : []
const salesItems = Array.isArray(draft.salesItems) ? draft.salesItems : []
return !!(
String(form.chainId || '').trim() ||
String(form.merchantId || '').trim() ||
String(form.date || form.activityTime || '').trim() ||
clockPhotos.some(item => item && item.url) ||
boothPhotos.length ||
receiptPhotos.length ||
salesItems.length ||
String(form.summary || '').trim() ||
Number(form.rating) !== 5
)
},
async restoreDraft(draft) {
const nextForm = Object.assign(this.buildForm(), this.cloneValue(draft.form || {}))
if (!nextForm.date && nextForm.activityTime) {
nextForm.date = nextForm.activityTime
}
delete nextForm.activityTime
if (!nextForm.chainId && nextForm.chainName) {
const matchedChain = this.merchantChainList.find(item => String(item.text) === String(nextForm.chainName))
if (matchedChain) nextForm.chainId = matchedChain.id
}
this.form = nextForm
this.clockPhotos = this.normalizeDraftClockPhotos(draft.clockPhotos)
this.boothPhotos = Array.isArray(draft.boothPhotos) ? [...draft.boothPhotos] : []
this.receiptPhotos = Array.isArray(draft.receiptPhotos) ? [...draft.receiptPhotos] : []
this.salesItems = Array.isArray(draft.salesItems)
? this.cloneValue(draft.salesItems).map(item => Object.assign({}, item))
: []
this.currentAddress = draft.currentAddress || ''
this.lat = draft.lat
this.lng = draft.lng
this.currentProvince = draft.currentProvince || ''
this.currentCity = draft.currentCity || ''
this.salesKeyword = ''
this.showProductPicker = false
this.showChainPicker = false
this.showMerchantPicker = false
if (this.form.chainName) {
await this.getProductGroups()
}
},
async fetchCurrentLocation() {
const location = await getLocationOnly().catch(() => null)
if (!location) return
this.syncLocation(location)
},
syncLocation(location) {
this.lat = Number(location.latitude)
this.lng = Number(location.longitude)
this.currentProvince = location.province_str || ''
this.currentCity = location.city_str || ''
this.currentAddress = location.full || [location.province_str, location.city_str, location.area_str].filter(Boolean).join('')
},
normalizeMerchant(item) {
const id = item.id ?? item.merchantId ?? item.merchant_id
const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
const lat = Number(item.lat ?? item.latitude ?? item.merchantLat ?? item.merchant_lat)
const lng = Number(item.lng ?? item.longitude ?? item.merchantLng ?? item.merchant_lng)
const address = [item.province, item.city, item.region, item.area, item.address].filter(Boolean).join('')
return {
id,
text,
chainId: item.chainId ?? item.chain_id ?? '',
chainName: item.chainName || item.chain || item.chainAbb || '',
lat: Number.isFinite(lat) ? lat : null,
lng: Number.isFinite(lng) ? lng : null,
province: item.province || item.province_str || '',
city: item.city || item.city_str || '',
address
}
},
normalizeChainIdForApi(rawChainId) {
const text = String(rawChainId || '').trim()
if (!text || text.startsWith('chain_')) return ''
return text
},
pickFirstText(source, keys = []) {
if (!source || typeof source !== 'object') return ''
for (let i = 0; i < keys.length; i += 1) {
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 += 1) {
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]
},
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 ?? chainName
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 += 1) {
const list = item[childCandidates[i]]
if (Array.isArray(list) && list.length) {
rawChildren = list
break
}
}
let merchants = rawChildren
.map(child => this.normalizeMerchant(child))
.filter(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.id).trim() !== '')
.map(merchant => Object.assign({}, merchant, {chainId, chainName}))
if (!merchants.length) {
const selfMerchant = this.normalizeMerchant(item)
if (selfMerchant.id !== undefined && selfMerchant.id !== null && String(selfMerchant.id).trim() !== '') {
merchants = [Object.assign({}, selfMerchant, {chainId, chainName})]
}
}
return {
id: chainId,
text: chainName,
merchants
}
},
groupMerchantListByChain(list) {
const chainMap = {}
;(Array.isArray(list) ? list : []).forEach((item, index) => {
const merchant = this.normalizeMerchant(item)
if (merchant.id === undefined || merchant.id === null || String(merchant.id).trim() === '') return
const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'chain_name'])
if (!chainName) return
const rawChainId = item.chainId ?? item.chain_id ?? item.chainCode ?? item.chain_code ?? chainName
const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
? `chain_${index}`
: rawChainId
const key = String(chainId)
if (!chainMap[key]) {
chainMap[key] = {id: chainId, text: chainName, merchants: []}
}
chainMap[key].merchants.push(Object.assign({}, merchant, {chainId, chainName}))
})
return Object.keys(chainMap).map(key => chainMap[key])
},
flattenMerchantsFromChains(chainList) {
const list = []
;(Array.isArray(chainList) ? chainList : []).forEach(chain => {
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
merchants.forEach(merchant => {
if (!merchant || merchant.id === '' || merchant.id === null || merchant.id === undefined) return
list.push(Object.assign({}, merchant, {
chainId: merchant.chainId || chain.id || '',
chainName: merchant.chainName || chain.text || ''
}))
})
})
return list
},
filterMerchantStores(merchants) {
const list = Array.isArray(merchants) ? merchants : []
const keyword = (this.merchantStoreSearchKeyword || '').trim()
if (!keyword) return list
return list.filter(merchant => (merchant.text || '').includes(keyword))
},
applyMerchant(chain, merchant) {
if (!merchant) return
this.form.chainName = (chain && chain.text) || merchant.chainName || ''
this.form.chainId = (chain && chain.id) ?? merchant.chainId ?? ''
this.form.merchantName = merchant.text || ''
this.form.merchantId = merchant.id ?? ''
this.form.province = merchant.province || this.currentProvince || ''
this.form.city = merchant.city || this.currentCity || ''
this.form.lat = Number.isFinite(merchant.lat) ? merchant.lat : ''
this.form.lng = Number.isFinite(merchant.lng) ? merchant.lng : ''
},
async getMerchantList() {
this.merchantLoading = true
try {
const {data} = await merchantGroupByChainReq({})
const list = this.unwrapList(data)
const groupedList = this.groupMerchantListByChain(list)
this.merchantChainList = (groupedList.length
? groupedList
: list.map((item, index) => this.normalizeMerchantChain(item, index)))
.filter(item => Array.isArray(item.merchants) && item.merchants.length > 0)
} catch (error) {
this.merchantChainList = []
uni.showToast({
title: '门店列表获取失败',
icon: 'none'
})
} finally {
this.merchantLoading = false
}
},
openChainPicker() {
if (!this.merchantChainList.length) return
const chainIndex = this.merchantChainOptions.findIndex(item => String(item.id) === String(this.form.chainId))
this.chainPickerValue = [chainIndex > -1 ? chainIndex : 0]
this.showChainPicker = true
},
closeChainPicker() {
this.showChainPicker = false
},
onChainPickerChange(e) {
let chainIndex = Array.isArray(e.detail.value) ? Number(e.detail.value[0] || 0) : 0
if (!Number.isFinite(chainIndex) || chainIndex < 0 || chainIndex > this.merchantChainOptions.length - 1) {
chainIndex = 0
}
this.chainPickerValue = [chainIndex]
},
async confirmChainPicker() {
const chain = this.merchantChainOptions[this.chainPickerValue[0]] || null
if (!chain || !chain.id) {
uni.showToast({title: '请选择连锁', icon: 'none'})
return
}
const changed = String(this.form.chainId || '') !== String(chain.id)
this.form.chainId = chain.id
this.form.chainName = chain.text || ''
if (changed) {
this.form.merchantId = ''
this.form.merchantName = ''
this.form.province = ''
this.form.city = ''
this.form.lat = ''
this.form.lng = ''
this.merchantStoreSearchKeyword = ''
this.merchantPickerValue = [0]
this.salesItems = []
this.productGroups = []
this.productLoadedChainName = ''
this.activeProductGroupKey = ''
this.salesKeyword = ''
}
this.closeChainPicker()
if (changed) await this.getProductGroups()
},
openMerchantPicker() {
if (!this.form.chainId) {
uni.showToast({title: '请先选择连锁', icon: 'none'})
return
}
if (!this.merchantChainList.length) return
this.merchantStoreSearchKeyword = ''
const merchants = this.merchantStoreOptions
const merchantIndex = merchants.findIndex(item => String(item.id) === String(this.form.merchantId))
this.merchantPickerValue = [merchantIndex > -1 ? merchantIndex : 0]
this.showMerchantPicker = true
},
closeMerchantPicker() {
this.showMerchantPicker = false
},
openActivityTimePicker() {
this.showActivityTimePicker = true
},
closeActivityTimePicker() {
this.showActivityTimePicker = false
},
onActivityTimeConfirm(e) {
this.form.date = dayjs(e.value).format('YYYY-MM-DD')
this.closeActivityTimePicker()
},
onMerchantPickerChange(e) {
let merchantIndex = Array.isArray(e.detail.value) ? Number(e.detail.value[0] || 0) : 0
if (!Number.isFinite(merchantIndex) || merchantIndex < 0) merchantIndex = 0
const merchants = this.merchantStoreOptions
if (!merchants.length || !merchants[0].id) {
merchantIndex = 0
} else if (merchantIndex > merchants.length - 1) {
merchantIndex = 0
}
this.merchantPickerValue = [merchantIndex]
},
async confirmMerchantPicker() {
const chain = this.merchantChainOptions.find(item => String(item.id) === String(this.form.chainId)) || null
const selected = this.merchantStoreOptions[this.merchantPickerValue[0]] || null
if (!selected || !selected.id) {
uni.showToast({
title: '请选择门店',
icon: 'none'
})
return
}
const prevChainName = String(this.form.chainName || '').trim()
this.applyMerchant(chain, selected)
const nextChainName = String(this.form.chainName || '').trim()
this.salesKeyword = ''
this.productGroups = []
this.productLoadedChainName = ''
this.activeProductGroupKey = ''
this.closeMerchantPicker()
if (nextChainName && nextChainName !== prevChainName) {
await this.getProductGroups()
}
},
isProductNode(node) {
if (!node || typeof node !== 'object') return false
const product = node.product && typeof node.product === 'object' ? node.product : {}
const hasChildrenOnly = Array.isArray(node.children)
&& !this.pickFirstText(node, ['goodsName', 'goods_name', 'goodsCode', 'code', 'productCode', 'batchCode'])
&& !node.product
if (hasChildrenOnly) return false
return !!(
this.pickFirstText(node, ['goodsName', 'goods_name', 'goodsCode', 'code', 'productCode', 'batchCode']) ||
this.pickFirstText(product, ['goodsName', 'goods_name', 'goodsCode', 'code', 'productCode', 'batchCode'])
)
},
collectProductNodes(node, depth = 0) {
if (!node || depth > 4) return []
if (Array.isArray(node)) {
return node.reduce((acc, item) => acc.concat(this.collectProductNodes(item, depth + 1)), [])
}
if (typeof node !== 'object') return []
if (this.isProductNode(node)) return [node]
const keys = ['productList', 'products', 'list', 'records', 'rows', 'items', 'children', 'data']
return keys.reduce((acc, key) => {
const value = node[key]
if (Array.isArray(value) || (value && typeof value === 'object')) {
return acc.concat(this.collectProductNodes(value, depth + 1))
}
return acc
}, [])
},
buildCategoryTabs(rawData) {
const rootList = this.unwrapList(rawData)
const tabs = []
rootList.forEach((categoryNode, categoryIndex) => {
if (!categoryNode || typeof categoryNode !== 'object') return
const label = categoryNode.cate
const products = Array.isArray(categoryNode.children) ? categoryNode.children : []
if (!label || !products.length) return
tabs.push({
key: `cate_children_${categoryIndex}`,
label,
rawProducts: products
})
})
if (tabs.length) return tabs
const allProducts = this.collectProductNodes(rootList)
if (allProducts.length) {
tabs.push({
key: 'all',
label: '全部',
rawProducts: allProducts
})
}
return tabs
},
normalizeSpecOptions(item) {
const raw = item.specOptions || item.specList || item.specs || []
if (Array.isArray(raw)) {
return raw.map(value => `${value}`.trim()).filter(Boolean)
}
if (typeof raw === 'string') {
return raw.split(/[、,,]/).map(value => value.trim()).filter(Boolean)
}
const spec = item.spec || item.specification || item.standard || ''
return spec ? [spec] : []
},
normalizeProduct(item) {
const product = item && item.product ? item.product : {}
const code = item.goodsCode || item.code || item.productCode || item.batchCode || product.code || product.goodsCode || ''
const productId = item.productId || item.id || item.goodsId || item.skuId || product.productId || product.id || ''
const name = item.goodsName || item.goods_name || item.title || item.name || product.title || product.goodsName || product.name || ''
const maker = item.makerAbb || item.maker || item.manufacturer || product.makerAbb || product.maker || product.manufacturer || ''
const spec = product.spec || item.spec || item.specification || ''
const cover = item.cover || product.cover || ''
const chainId = item.chainId || item.chain_id || product.chainId || product.chain_id || ''
const chainName = item.chainName || item.chain_name || item.chain || product.chainName || product.chain_name || product.chain || ''
const merchantId = item.merchantId || item.merchant_id || product.merchantId || product.merchant_id || ''
const merchantName = item.merchantName || item.merchant_name || item.shopName || item.shop_name || product.merchantName || product.merchant_name || ''
return {
productId,
goodsCode: code,
name,
cover,
makerAbb: maker,
spec,
chainId,
chainName,
merchantId,
merchantName,
unit: item.unit || product.unit || '盒',
taskIncome: this.resolveTaskIncome(item),
specOptions: this.normalizeSpecOptions(Object.assign({}, product, item)),
qty: ''
}
},
hasDisplayValue(value) {
return value !== undefined && value !== null && String(value).trim() !== ''
},
resolveTaskIncome(item) {
if (!item || typeof item !== 'object') return ''
const product = item.product && typeof item.product === 'object' ? item.product : {}
const candidates = [
item.taskIncome
]
for (let i = 0; i < candidates.length; i += 1) {
const value = candidates[i]
if (this.hasDisplayValue(value)) return value
}
return ''
},
hasTaskIncome(item) {
return this.hasDisplayValue(this.resolveTaskIncome(item))
},
formatTaskIncome(item) {
const value = this.resolveTaskIncome(item)
if (!this.hasDisplayValue(value)) return ''
const num = Number(value)
if (!Number.isFinite(num)) return String(value)
return Number.isInteger(num) ? String(num) : num.toFixed(2)
},
parseSalesQty(value) {
const num = Number(value)
return Number.isFinite(num) ? num : 0
},
getTaskIncomeNumber(item) {
const num = Number(this.resolveTaskIncome(item))
return Number.isFinite(num) ? num : 0
},
formatMoney(value) {
const num = Number(value)
if (!Number.isFinite(num)) return '0.00'
return num.toFixed(2)
},
filterProductGroupsByChain(groups = [], rawChainId = '') {
const chainId = this.normalizeChainIdForApi(rawChainId)
if (!chainId) return Array.isArray(groups) ? groups : []
return (Array.isArray(groups) ? groups : [])
.map(group => Object.assign({}, group, {
items: (Array.isArray(group.items) ? group.items : []).filter(item => {
const productChainId = String(item.chainId || '').trim()
return !productChainId || productChainId === chainId
})
}))
.filter(group => Array.isArray(group.items) && group.items.length)
},
buildSalesGroupsFromProductData(rawData) {
const tabs = this.buildCategoryTabs(rawData)
let rowSeed = 0
const groups = tabs.map((tab, tabIndex) => {
const items = (Array.isArray(tab.rawProducts) ? tab.rawProducts : [])
.map((rawItem, itemIndex) => {
const product = this.normalizeProduct(rawItem)
if (!product.name) return null
const specOptions = Array.isArray(product.specOptions) && product.specOptions.length
? product.specOptions
: [product.spec || '规格']
return {
...product,
groupTitle: tab.label || `分类${tabIndex + 1}`,
spec: product.spec || specOptions[0] || '规格',
specOptions,
_key: `sales_${tabIndex}_${itemIndex}_${rowSeed++}`
}
})
.filter(Boolean)
return {
key: tab.key || `group_${tabIndex}`,
title: tab.label || `分类${tabIndex + 1}`,
items
}
}).filter(group => group.items.length)
return groups
},
async getProductGroups() {
const chainName = String(this.form.chainName || '').trim()
if (!chainName) {
this.productGroups = []
this.productLoadedChainName = ''
this.activeProductGroupKey = ''
return
}
this.productLoading = true
try {
const {data} = await productGroupByCateReq({chain: chainName, type: 0})
this.productGroups = this.buildSalesGroupsFromProductData(data)
this.productLoadedChainName = chainName
} catch (error) {
this.productGroups = []
this.productLoadedChainName = ''
uni.showToast({
title: '商品数据获取失败',
icon: 'none'
})
} finally {
this.activeProductGroupKey = this.productGroups[0] ? this.productGroups[0].key : ''
this.productLoading = false
}
},
async openProductPicker() {
if (!this.form.chainName || !this.form.merchantId) {
uni.showToast({
title: '请先选择连锁和门店',
icon: 'none'
})
return
}
const currentChainName = String(this.form.chainName || '').trim()
if (!currentChainName) {
uni.showToast({
title: '当前连锁无效',
icon: 'none'
})
return
}
if (this.productLoadedChainName !== currentChainName || !this.productGroups.length) {
await this.getProductGroups()
}
if (!this.filteredProductGroups.length) {
uni.showToast({
title: '当前连锁暂无可选商品',
icon: 'none'
})
return
}
if (!this.activeProductGroupKey) {
this.activeProductGroupKey = this.filteredProductGroups[0] ? this.filteredProductGroups[0].key : ''
}
this.showProductPicker = true
},
closeProductPicker() {
this.showProductPicker = false
},
selectProductGroup(key) {
this.activeProductGroupKey = key
},
isProductSelected(product) {
return (Array.isArray(this.salesItems) ? this.salesItems : []).some(item => {
if (product.productId && item.productId) {
return String(item.productId) === String(product.productId)
}
return item.name === product.name
})
},
selectProduct(product) {
if (this.isProductSelected(product)) {
uni.showToast({
title: '该商品已添加',
icon: 'none'
})
return
}
const specOptions = Array.isArray(product.specOptions) && product.specOptions.length
? [...product.specOptions]
: [product.spec || '规格']
this.salesItems.push({
...product,
groupTitle: product.groupTitle || '',
specOptions,
spec: product.spec || specOptions[0] || '规格',
qty: ''
})
this.showProductPicker = false
this.salesKeyword = ''
},
removeSalesItem(index) {
this.salesItems.splice(index, 1)
},
onSpecChange(event, item) {
const value = Number(event.detail.value || 0)
const nextSpec = item.specOptions[value] || item.spec
item.spec = nextSpec
},
async ensureLogin() {
if (this.isLoggedIn) return true
this.$refs.Login.open()
return false
},
async handleClockAction(index) {
const canContinue = await this.ensureLogin()
if (!canContinue) return
await this.uploadClock(index)
},
formatTime(date) {
const hour = `${date.getHours()}`.padStart(2, '0')
const minute = `${date.getMinutes()}`.padStart(2, '0')
return `${hour}:${minute}`
},
formatDate(date) {
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}.${month}.${day}`
},
makeTextBase64(text) {
const base64 = Base64.encode(text)
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
},
buildWatermarkImageUrl(path, address, date, time) {
let url = `${path}?x-oss-process=image/watermark,text_${this.makeTextBase64(address)},g_sw,color_FFFFFF,x_10,y_120`
url += `/watermark,text_${this.makeTextBase64(date)},g_sw,color_FFFFFF,x_10,y_60`
url += `/watermark,text_${this.makeTextBase64(time)},g_sw,color_FFFFFF,x_10,y_10`
return url
},
async uploadClock(index) {
uni.showLoading({title: '上传中'})
try {
const res = await chooseImg(1, ['camera']).catch(() => [])
const next = res[0] && res[0].path ? res[0].path : ''
if (!next) return
const location = await getLocationOnly()
if (!location) return
this.syncLocation(location)
const now = new Date()
const time = this.formatTime(now)
const date = this.formatDate(now)
const address = `${location.province_str}${location.city_str}${location.area_str}`
const url = this.buildWatermarkImageUrl(next, address, date, time)
this.$set(this.clockPhotos, index, Object.assign({}, this.clockPhotos[index], {
url,
time,
date,
lat: location.latitude,
lng: location.longitude,
location: location.full || [location.province_str, location.city_str, location.area_str].filter(Boolean).join('')
}))
} finally {
uni.hideLoading()
}
},
previewImages(urls = [], current = '') {
const list = Array.isArray(urls) ? urls.filter(Boolean) : []
if (!list.length) return
uni.previewImage({
urls: list,
current: current || list[0]
})
},
async uploadBooth() {
const canContinue = await this.ensureLogin()
if (!canContinue) return
const remain = this.boothLimit - this.boothPhotos.length
if (remain <= 0) return
const res = await chooseImg(remain)
this.boothPhotos.push(...res.map(item => item.path).filter(Boolean))
},
removeBooth(index) {
this.boothPhotos.splice(index, 1)
},
async uploadReceipt() {
const canContinue = await this.ensureLogin()
if (!canContinue) return
const remain = this.receiptLimit - this.receiptPhotos.length
if (remain <= 0) return
const res = await chooseImg(remain)
this.receiptPhotos.push(...res.map(item => item.path).filter(Boolean))
},
removeReceipt(index) {
this.receiptPhotos.splice(index, 1)
},
toast(message, title = '提示') {
return new Promise(resolve => {
uni.showModal({
title,
content: String(message || ''),
showCancel: false,
success: resolve,
fail: resolve
})
})
},
fillEmptySalesQtyWithZero() {
const items = Array.isArray(this.salesItems) ? this.salesItems : []
items.forEach(item => {
const qty = item.qty
const emptyQty = qty === '' || qty === null || qty === undefined || (typeof qty === 'string' && qty.trim() === '')
if (emptyQty) item.qty = 0
})
},
buildSubmitSalesGroups() {
const groupsMap = {}
;(Array.isArray(this.salesItems) ? this.salesItems : []).forEach(item => {
const groupTitle = item.groupTitle || '自定义商品'
if (!groupsMap[groupTitle]) {
groupsMap[groupTitle] = {
title: groupTitle,
items: []
}
}
groupsMap[groupTitle].items.push({
productId: item.productId || '',
goodsCode: item.goodsCode || '',
name: item.name || '',
makerAbb: item.makerAbb || '',
spec: item.spec || '',
taskIncome: this.resolveTaskIncome(item),
qty: item.qty,
unit: item.unit || '盒',
specOptions: Array.isArray(item.specOptions) ? item.specOptions : []
})
})
return Object.keys(groupsMap).map(key => groupsMap[key])
},
validateSubmit() {
if (!this.form.chainId) {
this.toast('请选择连锁')
return false
}
if (!this.form.merchantId) {
this.toast('请选择门店')
return false
}
if (!this.form.date) {
this.toast('请选择活动日期')
return false
}
if (!this.currentAddress || !this.lat || !this.lng) {
this.toast('请先完成定位')
return false
}
if (!this.clockPhotos[0] || !this.clockPhotos[0].url) {
this.toast('请完成上班打卡')
return false
}
if (!this.clockPhotos[1] || !this.clockPhotos[1].url) {
this.toast('请完成下班打卡')
return false
}
if (!this.receiptPhotos.length) {
this.toast('请上传小票原图')
return false
}
const items = Array.isArray(this.salesItems) ? this.salesItems : []
if (!items.length) {
this.toast('请至少添加一项销量商品')
return false
}
for (let i = 0; i < items.length; i += 1) {
const item = items[i]
const qty = item.qty
const emptyQty = qty === '' || qty === null || qty === undefined || (typeof qty === 'string' && qty.trim() === '')
if (emptyQty) {
this.toast(`请填写${item.name || '商品'}销量`)
return false
}
if (Number(qty) < 0) {
this.toast(`${item.name || '商品'}销量不能小于0`)
return false
}
}
const summaryText = (this.form.summary || '').trim()
if (!summaryText) {
this.toast('请填写活动小结')
return false
}
return true
},
buildPayload() {
const summaryText = (this.form.summary || '').trim()
const imageList = [
...this.clockPhotos.map(item => item && item.url).filter(Boolean),
...this.boothPhotos,
...this.receiptPhotos
]
return {
merchantId: this.form.merchantId,
merchantName: this.form.merchantName,
chainId: this.form.chainId,
chainName: this.form.chainName,
date: this.form.date,
province: this.form.province || this.currentProvince || '',
city: this.form.city || this.currentCity || '',
lat: this.lat,
lng: this.lng,
location: this.currentAddress,
image: imageList.length ? JSON.stringify(imageList) : null,
clockPhotos: JSON.stringify(this.clockPhotos || []),
boothPhotos: JSON.stringify(this.boothPhotos || []),
salesGroups: JSON.stringify(this.buildSubmitSalesGroups()),
receiptPhoto: JSON.stringify(this.receiptPhotos || []),
summary: JSON.stringify(summaryText),
reward: this.totalRewardAmount,
rating: this.form.rating,
status: 0
}
},
async submit() {
const canContinue = await this.ensureLogin()
if (!canContinue || this.submitLoading) return
this.fillEmptySalesQtyWithZero()
if (!this.validateSubmit()) return
this.submitLoading = true
try {
await selfReportAddReq(this.buildPayload())
this.clearDraft()
await this.toast('提交成功,请等待审核')
uni.navigateBack({
fail: () => {
uni.switchTab({url: '/pages/mine/index'})
}
})
} catch (error) {
await this.toast(error && error.message ? error.message : '提交失败')
} finally {
this.submitLoading = false
}
}
}
}
</script>
<style lang="scss" scoped>
.self-report-page {
--form-actions-height: calc(118rpx + env(safe-area-inset-bottom));
min-height: 100vh;
background: #f5f5f5;
}
.sr-merchant-row {
margin: 20rpx 20rpx 0;
padding: 16rpx 18rpx;
border: 3px solid #ff8a4c;
border-radius: 16rpx;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
}
.sr-merchant-label {
color: grey;
font-size: 26rpx;
min-width: 140rpx;
}
.sr-merchant-right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8rpx;
min-height: 64rpx;
}
.sr-merchant-text {
display: block;
color: #111827;
font-size: 26rpx;
font-weight: bold;
line-height: 1.35;
text-align: right;
white-space: normal;
word-break: break-all;
max-width: 430rpx;
}
.sr-merchant-placeholder {
color: #9ca3af;
font-size: 26rpx;
}
.store-card {
margin: 24rpx 20rpx 0;
padding: 28rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #2fc6a7, #1db896);
color: #ffffff;
box-shadow: 0 18rpx 36rpx rgba(47, 198, 167, 0.25);
}
.store-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 24rpx;
}
.store-title {
font-size: 36rpx;
font-weight: 700;
}
.store-desc {
margin-top: 10rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
}
.store-tag {
padding: 10rpx 18rpx;
border-radius: 999rpx;
font-size: 24rpx;
background: rgba(255, 255, 255, 0.16);
}
.store-field {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
padding: 18rpx 0;
border-top: 1rpx solid rgba(255, 255, 255, 0.16);
}
.store-label {
width: 140rpx;
color: rgba(255, 255, 255, 0.72);
font-size: 26rpx;
}
.store-value {
flex: 1;
text-align: right;
font-size: 28rpx;
font-weight: 600;
}
.store-value.placeholder {
color: rgba(255, 255, 255, 0.65);
}
.store-value.address {
font-size: 24rpx;
line-height: 1.5;
}
.block {
margin: 20rpx;
}
.section-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 20rpx;
}
.section-extra {
color: #7a8495;
font-size: 24rpx;
}
.section-title.tight {
margin-bottom: 0;
}
.section-sub {
margin-left: 8rpx;
font-size: 24rpx;
color: #7a8495;
}
.clock-grid {
display: flex;
gap: 18rpx;
}
.clock-card {
flex: 1;
}
.clock-card.primary .clock-thumb {
border-color: #4c7bff;
}
.clock-card.warm .clock-thumb {
border-color: #f3a45c;
}
.clock-thumb {
position: relative;
height: 220rpx;
border-radius: 20rpx;
overflow: hidden;
background: linear-gradient(180deg, #1f2c3c, #151e29);
border: 2rpx solid #2b3950;
}
.clock-image {
width: 100%;
height: 100%;
}
.clock-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.25);
}
.clock-check {
position: absolute;
top: 10rpx;
right: 10rpx;
background: #ffffff;
border-radius: 50%;
padding: 2rpx;
}
.clock-info {
position: absolute;
left: 16rpx;
bottom: 16rpx;
color: #ffffff;
}
.clock-time {
font-size: 34rpx;
font-weight: 600;
}
.clock-date {
font-size: 22rpx;
opacity: 0.9;
margin-top: 4rpx;
}
.photo-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.photo-item {
width: calc((100% - 32rpx) / 3);
height: 200rpx;
position: relative;
border-radius: 18rpx;
overflow: hidden;
background: #f8fafc;
border: 1rpx solid #e2e8f0;
}
.photo-item.add {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #fafafa, #f3f4f6);
}
.photo-image {
width: 100%;
height: 100%;
}
.photo-remove {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: rgba(15, 23, 42, 0.65);
color: #ffffff;
font-size: 22rpx;
display: flex;
align-items: center;
justify-content: center;
}
.sales-search {
margin-bottom: 18rpx;
}
.sales-actions {
margin-bottom: 18rpx;
}
.sales-add-btn {
width: 240rpx;
min-height: 76rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #ff8a4c, #ff6b4a);
box-shadow: 0 12rpx 24rpx rgba(255, 107, 74, 0.2);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10rpx;
color: #ffffff;
font-size: 26rpx;
font-weight: 700;
letter-spacing: 1rpx;
}
.selected-sales-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.selected-sales-item {
padding: 22rpx;
border-radius: 22rpx;
background: linear-gradient(180deg, #fffaf5, #ffffff);
border: 1rpx solid #fde7d8;
}
.selected-sales-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
}
.selected-sales-name {
font-size: 30rpx;
font-weight: 700;
color: #0f172a;
}
.selected-sales-group {
margin-top: 8rpx;
font-size: 22rpx;
color: #c2410c;
}
.selected-sales-remove {
color: #ef4444;
font-size: 24rpx;
}
.selected-sales-meta {
margin-top: 16rpx;
display: flex;
flex-wrap: wrap;
gap: 14rpx;
}
.selected-sales-meta-item {
min-width: 220rpx;
padding: 12rpx 16rpx;
border-radius: 16rpx;
background: #fff7ed;
}
.meta-label {
color: #9a3412;
font-size: 22rpx;
}
.meta-value {
display: block;
margin-top: 4rpx;
color: #7c2d12;
font-size: 24rpx;
}
.sales-controls {
margin-top: 16rpx;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18rpx;
}
.spec-wrap {
flex: 1;
min-width: 0;
}
.spec-picker {
display: block;
}
.spec-pill {
min-height: 72rpx;
border-radius: 999rpx;
background: #fff7ed;
border: 1rpx solid #fdba74;
color: #c2410c;
display: flex;
align-items: center;
justify-content: center;
padding: 0 22rpx;
font-size: 24rpx;
}
.sales-income {
margin-top: 10rpx;
padding-left: 8rpx;
color: #dc2626;
font-size: 24rpx;
line-height: 1.4;
}
.qty-box {
width: 240rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12rpx;
padding: 0 20rpx;
min-height: 72rpx;
border-radius: 18rpx;
background: #f8fafc;
border: 1rpx solid #dbe3ef;
}
.qty-input {
flex: 1;
font-size: 30rpx;
color: #0f172a;
}
.qty-unit {
color: #7a8495;
font-size: 24rpx;
}
.summary-input {
min-height: 180rpx;
width: 100%;
padding: 20rpx;
box-sizing: border-box;
border-radius: 18rpx;
background: #f8fafc;
border: 1rpx solid #dbe3ef;
color: #0f172a;
font-size: 28rpx;
line-height: 1.6;
}
.rating-row {
display: flex;
justify-content: center;
padding-top: 6rpx;
}
.form-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
height: var(--form-actions-height);
padding: 20rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
display: flex;
gap: 16rpx;
background: #ffffff;
box-shadow: 0 -4rpx 16rpx rgba(15, 23, 42, 0.08);
}
.form-actions-placeholder {
height: var(--form-actions-height);
}
.draft-btn {
flex: 1;
min-height: 78rpx;
border-radius: 999rpx;
border: 1rpx solid #ff8a4c;
background: #fff7f2;
color: #ff7a35;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
}
.submit {
flex: 1;
margin: 0;
min-height: 78rpx;
padding: 0 24rpx;
font-size: 28rpx;
font-weight: 700;
}
.merchant-picker {
background: #ffffff;
padding-bottom: calc(env(safe-area-inset-bottom) + 12rpx);
}
.merchant-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx 20rpx;
}
.picker-title {
font-size: 32rpx;
font-weight: 700;
color: #0f172a;
}
.picker-action {
font-size: 28rpx;
color: #7a8495;
}
.picker-action.primary {
color: #2563eb;
font-weight: 700;
}
.merchant-picker-search {
padding: 0 24rpx 20rpx;
}
.merchant-picker-search-row {
display: flex;
gap: 14rpx;
}
.merchant-search-item {
flex: 1;
min-width: 0;
}
.merchant-picker-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0;
}
.merchant-picker-view {
width: 100%;
height: 420rpx;
}
.merchant-picker-item {
display: flex;
align-items: center;
justify-content: center;
height: 64px;
padding: 0 24rpx;
text-align: center;
font-size: 26rpx;
color: #1f2937;
}
.product-picker {
background: #ffffff;
padding-bottom: calc(env(safe-area-inset-bottom) + 12rpx);
}
.product-panel {
display: flex;
height: 62vh;
border-top: 1rpx solid #eef2f7;
}
.category-sidebar {
width: 220rpx;
background: linear-gradient(180deg, #f8fafc, #fff7ed);
border-right: 1rpx solid #eef2f7;
}
.category-item {
min-height: 88rpx;
padding: 18rpx 16rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: #64748b;
font-size: 24rpx;
border-bottom: 1rpx solid rgba(226, 232, 240, 0.8);
}
.category-item-active {
background: #ffffff;
color: #ea580c;
font-weight: 700;
}
.product-panel-main {
flex: 1;
padding: 20rpx 24rpx 24rpx;
box-sizing: border-box;
}
.product-picker-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
padding: 18rpx 20rpx;
border-radius: 18rpx;
background: #f8fafc;
border: 1rpx solid #e2e8f0;
}
.product-picker-item + .product-picker-item {
margin-top: 12rpx;
}
.product-picker-main {
flex: 1;
min-width: 0;
}
.product-picker-cover {
flex-shrink: 0;
width: 96rpx;
height: 96rpx;
border-radius: 16rpx;
background: #ffffff;
}
.product-picker-name {
color: #0f172a;
font-size: 28rpx;
font-weight: 600;
}
.product-picker-sub {
margin-top: 6rpx;
color: #64748b;
font-size: 22rpx;
}
.product-picker-income {
margin-top: 6rpx;
color: #dc2626;
font-size: 22rpx;
}
.product-picker-tag {
flex-shrink: 0;
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: #dcfce7;
color: #15803d;
font-size: 22rpx;
font-weight: 700;
}
.audit-empty-tip {
padding: 30rpx 0;
text-align: center;
color: #94a3b8;
font-size: 26rpx;
}
.reward-summary-card {
display: flex;
align-items: center;
justify-content: space-between;
}
.reward-summary-label {
font-size: 28rpx;
font-weight: 600;
color: #0f172a;
}
.reward-summary-value {
font-size: 32rpx;
font-weight: 700;
color: #dc2626;
}
</style>