575b4a723a
门店接口 /shop/merchant/groupByChain 一次返回 8105 家门店(约 1.1MB)。
此前把门店明细原样放进页面 data,data 树膨胀到 1118KB,导致此后每次
setData 都要重新 diff/序列化整棵树——即使载荷只有 1 个字段,耗时也从
约 490ms 涨到约 1000ms,首屏因此多花约 3.5s。
改为把明细挂在实例属性 merchantStore 上(不进页面 data),
merchantChainList 只保留 {id, text, merchantCount} 骨架,
新增 getMerchantsByChainId() / getVisibleMerchants() 按需取数。
涉及页面:
- packageUser/self-report/create.vue
- packageUser/chronic/index.vue
- pages/task/detail.vue
- pages/share/create.vue
- pages/share/index.vue
- pages/audit/components/AuditShareItem.vue
实测(微信开发者工具自动化):
- 页面 data 门店明细 1118KB -> 1KB
- 单字段 setData 998ms -> 10~53ms
- self-report/create 首屏 9055ms -> 5341ms
- 门店 picker 选项数不变(大参林 2700 项),搜索与选中回填均正常
2110 lines
66 KiB
Vue
2110 lines
66 KiB
Vue
<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: '暂无连锁', merchantCount: 0}]
|
||
},
|
||
merchantStoreOptions() {
|
||
// 门店明细不放在页面 data 里(见 setMerchantChains),从实例上的 store 取。
|
||
// 这里读 merchantChainList 是为了建立响应式依赖:门店数据到达后需要重算。
|
||
const chainList = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
|
||
const hasChain = chainList.some(chain => String(chain.id) === String(this.form.chainId))
|
||
const merchants = hasChain ? this.getMerchantsByChainId(this.form.chainId) : []
|
||
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]
|
||
}
|
||
},
|
||
created() {
|
||
// 门店明细挂实例上而非 data:data 里的一切都会进页面 data 树并被 setData 序列化。
|
||
this.merchantStore = []
|
||
},
|
||
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)
|
||
const chains = (groupedList.length
|
||
? groupedList
|
||
: list.map((item, index) => this.normalizeMerchantChain(item, index)))
|
||
.filter(item => Array.isArray(item.merchants) && item.merchants.length > 0)
|
||
this.setMerchantChains(chains)
|
||
} catch (error) {
|
||
this.setMerchantChains([])
|
||
uni.showToast({
|
||
title: '门店列表获取失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
this.merchantLoading = false
|
||
}
|
||
},
|
||
// 门店明细(全量可达 8000+ 条)只放在实例上,不进页面 data。
|
||
// 一旦放进 data,页面 data 树会膨胀到 1MB+,此后每次 setData 都要重新
|
||
// diff/序列化整棵树,连只含一个字段的 setData 也会从 ~490ms 涨到 ~1000ms。
|
||
setMerchantChains(chains) {
|
||
const list = Array.isArray(chains) ? chains : []
|
||
this.merchantStore = list.map(chain => ({
|
||
id: chain.id,
|
||
text: chain.text,
|
||
merchants: Array.isArray(chain.merchants) ? chain.merchants : []
|
||
}))
|
||
this.merchantChainList = this.merchantStore.map(chain => ({
|
||
id: chain.id,
|
||
text: chain.text,
|
||
merchantCount: chain.merchants.length
|
||
}))
|
||
},
|
||
getMerchantsByChainId(chainId) {
|
||
const store = Array.isArray(this.merchantStore) ? this.merchantStore : []
|
||
const chain = store.find(item => String(item.id) === String(chainId))
|
||
return chain && Array.isArray(chain.merchants) ? chain.merchants : []
|
||
},
|
||
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>
|