3571 lines
117 KiB
Vue
3571 lines
117 KiB
Vue
<template>
|
||
<view class="detail-page">
|
||
<view class="page-hero">
|
||
<AppNavbar title="任务详情" show-back/>
|
||
</view>
|
||
<view class="store-card">
|
||
<view class="flex justify-between items-center">
|
||
<view class="flex justify-start items-center">
|
||
<text class="mr-15">{{ task.title }}</text>
|
||
<view class="store-tag">{{ taskTag }}</view>
|
||
</view>
|
||
</view>
|
||
<view class="store-title">{{ storeName }}</view>
|
||
<view v-if="storeChainText" class="store-chain">
|
||
<text class="store-chain-label">所属连锁</text>
|
||
<text class="store-chain-value">{{ storeChainText }}</text>
|
||
</view>
|
||
<view class="store-address">
|
||
<u-icon name="map" size="16" color="rgba(255, 255, 255, 0.9)"/>
|
||
<text>{{ storeAddress }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card block total-progress-card">
|
||
<view class="total-progress-head">
|
||
<text class="total-progress-title">总进度</text>
|
||
<text class="total-progress-value">{{ totalProgressText }}</text>
|
||
</view>
|
||
<view class="total-progress-bar">
|
||
<view class="total-progress-fill" :style="{ width: `${totalProgressPercent}%` }"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card block detail-info">
|
||
<view class="detail-info-head" @tap="toggleDetailExpanded">
|
||
<text class="detail-info-title">任务详情</text>
|
||
<view class="detail-info-toggle">
|
||
<text class="detail-info-toggle-text">{{ detailExpanded ? '收起' : '展开' }}</text>
|
||
<view :class="['detail-info-toggle-arrow', detailExpanded ? 'open' : '']">
|
||
<u-icon name="arrow-right" size="16" color="#8f9bb3"/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="detailExpanded" class="detail-info-body">
|
||
<view class="detail-requirement">
|
||
<view class="detail-block-head">
|
||
<view class="detail-icon">
|
||
<u-icon
|
||
name="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260127/a6a3db8692c2366cf3d219b8d5009b7d.png"
|
||
size="34rpx"/>
|
||
</view>
|
||
<text class="detail-block-title">任务要求</text>
|
||
</view>
|
||
<text class="detail-block-text">{{ detailRequirement }}</text>
|
||
</view>
|
||
<!-- <view class="detail-section" v-if="task.cateId !== 4">-->
|
||
<!-- <view class="detail-section-title">-->
|
||
<!-- <view class="detail-section-dot orange"></view>-->
|
||
<!-- <text>重点爆破品种</text>-->
|
||
<!-- </view>-->
|
||
<!-- <view class="detail-section-card">{{ detailFocus }}</view>-->
|
||
<!-- </view>-->
|
||
<view class="detail-section">
|
||
<view class="detail-section-title">
|
||
<view class="detail-section-dot green"></view>
|
||
<text>活动政策</text>
|
||
</view>
|
||
<view class="detail-section-card">{{ detailPolicy }}</view>
|
||
</view>
|
||
<view class="detail-section">
|
||
<view class="detail-section-title">
|
||
<view class="detail-section-dot blue"></view>
|
||
<text>任务执行时间段</text>
|
||
</view>
|
||
<view class="detail-section-card">{{ taskExecutePeriod }}</view>
|
||
</view>
|
||
<!-- <view v-if="!manualSalesMode" class="detail-section">-->
|
||
<!-- <view class="detail-section-title">-->
|
||
<!-- <view class="detail-section-dot orange"></view>-->
|
||
<!-- <text>任务奖金</text>-->
|
||
<!-- </view>-->
|
||
<!-- <view class="detail-section-card">销售奖金{{ salesBonusDisplay }}元,额外奖金{{ extraBonusDisplay }}元</view>-->
|
||
<!-- </view>-->
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!isStoreTask" 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] && 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 class="detail-section-card location-card" v-if="isAuditEntry && clockPhotos[0] && clockPhotos[0].lat">
|
||
<view class="location-row">
|
||
<text class="location-label">纬度:</text>
|
||
<text class="location-value">{{ clockPhotos[0].lat }}</text>
|
||
</view>
|
||
<view class="location-row">
|
||
<text class="location-label">经度:</text>
|
||
<text class="location-value">{{ clockPhotos[0].lng }}</text>
|
||
</view>
|
||
<view class="location-row">
|
||
<text class="location-label">详细地址:</text>
|
||
<text class="location-value">{{ clockPhotos[0].location }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="isStoreTask" class="card block">
|
||
<view class="section-title-row">
|
||
<view class="section-title">巡店列表</view>
|
||
<view class="section-extra" v-if="totalTaskNum">目标{{ totalTaskNum }}家</view>
|
||
</view>
|
||
<view v-if="!storeList.length" class="audit-empty-tip">暂无巡店记录</view>
|
||
<view v-for="(item, idx) in storeList" :key="`store-${idx}`" class="visit-store-card">
|
||
<view class="visit-store-head">
|
||
<view>
|
||
<view class="visit-store-title">巡店{{ idx + 1 }}</view>
|
||
<view v-if="item.visitTime" class="visit-store-time">{{ item.visitTime }}</view>
|
||
</view>
|
||
<view
|
||
v-if="!isAuditEntry && storeList.length > 1"
|
||
class="visit-store-remove"
|
||
@tap="removeStoreItem(idx)"
|
||
>
|
||
删除
|
||
</view>
|
||
</view>
|
||
<view class="visit-store-field">
|
||
<view class="visit-store-label">选择门店</view>
|
||
<view
|
||
:class="['visit-store-picker-inner', item.merchantName ? '' : 'placeholder']"
|
||
@tap="openStoreMerchantPicker(idx)"
|
||
>
|
||
{{
|
||
item.merchantName ? `${item.chainName || '-'}/${item.merchantName}` : (merchantLoading ? '门店加载中...' : '请选择连锁和门店')
|
||
}}
|
||
</view>
|
||
</view>
|
||
<view class="visit-store-field">
|
||
<view class="visit-store-label-row">
|
||
<view class="visit-store-label">定位</view>
|
||
<view v-if="!isAuditEntry" class="visit-store-action" @tap="fetchStoreLocation(idx)">获取定位</view>
|
||
</view>
|
||
<view class="detail-section-card location-card">
|
||
<view class="location-row">
|
||
<text class="location-label">纬度:</text>
|
||
<text class="location-value">{{ formatCoordinate(item.lat) }}</text>
|
||
</view>
|
||
<view class="location-row">
|
||
<text class="location-label">经度:</text>
|
||
<text class="location-value">{{ formatCoordinate(item.lng) }}</text>
|
||
</view>
|
||
<view class="location-row">
|
||
<text class="location-label">详细地址:</text>
|
||
<text class="location-value">{{ item.location || '暂无地址' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="visit-store-field">
|
||
<view class="section-title-row">
|
||
<view class="visit-store-label">打卡拍照</view>
|
||
<view class="section-extra">最多9张</view>
|
||
</view>
|
||
<view v-if="!item.photos || item.photos.length === 0" class="photo-grid">
|
||
<view v-if="!isAuditEntry" class="photo-item add" @tap="uploadStorePhotos(idx)">
|
||
<u-icon name="photo" size="32" color="#c6ccd8"/>
|
||
</view>
|
||
<view v-else class="audit-empty-tip visit-store-empty">暂无巡店照片</view>
|
||
</view>
|
||
<view v-else class="photo-grid">
|
||
<view v-for="(photo, photoIdx) in item.photos" :key="`store-${idx}-photo-${photoIdx}`" class="photo-item">
|
||
<image :src="photo" mode="aspectFill" class="photo-image" @tap="previewStorePhotos(idx, photoIdx)"/>
|
||
<view v-if="!isAuditEntry" class="photo-remove" @tap="removeStorePhoto(idx, photoIdx)">x</view>
|
||
</view>
|
||
<view v-if="!isAuditEntry && item.photos.length < 9" class="photo-item add" @tap="uploadStorePhotos(idx)">
|
||
<u-icon name="photo" size="32" color="#c6ccd8"/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view
|
||
v-if="!isAuditEntry"
|
||
class="visit-store-add"
|
||
@tap="addStoreItem"
|
||
>
|
||
新增巡店
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!isStoreTask" class="card block">
|
||
<view class="section-title-row">
|
||
<view class="section-title">门头摆台照</view>
|
||
<view class="section-extra">最多{{ boothLimit }}张</view>
|
||
</view>
|
||
<view v-if="isAuditEntry && boothPhotos.length === 0" class="audit-empty-tip">暂无门头摆台照</view>
|
||
<view v-else class="photo-grid">
|
||
<view v-for="(item, idx) in boothPhotos" :key="idx" class="photo-item">
|
||
<image :src="item" mode="aspectFill" class="photo-image" @tap="handleBoothAction(idx)"/>
|
||
<view v-if="!isAuditEntry" class="photo-remove" @tap="removeBooth(idx)">x</view>
|
||
</view>
|
||
<view v-if="!isAuditEntry && boothPhotos.length < boothLimit" class="photo-item add" @tap="uploadBooth">
|
||
<u-icon name="photo" size="32" color="#c6ccd8"/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!isStoreTask" class="card block">
|
||
<view class="section-title-row sales-title-row">
|
||
<view class="section-title">销量填写</view>
|
||
<view v-if="manualSalesMode" class="section-extra">已添加{{ manualSalesItemCount }}项</view>
|
||
</view>
|
||
<view v-if="manualSalesMode && !isAuditEntry" 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="manualSalesMode && manualSalesItemCount === 0" class="audit-empty-tip">
|
||
{{ isAuditEntry ? '暂无销量商品' : '请点击“新增商品”逐个添加销量商品' }}
|
||
</view>
|
||
<block v-if="!manualSalesMode || manualSalesItemCount > 0">
|
||
<view class="sales-group" v-for="(group, gIdx) in salesGroups" :key="gIdx">
|
||
<view class="sales-head" @tap="toggleGroup(gIdx)">
|
||
<view class="sales-title">{{ group.title }}</view>
|
||
<view :class="['sales-arrow', group.open ? 'open' : '']">
|
||
<u-icon name="arrow-right" size="16" color="#ffffff"/>
|
||
</view>
|
||
</view>
|
||
<view v-if="group.open" class="sales-items">
|
||
<view class="sales-item" v-for="(item, iIdx) in group.items" :key="iIdx">
|
||
<view class="sales-main">
|
||
<!-- <view :class="['sales-check', item.checked ? 'checked' : '']" @tap.stop="toggleItem(gIdx, iIdx)">-->
|
||
<!-- <u-icon v-if="item.checked" name="checkmark" size="14" color="#ffffff"/>-->
|
||
<!-- </view>-->
|
||
<view class="sales-name">
|
||
<text>{{ item.name }}</text>
|
||
<!-- <text v-if="hasTaskIncome(item)" class="sales-target">单品奖金</text>-->
|
||
<!-- <text v-if="hasTaskIncome(item)" class="text-red text-25">¥{{ formatTaskIncome(item) }}</text>-->
|
||
</view>
|
||
<view v-if="manualSalesMode && !isAuditEntry" class="sales-remove" @tap.stop="removeManualSalesItem(gIdx, iIdx)">删除</view>
|
||
</view>
|
||
<view v-if="item.taskNum" class="sales-progress">
|
||
<view class="sales-progress-bar">
|
||
<view class="sales-progress-fill" :style="{ width: `${salesProgress(item)}%` }"></view>
|
||
</view>
|
||
<text class="sales-progress-text">{{ salesProgressText(item) }}</text>
|
||
</view>
|
||
<view class="sales-controls">
|
||
<picker class="spec-picker" mode="selector" :range="item.specOptions"
|
||
@change="onSpecChange($event, gIdx, iIdx)">
|
||
<view class="spec-pill">
|
||
<text>{{ item.spec }}</text>
|
||
</view>
|
||
</picker>
|
||
<view class="qty-box">
|
||
<input v-model="item.qty" class="qty-input" type="number" placeholder="0" @input="scheduleSave"/>
|
||
<text class="qty-unit">{{ item.unit }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
</view>
|
||
|
||
<u-popup :show="showProductPicker" mode="bottom" round="28rpx" @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"/>
|
||
</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', 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
|
||
v-for="item in displayedProductOptions"
|
||
:key="item._key"
|
||
class="product-picker-item"
|
||
@tap="selectProduct(item)"
|
||
>
|
||
<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>
|
||
|
||
<view v-if="!isStoreTask" 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>
|
||
<template v-if="receiptPhotos.length === 0">
|
||
<view v-if="isAuditEntry" class="audit-empty-tip">暂无小票</view>
|
||
<view v-else class="receipt-upload" @tap="uploadReceipt">
|
||
<view class="receipt-placeholder">
|
||
<view class="receipt-icon">
|
||
<u-icon name="arrow-upward" size="30" color="#8f9bb3"/>
|
||
</view>
|
||
<view class="receipt-title">点击拍照上传</view>
|
||
<view class="receipt-desc">支持jpg/png,需清晰可见</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
<view v-else class="photo-grid">
|
||
<view v-for="(item, idx) in receiptPhotos" :key="idx" class="photo-item">
|
||
<image :src="item" mode="aspectFill" class="photo-image" @tap="handleReceiptAction(idx)"/>
|
||
<view v-if="!isAuditEntry" class="photo-remove" @tap="removeReceipt(idx)">x</view>
|
||
</view>
|
||
<view v-if="!isAuditEntry && 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="summary" class="summary-input" placeholder="请填写活动情况" @input="scheduleSave"/>
|
||
</view>
|
||
|
||
<view v-if="!isStoreTask" 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] && 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 class="detail-section-card location-card" v-if="isAuditEntry && clockPhotos[1] && clockPhotos[1].lat">
|
||
<view class="location-row">
|
||
<text class="location-label">纬度:</text>
|
||
<text class="location-value">{{ clockPhotos[1].lat }}</text>
|
||
</view>
|
||
<view class="location-row">
|
||
<text class="location-label">经度:</text>
|
||
<text class="location-value">{{ clockPhotos[1].lng }}</text>
|
||
</view>
|
||
<view class="location-row">
|
||
<text class="location-label">详细地址:</text>
|
||
<text class="location-value">{{ clockPhotos[1].location }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!isStoreTask" 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 v-model="rating" :count="5" :size="34" active-color="#ff8a4c" inactive-color="#e4e8ef"
|
||
@change="scheduleSave"/>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!isAuditEntry && (task.userTask.status === 0 || task.userTask.status === 3)" class="task-submit-bar">
|
||
<view class="primary-btn submit" @tap="submit">提交</view>
|
||
</view>
|
||
<view v-if="!isAuditEntry && (task.userTask.status === 0 || task.userTask.status === 3)" class="task-submit-placeholder"/>
|
||
<view v-if="!isAuditEntry && task.userTask.status === 1" class="text-green text-center mt-20">审核中</view>
|
||
<view v-if="!isAuditEntry && task.userTask.status === 2" class="text-green text-center mt-20">审核通过</view>
|
||
<view v-if="isAuditEntry" class="audit-action-bar">
|
||
<view v-if="canAuditApproveReject" class="audit-action-list">
|
||
<view class="audit-action-btn" @tap="handleAuditApprove">通过</view>
|
||
<view class="audit-action-btn audit-action-btn-danger" @tap="openAuditRejectPopup">驳回</view>
|
||
</view>
|
||
<view v-if="canAuditCopy" class="audit-action-list mt-12">
|
||
<view class="audit-action-btn" @tap="openAuditCopyPopup">抄送</view>
|
||
</view>
|
||
</view>
|
||
<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">
|
||
<u-input v-model="merchantSearchKeyword" placeholder="搜索连锁/门店" shape="circle"
|
||
:customStyle="{border: '3px solid #ff8a4c !important'}"/>
|
||
<!-- <input-->
|
||
<!-- v-model="merchantSearchKeyword"-->
|
||
<!-- class="merchant-search-input"-->
|
||
<!-- placeholder="搜索连锁/门店"-->
|
||
<!-- placeholder-class="merchant-search-placeholder"-->
|
||
<!-- />-->
|
||
</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 merchantChainOptions" :key="`chain-${idx}`" class="merchant-picker-item">
|
||
{{ item.text }}
|
||
</view>
|
||
</picker-view-column>
|
||
<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>
|
||
<Login ref="Login" @done="getUserData"/>
|
||
|
||
<view v-if="showAuditRejectPopup" class="audit-popup-mask" @tap="closeAuditRejectPopup"></view>
|
||
<view v-if="showAuditRejectPopup" class="audit-popup card" @tap.stop>
|
||
<view class="audit-popup-head">
|
||
<view class="section-title">填写拒绝原因</view>
|
||
<view class="close-btn" @tap="closeAuditRejectPopup">关闭</view>
|
||
</view>
|
||
<view class="audit-form-row">
|
||
<view class="audit-form-label">拒绝原因</view>
|
||
<input v-model="auditRejectReason" class="audit-form-input" type="text" placeholder="请输入拒绝原因"/>
|
||
</view>
|
||
<view class="primary-btn audit-submit-btn" @tap="submitAuditRejectReason">提交</view>
|
||
</view>
|
||
|
||
<view v-if="showAuditCopyPopup" class="audit-popup-mask" @tap="closeAuditCopyPopup" @touchmove.stop.prevent></view>
|
||
<view v-if="showAuditCopyPopup" class="audit-popup card" @tap.stop>
|
||
<view class="audit-popup-head">
|
||
<view class="section-title">抄送</view>
|
||
<view class="close-btn" @tap="closeAuditCopyPopup">关闭</view>
|
||
</view>
|
||
<view class="audit-copy-search-row">
|
||
<input
|
||
v-model="auditCopyKeyword"
|
||
class="audit-copy-search-input"
|
||
type="text"
|
||
placeholder="输入姓名/手机号搜索用户"
|
||
@confirm="searchAuditCopyUsers"
|
||
/>
|
||
<view class="audit-copy-search-btn" @tap="searchAuditCopyUsers">搜索</view>
|
||
</view>
|
||
<view v-if="auditCopySearching" class="audit-copy-loading">搜索中...</view>
|
||
<scroll-view v-else class="audit-copy-user-list" scroll-y>
|
||
<view
|
||
v-for="user in auditCopyUsers"
|
||
:key="`audit-copy-user-${user.id}`"
|
||
:class="['audit-copy-user-item', auditSelectedCopyUserId === user.id ? 'audit-copy-user-item-active' : '']"
|
||
@tap="auditSelectedCopyUserId = user.id"
|
||
>
|
||
<text class="audit-copy-user-name">{{ user.label }}</text>
|
||
<text class="audit-copy-user-phone">{{ user.phone || '' }}</text>
|
||
</view>
|
||
<view v-if="auditCopyUsers.length === 0" class="audit-copy-empty">暂无用户</view>
|
||
</scroll-view>
|
||
<view class="primary-btn audit-submit-btn" @tap="submitAuditCopy">确认抄送</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import Login from "@/components/Login.vue";
|
||
import AppNavbar from "@/components/AppNavbar.vue";
|
||
import {userInfoReq, userSearchReq} from "@/api/user";
|
||
import {getUserInfo} from "@/util/user";
|
||
import {chooseImg, getLocationOnly, calcDistanceMeters} from "@/util";
|
||
import {changeUserTaskReq, copyUserTaskReq, taskInfoReq} from "@/api/task";
|
||
import {merchantGroupByChainReq} from "@/api/shop";
|
||
import {productGroupByCateReq} from "@/api/product";
|
||
import {Base64} from 'js-base64';
|
||
import dayjs from "dayjs";
|
||
|
||
export default {
|
||
components: {Login, AppNavbar},
|
||
data() {
|
||
return {
|
||
task: {
|
||
title: '任务详情',
|
||
reward: 0,
|
||
target: ''
|
||
},
|
||
clockPhotos: [
|
||
{key: 'in', label: '上班', time: '', date: '', url: ''},
|
||
{key: 'out', label: '下班', time: '', date: '', url: ''}
|
||
],
|
||
boothPhotos: [],
|
||
boothLimit: 6,
|
||
receiptPhotos: [],
|
||
receiptLimit: 999,
|
||
summary: '',
|
||
rating: 0,
|
||
salesGroups: [],
|
||
productGroups: [],
|
||
productLoading: false,
|
||
productLoadedChainName: '',
|
||
showProductPicker: false,
|
||
salesKeyword: '',
|
||
activeProductGroupKey: '',
|
||
storeList: [],
|
||
merchantChainList: [],
|
||
merchantLoading: false,
|
||
showMerchantPicker: false,
|
||
merchantSearchKeyword: '',
|
||
merchantPickerValue: [0, 0],
|
||
activeStorePickerIndex: -1,
|
||
id: null,
|
||
isLoggedIn: false,
|
||
lat: null,
|
||
lng: null,
|
||
currentAddress: '',
|
||
detailExpanded: true,
|
||
auditEntry: false,
|
||
auditId: '',
|
||
auditTaskId: '',
|
||
auditUserId: '',
|
||
auditStatus: null,
|
||
auditSubmitting: false,
|
||
showAuditRejectPopup: false,
|
||
auditRejectReason: '',
|
||
showAuditCopyPopup: false,
|
||
auditCopyKeyword: '',
|
||
auditCopyUsers: [],
|
||
auditCopySearching: false,
|
||
auditSelectedCopyUserId: '',
|
||
autoSaveTimer: null,
|
||
autoSaving: false,
|
||
autoSavePending: false
|
||
}
|
||
},
|
||
computed: {
|
||
isAuditEntry() {
|
||
return this.auditEntry
|
||
},
|
||
isStoreTask() {
|
||
return Number(this.task && this.task.cateId) === 4
|
||
},
|
||
canAuditApproveReject() {
|
||
return this.isAuditEntry && Number(this.auditStatus) === 1
|
||
},
|
||
canAuditCopy() {
|
||
if (!this.isAuditEntry) return false
|
||
const status = Number(this.auditStatus)
|
||
return status === 1 || status === 3
|
||
},
|
||
manualSalesMode() {
|
||
if (this.isStoreTask) return false
|
||
const list = Array.isArray(this.task && this.task.productList) ? this.task.productList : []
|
||
return list.length === 0
|
||
},
|
||
manualSalesItemCount() {
|
||
return (Array.isArray(this.salesGroups) ? this.salesGroups : []).reduce((total, group) => {
|
||
const items = Array.isArray(group.items) ? group.items : []
|
||
return total + items.length
|
||
}, 0)
|
||
},
|
||
productChainName() {
|
||
const task = this.task || {}
|
||
const merchant = task && task.merchant ? task.merchant : {}
|
||
const chain = merchant.chain || merchant.chainName || merchant.chainAbb || task.chain || task.chainName || task.chainAbb || ''
|
||
return String(chain || '').trim()
|
||
},
|
||
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 : []
|
||
},
|
||
merchantChainOptions() {
|
||
const list = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
|
||
const keyword = (this.merchantSearchKeyword || '').trim()
|
||
if (!keyword) return list.length ? list : [{id: '', text: '暂无连锁', merchants: []}]
|
||
const filtered = list
|
||
.map(chain => {
|
||
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
|
||
const chainMatch = (chain.text || '').includes(keyword)
|
||
if (chainMatch) return {...chain}
|
||
const matchedMerchants = merchants.filter(merchant => (merchant.text || '').includes(keyword))
|
||
if (!matchedMerchants.length) return null
|
||
return {
|
||
...chain,
|
||
merchants: matchedMerchants
|
||
}
|
||
})
|
||
.filter(Boolean)
|
||
return filtered.length ? filtered : [{id: '', text: '无匹配连锁', merchants: []}]
|
||
},
|
||
merchantStoreOptions() {
|
||
const chainIndex = Number(this.merchantPickerValue[0] || 0)
|
||
const currentChain = this.merchantChainOptions[chainIndex] || this.merchantChainOptions[0] || {}
|
||
const merchants = Array.isArray(currentChain.merchants) ? currentChain.merchants : []
|
||
return merchants.length ? merchants : [{id: '', text: '无门店'}]
|
||
},
|
||
merchantIndicatorStyle() {
|
||
return 'height: 44px;'
|
||
},
|
||
storeName() {
|
||
const merchant = (this.task && this.task.merchant) ? this.task.merchant : {}
|
||
return merchant.name || this.task.title || '门店名称'
|
||
},
|
||
storeChainText() {
|
||
const task = this.task || {}
|
||
const merchant = task && task.merchant ? task.merchant : {}
|
||
const chain = merchant.chain || merchant.chainName || merchant.chainAbb || task.chain || task.chainName || task.chainAbb || ''
|
||
return String(chain || '').trim()
|
||
},
|
||
storeAddress() {
|
||
const merchant = (this.task && this.task.merchant) ? this.task.merchant : {}
|
||
const fullAddress = [merchant.province, merchant.city, merchant.region, merchant.address].filter(Boolean).join('')
|
||
return fullAddress || this.task.address || '暂无地址'
|
||
},
|
||
taskTag() {
|
||
const cate = (this.task && this.task.cate) ? this.task.cate : {}
|
||
return cate.title || this.task.target || '日常驻店'
|
||
},
|
||
detailRequirement() {
|
||
return this.pickDetailValue(
|
||
['detail'],
|
||
'上下班打卡,拍摄活动照片,提交活动小结'
|
||
);
|
||
},
|
||
detailFocus() {
|
||
if (!this.task.productList || !this.task.productList.length) return ''
|
||
return this.task.productList.map(item => item.product.title).join('、')
|
||
},
|
||
detailPolicy() {
|
||
return this.pickDetailValue(
|
||
['policy', 'policyDesc', 'policyText', 'policyRemark', 'activityPolicy', 'policyDetail'],
|
||
'益血生两盒75折,骨通贴膏买二送一'
|
||
);
|
||
},
|
||
taskExecutePeriod() {
|
||
const startRaw = this.task && this.task.taskStartTime ? this.task.taskStartTime : ''
|
||
const endRaw = this.task && this.task.taskEndTime ? this.task.taskEndTime : ''
|
||
const start = this.trimSecond(startRaw)
|
||
const end = this.trimSecond(endRaw)
|
||
if (start && end) return `${start} 至 ${end}`
|
||
if (start) return `${start} 至 -`
|
||
if (end) return `- 至 ${end}`
|
||
return '暂无时间段'
|
||
},
|
||
totalTaskNum() {
|
||
return this.calcTotalTaskNum();
|
||
},
|
||
totalQty() {
|
||
return this.calcTotalQty();
|
||
},
|
||
totalProgressPercent() {
|
||
if (!this.totalTaskNum) return 0;
|
||
return Math.min(100, Math.round((this.totalQty / this.totalTaskNum) * 100));
|
||
},
|
||
totalProgressText() {
|
||
return `${this.totalQty}/${this.totalTaskNum || 0}`;
|
||
},
|
||
salesBonusDisplay() {
|
||
return this.formatAmountDisplay(this.resolveTaskAmount([
|
||
this.task && this.task.reward,
|
||
this.task && this.task.taskIncome
|
||
]))
|
||
},
|
||
extraBonusDisplay() {
|
||
return this.formatAmountDisplay(this.resolveTaskAmount([
|
||
this.task && this.task.extraIncome,
|
||
this.task && this.task.extraReward,
|
||
this.task && this.task.extra_bonus,
|
||
this.task && this.task.extraBonus
|
||
]))
|
||
}
|
||
},
|
||
watch: {
|
||
merchantSearchKeyword() {
|
||
this.merchantPickerValue = [0, 0]
|
||
}
|
||
},
|
||
async onLoad(options) {
|
||
this.auditEntry = String(options.fromAudit || '') === '1'
|
||
this.auditId = options.auditId || ''
|
||
this.auditTaskId = options.auditTaskId || ''
|
||
this.auditUserId = options.auditUserId || ''
|
||
if (options.auditStatus !== undefined && options.auditStatus !== null && options.auditStatus !== '') {
|
||
const status = Number(options.auditStatus)
|
||
this.auditStatus = Number.isFinite(status) ? status : null
|
||
}
|
||
if (options.title) this.task.title = decodeURIComponent(options.title)
|
||
if (options.reward) this.task.reward = options.reward
|
||
if (options.target) this.task.target = decodeURIComponent(options.target)
|
||
if (options.id) {
|
||
this.id = Number(options.id)
|
||
this.getInfo()
|
||
}
|
||
},
|
||
async onShow() {
|
||
if (getUserInfo().token) this.getUserData()
|
||
else {
|
||
this.$refs.Login.open()
|
||
}
|
||
if (this.isAuditEntry) return
|
||
const res = await getLocationOnly().catch(() => {
|
||
this.toast('请允许位置授权')
|
||
})
|
||
if (res) {
|
||
this.lat = res.latitude
|
||
this.lng = res.longitude
|
||
this.currentAddress = res.full || [res.province_str, res.city_str, res.area_str].filter(Boolean).join('')
|
||
this.applyNearestMerchantToAutoItems()
|
||
}
|
||
},
|
||
methods: {
|
||
calReward() {
|
||
const reward = this.resolveTaskAmount([
|
||
this.task && this.task.reward,
|
||
this.task && this.task.taskIncome
|
||
])
|
||
const extraReward = this.resolveTaskAmount([
|
||
this.task && this.task.extraIncome,
|
||
this.task && this.task.extraReward,
|
||
this.task && this.task.extra_bonus,
|
||
this.task && this.task.extraBonus
|
||
])
|
||
return (reward + extraReward).toFixed(2)
|
||
},
|
||
resolveTaskAmount(candidates = []) {
|
||
for (let i = 0; i < candidates.length; i += 1) {
|
||
const value = candidates[i]
|
||
if (value === undefined || value === null || String(value).trim() === '') continue
|
||
const num = Number(value)
|
||
if (Number.isFinite(num)) return num
|
||
}
|
||
return 0
|
||
},
|
||
formatAmountDisplay(value) {
|
||
const num = Number(value)
|
||
if (!Number.isFinite(num)) return '0'
|
||
return Number.isInteger(num) ? String(num) : num.toFixed(2)
|
||
},
|
||
async getUserData() {
|
||
const {data} = await userInfoReq()
|
||
this.isLoggedIn = true
|
||
this.user = data
|
||
},
|
||
async getInfo() {
|
||
const {data} = await taskInfoReq(this.id)
|
||
this.task = data
|
||
const applied = this.applyUserTask(data.userTask)
|
||
if (this.isStoreTask) {
|
||
await this.prepareStoreTaskData()
|
||
} else if (applied) {
|
||
this.salesGroups = this.manualSalesMode
|
||
? this.attachTaskNumToSalesGroups(this.salesGroups, data.productGroup)
|
||
: this.mergeSalesGroupsWithProductGroups(this.salesGroups, data.productGroup)
|
||
} else if (this.manualSalesMode) {
|
||
this.salesGroups = []
|
||
} else {
|
||
this.salesGroups = this.buildSalesGroups(data.productGroup)
|
||
}
|
||
if (this.isAuditEntry && (this.auditStatus === null || this.auditStatus === undefined || Number.isNaN(this.auditStatus))) {
|
||
const status = Number(data && data.userTask ? data.userTask.status : NaN)
|
||
this.auditStatus = Number.isFinite(status) ? status : null
|
||
if (data.userTask && data.userTask.lat) this.lat = data.userTask.lat
|
||
if (data.userTask && data.userTask.lng) this.lng = data.userTask.lng
|
||
if (data.userTask && data.userTask.location) this.currentAddress = data.userTask.location
|
||
}
|
||
},
|
||
applyUserTask(userTask) {
|
||
if (!userTask) return false
|
||
const storeList = this.parseJsonMaybe(userTask.storeList)
|
||
if (Array.isArray(storeList)) {
|
||
this.storeList = storeList.map(item => this.normalizeStoreItem(item))
|
||
}
|
||
const clockPhotos = this.parseJsonMaybe(userTask.clockPhotos)
|
||
if (Array.isArray(clockPhotos) && clockPhotos.length) {
|
||
this.clockPhotos = this.normalizeClockPhotos(clockPhotos)
|
||
}
|
||
const boothPhotos = this.parseJsonMaybe(userTask.boothPhotos)
|
||
if (Array.isArray(boothPhotos)) {
|
||
this.boothPhotos = boothPhotos
|
||
}
|
||
const receiptPhoto = this.parseJsonMaybe(userTask.receiptPhoto)
|
||
this.receiptPhotos = this.normalizeReceiptPhotos(receiptPhoto)
|
||
const summary = this.parseJsonMaybe(userTask.summary)
|
||
if (typeof summary === 'string') {
|
||
this.summary = summary
|
||
}
|
||
const rating = this.parseJsonMaybe(userTask.rating)
|
||
if (rating !== undefined && rating !== null && rating !== '') {
|
||
this.rating = Number(rating) || 0
|
||
}
|
||
const salesGroups = this.parseJsonMaybe(userTask.salesGroups)
|
||
if (Array.isArray(salesGroups) && salesGroups.length) {
|
||
this.salesGroups = this.attachTaskNumToSalesGroups(salesGroups)
|
||
console.log('1', this.salesGroups)
|
||
return true
|
||
}
|
||
return Array.isArray(this.storeList) && this.storeList.length > 0
|
||
},
|
||
parseJsonMaybe(value) {
|
||
if (value === null || value === undefined) return value
|
||
if (typeof value === 'string') {
|
||
const trimmed = value.trim()
|
||
if (!trimmed) return ''
|
||
try {
|
||
return JSON.parse(trimmed)
|
||
} catch (error) {
|
||
return value
|
||
}
|
||
}
|
||
return value
|
||
},
|
||
normalizeReceiptPhotos(value) {
|
||
if (!value) return []
|
||
const list = Array.isArray(value) ? value : [value]
|
||
return list
|
||
.map(item => {
|
||
if (!item) return ''
|
||
if (typeof item === 'string') return item
|
||
return item.url || item.path || item.image || ''
|
||
})
|
||
.filter(Boolean)
|
||
},
|
||
normalizeClockPhotos(list) {
|
||
const base = Array.isArray(this.clockPhotos) ? this.clockPhotos : []
|
||
if (!base.length) return list
|
||
const next = base.map((item, index) => Object.assign({}, item, list[index] || {}))
|
||
if (list.length > base.length) {
|
||
for (let i = base.length; i < list.length; i += 1) {
|
||
next.push(list[i])
|
||
}
|
||
}
|
||
return next
|
||
},
|
||
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
|
||
}
|
||
},
|
||
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
|
||
}
|
||
},
|
||
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
|
||
},
|
||
findNearestMerchant(list) {
|
||
if (!Number.isFinite(this.lat) || !Number.isFinite(this.lng)) return null
|
||
let nearest = null
|
||
let minDistance = Infinity
|
||
list.forEach(item => {
|
||
if (!Number.isFinite(item.lat) || !Number.isFinite(item.lng)) return
|
||
const meters = calcDistanceMeters(this.lat, this.lng, item.lat, item.lng)
|
||
if (!Number.isFinite(meters) || meters >= minDistance) return
|
||
minDistance = meters
|
||
nearest = item
|
||
})
|
||
return nearest
|
||
},
|
||
buildStoreMerchantFields(merchant = null, autoSelected = true) {
|
||
if (!merchant) {
|
||
return {
|
||
merchantId: '',
|
||
merchantName: '',
|
||
chainId: '',
|
||
chainName: '',
|
||
province: '',
|
||
city: '',
|
||
merchantLat: '',
|
||
merchantLng: '',
|
||
merchantAddress: '',
|
||
_autoSelected: autoSelected
|
||
}
|
||
}
|
||
return {
|
||
merchantId: merchant.id ?? '',
|
||
merchantName: merchant.text || '',
|
||
chainId: merchant.chainId || '',
|
||
chainName: merchant.chainName || '',
|
||
province: merchant.province || '',
|
||
city: merchant.city || '',
|
||
merchantLat: Number.isFinite(merchant.lat) ? merchant.lat : '',
|
||
merchantLng: Number.isFinite(merchant.lng) ? merchant.lng : '',
|
||
merchantAddress: merchant.address || '',
|
||
_autoSelected: autoSelected
|
||
}
|
||
},
|
||
createStoreItem(merchant = null, autoSelected = true) {
|
||
return Object.assign({
|
||
visitTime: this.formatDateTime(new Date()),
|
||
lat: '',
|
||
lng: '',
|
||
location: '',
|
||
photos: []
|
||
}, this.buildStoreMerchantFields(merchant, autoSelected))
|
||
},
|
||
normalizeStoreItem(item) {
|
||
const photos = Array.isArray(item && item.photos)
|
||
? item.photos.filter(Boolean)
|
||
: this.normalizeReceiptPhotos(item && (item.photos || item.images || item.image))
|
||
return {
|
||
merchantId: item && (item.merchantId ?? item.storeId ?? item.id) !== undefined
|
||
? (item.merchantId ?? item.storeId ?? item.id)
|
||
: '',
|
||
merchantName: item && (item.merchantName || item.storeName || item.name) ? (item.merchantName || item.storeName || item.name) : '',
|
||
chainId: item && item.chainId !== undefined ? item.chainId : '',
|
||
chainName: item && item.chainName ? item.chainName : '',
|
||
visitTime: item && (item.visitTime || item.time || item.createTime) ? (item.visitTime || item.time || item.createTime) : '',
|
||
province: item && item.province ? item.province : '',
|
||
city: item && item.city ? item.city : '',
|
||
merchantLat: item && item.merchantLat !== undefined ? item.merchantLat : '',
|
||
merchantLng: item && item.merchantLng !== undefined ? item.merchantLng : '',
|
||
merchantAddress: item && (item.merchantAddress || item.address) ? (item.merchantAddress || item.address) : '',
|
||
lat: item && item.lat !== undefined ? item.lat : '',
|
||
lng: item && item.lng !== undefined ? item.lng : '',
|
||
location: item && item.location ? item.location : '',
|
||
photos,
|
||
_autoSelected: false
|
||
}
|
||
},
|
||
sanitizeStoreList() {
|
||
const list = Array.isArray(this.storeList) ? this.storeList : []
|
||
return list.map(item => ({
|
||
merchantId: item.merchantId ?? '',
|
||
merchantName: item.merchantName || '',
|
||
chainId: item.chainId || '',
|
||
chainName: item.chainName || '',
|
||
visitTime: item.visitTime || '',
|
||
province: item.province || '',
|
||
city: item.city || '',
|
||
merchantLat: item.merchantLat !== undefined ? item.merchantLat : '',
|
||
merchantLng: item.merchantLng !== undefined ? item.merchantLng : '',
|
||
merchantAddress: item.merchantAddress || '',
|
||
lat: item.lat !== undefined ? item.lat : '',
|
||
lng: item.lng !== undefined ? item.lng : '',
|
||
location: item.location || '',
|
||
photos: Array.isArray(item.photos) ? item.photos.filter(Boolean) : []
|
||
}))
|
||
},
|
||
syncStoreMerchantMeta() {
|
||
const merchantList = this.flattenMerchantsFromChains(this.merchantChainList)
|
||
if (!Array.isArray(this.storeList) || !this.storeList.length || !merchantList.length) {
|
||
return
|
||
}
|
||
this.storeList = this.storeList.map(item => {
|
||
if (!item || item.merchantId === '' || item.merchantId === null || item.merchantId === undefined) return item
|
||
const matched = merchantList.find(merchant => String(merchant.id) === String(item.merchantId))
|
||
if (!matched) return item
|
||
return Object.assign({}, this.buildStoreMerchantFields(matched, !!item._autoSelected), item, {
|
||
chainId: item.chainId || matched.chainId || '',
|
||
chainName: item.chainName || matched.chainName || '',
|
||
merchantName: item.merchantName || matched.text || '',
|
||
province: item.province || matched.province || '',
|
||
city: item.city || matched.city || '',
|
||
merchantLat: item.merchantLat !== '' && item.merchantLat !== null && item.merchantLat !== undefined ? item.merchantLat : (Number.isFinite(matched.lat) ? matched.lat : ''),
|
||
merchantLng: item.merchantLng !== '' && item.merchantLng !== null && item.merchantLng !== undefined ? item.merchantLng : (Number.isFinite(matched.lng) ? matched.lng : ''),
|
||
merchantAddress: item.merchantAddress || matched.address || '',
|
||
_autoSelected: !!item._autoSelected
|
||
})
|
||
})
|
||
},
|
||
async getMerchantList() {
|
||
this.merchantLoading = true
|
||
try {
|
||
const {data} = await merchantGroupByChainReq({})
|
||
const list = this.unwrapList(data)
|
||
this.merchantChainList = list
|
||
.map((item, index) => this.normalizeMerchantChain(item, index))
|
||
.filter(item => Array.isArray(item.merchants) && item.merchants.length)
|
||
} catch (error) {
|
||
this.merchantChainList = []
|
||
this.toast('门店列表获取失败')
|
||
} finally {
|
||
this.merchantLoading = false
|
||
}
|
||
},
|
||
async prepareStoreTaskData() {
|
||
await this.getMerchantList()
|
||
this.syncStoreMerchantMeta()
|
||
if (!this.storeList.length) {
|
||
this.ensureDefaultStoreList()
|
||
if (!this.isAuditEntry && this.storeList.length && !this.storeList[0].location) {
|
||
await this.fetchStoreLocation(0, true)
|
||
}
|
||
return
|
||
}
|
||
this.applyNearestMerchantToAutoItems()
|
||
},
|
||
ensureDefaultStoreList() {
|
||
if (!this.isStoreTask || this.storeList.length) return
|
||
const merchantList = this.flattenMerchantsFromChains(this.merchantChainList)
|
||
const nearest = this.findNearestMerchant(merchantList)
|
||
const target = nearest || merchantList[0] || null
|
||
this.storeList = [this.createStoreItem(target, true)]
|
||
},
|
||
applyNearestMerchantToAutoItems() {
|
||
const merchantList = this.flattenMerchantsFromChains(this.merchantChainList)
|
||
if (!this.isStoreTask || !merchantList.length) return
|
||
const nearest = this.findNearestMerchant(merchantList)
|
||
const fallback = nearest || merchantList[0] || null
|
||
if (!fallback) return
|
||
this.storeList = (Array.isArray(this.storeList) ? this.storeList : []).map(item => {
|
||
if (!item) return this.createStoreItem(fallback, true)
|
||
if (!item._autoSelected) return item
|
||
return Object.assign({}, item, this.buildStoreMerchantFields(fallback, true))
|
||
})
|
||
},
|
||
getMerchantPickerIndexesByStore(item) {
|
||
const chainList = Array.isArray(this.merchantChainList) ? this.merchantChainList : []
|
||
if (!chainList.length) return [0, 0]
|
||
let chainIndex = chainList.findIndex(chain => String(chain.id) === String(item && item.chainId !== undefined ? item.chainId : ''))
|
||
if (chainIndex < 0 && item && item.merchantId) {
|
||
chainIndex = chainList.findIndex(chain => {
|
||
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
|
||
return merchants.some(merchant => String(merchant.id) === String(item.merchantId))
|
||
})
|
||
}
|
||
if (chainIndex < 0) chainIndex = 0
|
||
const merchants = Array.isArray(chainList[chainIndex].merchants) ? chainList[chainIndex].merchants : []
|
||
let merchantIndex = merchants.findIndex(merchant => String(merchant.id) === String(item && item.merchantId !== undefined ? item.merchantId : ''))
|
||
if (merchantIndex < 0) merchantIndex = 0
|
||
return [chainIndex, merchantIndex]
|
||
},
|
||
async openStoreMerchantPicker(index) {
|
||
if (this.isAuditEntry) return
|
||
if (!Array.isArray(this.merchantChainList) || !this.merchantChainList.length) {
|
||
uni.showLoading({
|
||
title: '加载门店列表中...',
|
||
})
|
||
await this.getMerchantList()
|
||
uni.hideLoading()
|
||
}
|
||
const item = this.storeList[index] || this.createStoreItem()
|
||
this.activeStorePickerIndex = index
|
||
this.merchantSearchKeyword = ''
|
||
this.merchantPickerValue = this.getMerchantPickerIndexesByStore(item)
|
||
this.showMerchantPicker = true
|
||
},
|
||
closeMerchantPicker() {
|
||
this.showMerchantPicker = false
|
||
this.activeStorePickerIndex = -1
|
||
},
|
||
onMerchantPickerChange(e) {
|
||
const value = e.detail && e.detail.value !== undefined ? e.detail.value : e.detail
|
||
const currentChainIndex = Number(this.merchantPickerValue[0] || 0)
|
||
let chainIndex = Array.isArray(value) ? Number(value[0] || 0) : Number(value || 0)
|
||
let merchantIndex = Array.isArray(value) ? Number(value[1] || 0) : 0
|
||
if (!Number.isFinite(chainIndex) || chainIndex < 0) chainIndex = 0
|
||
if (!Number.isFinite(merchantIndex) || merchantIndex < 0) merchantIndex = 0
|
||
const chain = this.merchantChainOptions[chainIndex] || this.merchantChainOptions[0] || {}
|
||
const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
|
||
if (!merchants.length) {
|
||
merchantIndex = 0
|
||
} else if (chainIndex !== currentChainIndex || merchantIndex > merchants.length - 1) {
|
||
merchantIndex = 0
|
||
}
|
||
this.merchantPickerValue = [chainIndex, merchantIndex]
|
||
},
|
||
confirmMerchantPicker() {
|
||
const index = this.activeStorePickerIndex
|
||
if (index < 0) {
|
||
this.closeMerchantPicker()
|
||
return
|
||
}
|
||
const chainIndex = Number(this.merchantPickerValue[0] || 0)
|
||
const merchantIndex = Number(this.merchantPickerValue[1] || 0)
|
||
const chain = this.merchantChainOptions[chainIndex] || null
|
||
const merchants = chain && Array.isArray(chain.merchants) ? chain.merchants : []
|
||
const selected = merchants[merchantIndex] || null
|
||
if (!selected || selected.id === '' || selected.id === null || selected.id === undefined) return
|
||
const current = this.storeList[index] || this.createStoreItem()
|
||
this.$set(this.storeList, index, Object.assign({}, current, this.buildStoreMerchantFields(Object.assign({}, selected, {
|
||
chainId: selected.chainId || (chain && chain.id) || '',
|
||
chainName: selected.chainName || (chain && chain.text) || ''
|
||
}), false)))
|
||
this.closeMerchantPicker()
|
||
this.scheduleSave()
|
||
},
|
||
async addStoreItem() {
|
||
const limit = this.safeNumber(this.task && this.task.storeNum)
|
||
if (limit && this.storeList.length >= limit) {
|
||
this.toast(`最多新增${limit}家门店`)
|
||
return
|
||
}
|
||
const merchantList = this.flattenMerchantsFromChains(this.merchantChainList)
|
||
const nearest = this.findNearestMerchant(merchantList)
|
||
const target = nearest || merchantList[0] || null
|
||
this.storeList.push(this.createStoreItem(target, true))
|
||
const index = this.storeList.length - 1
|
||
await this.fetchStoreLocation(index, true)
|
||
this.scheduleSave()
|
||
},
|
||
removeStoreItem(index) {
|
||
this.storeList.splice(index, 1)
|
||
if (!this.storeList.length) {
|
||
this.ensureDefaultStoreList()
|
||
}
|
||
this.save({silent: true})
|
||
},
|
||
async fetchStoreLocation(index, silent = false) {
|
||
if (this.isAuditEntry) return
|
||
if (!silent) {
|
||
uni.showLoading({
|
||
title: '定位中'
|
||
})
|
||
}
|
||
try {
|
||
const res = await getLocationOnly()
|
||
const current = this.storeList[index] || this.createStoreItem()
|
||
this.$set(this.storeList, index, Object.assign({}, current, {
|
||
lat: res.latitude,
|
||
lng: res.longitude,
|
||
location: res.full || [res.province_str, res.city_str, res.area_str].filter(Boolean).join(' ')
|
||
}))
|
||
await this.save({silent: true})
|
||
} catch (error) {
|
||
if (!silent) {
|
||
this.toast('定位失败')
|
||
}
|
||
} finally {
|
||
if (!silent) uni.hideLoading()
|
||
}
|
||
},
|
||
async uploadStorePhotos(index) {
|
||
if (!this.isLoggedIn) return this.$refs.Login.open()
|
||
uni.showLoading({
|
||
title: '上传中'
|
||
})
|
||
const current = this.storeList[index] || this.createStoreItem()
|
||
const photos = Array.isArray(current.photos) ? current.photos : []
|
||
const remain = 9 - photos.length
|
||
if (remain <= 0) {
|
||
uni.hideLoading()
|
||
return
|
||
}
|
||
const res = await chooseImg(remain, ['camera']).catch(() => {
|
||
uni.hideLoading()
|
||
})
|
||
if (!res || !res.length) {
|
||
uni.hideLoading()
|
||
return
|
||
}
|
||
const location = await getLocationOnly().catch(() => null)
|
||
const now = new Date()
|
||
const time = this.formatTime(now)
|
||
const date = this.formatDate(now)
|
||
const address = location
|
||
? `${location.province_str}${location.city_str}${location.area_str}`
|
||
: (current.location || this.currentAddress || '当前地址')
|
||
const nextPhotos = photos.concat(
|
||
res
|
||
.map(item => item && item.path ? this.buildWatermarkImageUrl(item.path, address, date, time) : '')
|
||
.filter(Boolean)
|
||
)
|
||
const nextStore = Object.assign({}, current, {
|
||
photos: nextPhotos
|
||
})
|
||
if (location) {
|
||
nextStore.lat = location.latitude
|
||
nextStore.lng = location.longitude
|
||
nextStore.location = location.full || [location.province_str, location.city_str, location.area_str].filter(Boolean).join(' ')
|
||
}
|
||
this.$set(this.storeList, index, nextStore)
|
||
uni.hideLoading()
|
||
await this.save({silent: true})
|
||
},
|
||
removeStorePhoto(storeIndex, photoIndex) {
|
||
const current = this.storeList[storeIndex] || this.createStoreItem()
|
||
const photos = Array.isArray(current.photos) ? [...current.photos] : []
|
||
photos.splice(photoIndex, 1)
|
||
this.$set(this.storeList, storeIndex, Object.assign({}, current, {photos}))
|
||
this.save({silent: true})
|
||
},
|
||
previewStorePhotos(storeIndex, photoIndex = 0) {
|
||
const current = this.storeList[storeIndex] || {}
|
||
const photos = Array.isArray(current.photos) ? current.photos.filter(Boolean) : []
|
||
if (!photos.length) return
|
||
this.previewImages(photos, photos[photoIndex] || photos[0])
|
||
},
|
||
isStoreItemCompleted(item) {
|
||
const photos = Array.isArray(item && item.photos) ? item.photos.filter(Boolean) : []
|
||
return !!(item && item.merchantId && item.location && item.lat && item.lng && photos.length)
|
||
},
|
||
pickDetailValue(keys, fallback) {
|
||
const task = this.task || {};
|
||
for (const key of keys) {
|
||
const text = this.formatDetailValue(task[key]);
|
||
if (text) return text;
|
||
}
|
||
return fallback;
|
||
},
|
||
formatDetailValue(value) {
|
||
if (value === 0) return '0';
|
||
if (!value) return '';
|
||
if (Array.isArray(value)) {
|
||
const list = [];
|
||
value.forEach(item => {
|
||
if (!item) return;
|
||
if (typeof item === 'string') {
|
||
list.push(item);
|
||
return;
|
||
}
|
||
if (Array.isArray(item)) {
|
||
item.forEach(child => {
|
||
if (child) list.push(String(child));
|
||
});
|
||
return;
|
||
}
|
||
const children = Array.isArray(item.children) ? item.children : [];
|
||
if (children.length) {
|
||
children.forEach(child => {
|
||
const name = child.title || child.name || child.goodsName || child.label || '';
|
||
if (name) list.push(name);
|
||
});
|
||
return;
|
||
}
|
||
const name = item.title || item.name || item.label || item.goodsName || item.spec || '';
|
||
if (name) list.push(name);
|
||
});
|
||
return list.filter(Boolean).join('、');
|
||
}
|
||
if (typeof value === 'object') {
|
||
const children = Array.isArray(value.children) ? value.children : [];
|
||
if (children.length) {
|
||
return children
|
||
.map(child => child.title || child.name || child.goodsName || child.label || '')
|
||
.filter(Boolean)
|
||
.join('、');
|
||
}
|
||
const name = value.title || value.name || value.label || value.goodsName;
|
||
if (name) return String(name);
|
||
}
|
||
return String(value);
|
||
},
|
||
trimSecond(value) {
|
||
const text = value === null || value === undefined ? '' : String(value).trim()
|
||
if (!text) return ''
|
||
return text.replace(/(\d{2}:\d{2}):\d{2}\b/, '$1')
|
||
},
|
||
formatCoordinate(value) {
|
||
const num = Number(value)
|
||
return Number.isFinite(num) ? num.toFixed(6) : '暂无'
|
||
},
|
||
previewImages(urls = [], current = '') {
|
||
const list = Array.isArray(urls) ? urls.filter(Boolean) : []
|
||
if (!list.length) return
|
||
uni.previewImage({
|
||
urls: list,
|
||
current: current || list[0]
|
||
})
|
||
},
|
||
handleClockAction(index) {
|
||
if (!this.isAuditEntry) {
|
||
this.uploadClock(index)
|
||
return
|
||
}
|
||
const list = Array.isArray(this.clockPhotos)
|
||
? this.clockPhotos.map(item => item && item.url).filter(Boolean)
|
||
: []
|
||
if (!list.length) return
|
||
const current = this.clockPhotos[index] && this.clockPhotos[index].url ? this.clockPhotos[index].url : list[0]
|
||
this.previewImages(list, current)
|
||
},
|
||
handleBoothAction(index) {
|
||
if (!this.isAuditEntry) return
|
||
const list = Array.isArray(this.boothPhotos) ? this.boothPhotos.filter(Boolean) : []
|
||
if (!list.length) return
|
||
const current = list[index] || list[0]
|
||
this.previewImages(list, current)
|
||
},
|
||
handleReceiptAction(index) {
|
||
if (!this.isAuditEntry) return
|
||
const list = Array.isArray(this.receiptPhotos) ? this.receiptPhotos.filter(Boolean) : []
|
||
if (!list.length) return
|
||
const current = list[index] || list[0]
|
||
this.previewImages(list, current)
|
||
},
|
||
async uploadClock(index) {
|
||
if (!this.isLoggedIn) return this.$refs.Login.open()
|
||
uni.showLoading({
|
||
title: '上传中'
|
||
})
|
||
const res = await chooseImg(1, ['camera']).catch(() => {
|
||
uni.hideLoading()
|
||
})
|
||
const now = new Date()
|
||
const time = this.formatTime(now)
|
||
const date = this.formatDate(now)
|
||
const next = res[0] && res[0].path ? res[0].path : ''
|
||
if (next) {
|
||
const location = await getLocationOnly()
|
||
if (location) {
|
||
const address = `${location.province_str}${location.city_str}${location.area_str}`
|
||
let url = `${next}?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`
|
||
this.$set(this.clockPhotos, index, Object.assign({}, this.clockPhotos[index], {
|
||
url,
|
||
time,
|
||
date
|
||
}))
|
||
}
|
||
}
|
||
uni.hideLoading()
|
||
await this.save({isClock: true, clockIndex: index, silent: true})
|
||
},
|
||
makeTextBase64(text) {
|
||
let 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 uploadBooth() {
|
||
if (!this.isLoggedIn) return this.$refs.Login.open()
|
||
const remain = this.boothLimit - this.boothPhotos.length
|
||
if (remain <= 0) return
|
||
const res = await chooseImg(remain)
|
||
this.boothPhotos.push(...res.map(i => i.path))
|
||
await this.save()
|
||
},
|
||
removeBooth(idx) {
|
||
this.boothPhotos.splice(idx, 1)
|
||
this.save()
|
||
},
|
||
async uploadReceipt() {
|
||
if (!this.isLoggedIn) return this.$refs.Login.open()
|
||
const remain = this.receiptLimit - this.receiptPhotos.length
|
||
if (remain <= 0) return
|
||
const res = await chooseImg(remain)
|
||
const next = res.map(item => item.path).filter(Boolean)
|
||
if (next.length) {
|
||
this.receiptPhotos.push(...next)
|
||
}
|
||
await this.save()
|
||
},
|
||
removeReceipt(index) {
|
||
this.receiptPhotos.splice(index, 1)
|
||
this.save()
|
||
},
|
||
scheduleSave() {
|
||
if (this.isAuditEntry) return
|
||
if (!this.task || !this.task.userTask) return
|
||
if (this.task.userTask.status === 1 || this.task.userTask.status === 2) return
|
||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
|
||
this.autoSaveTimer = setTimeout(() => {
|
||
this.autoSaveTimer = null
|
||
this.flushAutoSave()
|
||
}, 800)
|
||
},
|
||
async flushAutoSave() {
|
||
if (this.autoSaveTimer) {
|
||
clearTimeout(this.autoSaveTimer)
|
||
this.autoSaveTimer = null
|
||
}
|
||
if (this.isAuditEntry) return
|
||
if (!this.task || !this.task.userTask) return
|
||
if (this.task.userTask.status === 1 || this.task.userTask.status === 2) return
|
||
if (this.autoSaving) {
|
||
this.autoSavePending = true
|
||
return
|
||
}
|
||
this.autoSaving = true
|
||
try {
|
||
await this.save({silent: true})
|
||
} finally {
|
||
this.autoSaving = false
|
||
if (this.autoSavePending) {
|
||
this.autoSavePending = false
|
||
this.scheduleSave()
|
||
}
|
||
}
|
||
},
|
||
toggleGroup(index) {
|
||
const next = !this.salesGroups[index].open
|
||
this.$set(this.salesGroups, index, Object.assign({}, this.salesGroups[index], {open: next}))
|
||
},
|
||
toggleDetailExpanded() {
|
||
this.detailExpanded = !this.detailExpanded
|
||
},
|
||
toggleItem(groupIdx, itemIdx) {
|
||
const item = this.salesGroups[groupIdx].items[itemIdx]
|
||
this.$set(this.salesGroups[groupIdx].items, itemIdx, Object.assign({}, item, {checked: !item.checked}))
|
||
this.scheduleSave()
|
||
},
|
||
onSpecChange(event, groupIdx, itemIdx) {
|
||
const value = Number(event.detail.value || 0)
|
||
const item = this.salesGroups[groupIdx].items[itemIdx]
|
||
const nextSpec = item.specOptions[value] || item.spec
|
||
this.$set(this.salesGroups[groupIdx].items, itemIdx, Object.assign({}, item, {spec: nextSpec}))
|
||
this.scheduleSave()
|
||
},
|
||
toast(message, title = '提示') {
|
||
const content = message === undefined || message === null ? '' : String(message)
|
||
return new Promise(resolve => {
|
||
uni.showModal({
|
||
title,
|
||
content,
|
||
showCancel: false,
|
||
success: resolve,
|
||
fail: resolve
|
||
})
|
||
})
|
||
},
|
||
buildAuditBasePayload() {
|
||
const userTask = this.task && this.task.userTask ? this.task.userTask : {}
|
||
const id = this.auditId || userTask.id || ''
|
||
const taskId = this.auditTaskId || this.id || this.task.id || ''
|
||
const userId = this.auditUserId || userTask.userId || userTask.user_id || this.task.userId || ''
|
||
if (id === '' || id === null || id === undefined) {
|
||
this.toast('记录ID缺失')
|
||
return null
|
||
}
|
||
if (taskId === '' || taskId === null || taskId === undefined) {
|
||
this.toast('任务ID缺失')
|
||
return null
|
||
}
|
||
if (userId === '' || userId === null || userId === undefined) {
|
||
this.toast('用户ID缺失')
|
||
return null
|
||
}
|
||
return {
|
||
id,
|
||
taskId,
|
||
userId
|
||
}
|
||
},
|
||
async submitAuditAction(status, auditReason = '') {
|
||
if (!this.isAuditEntry || this.auditSubmitting) return
|
||
const base = this.buildAuditBasePayload()
|
||
if (!base) return
|
||
const payload = {
|
||
...base,
|
||
status
|
||
}
|
||
if (Number(status) === 3) {
|
||
payload.auditReason = auditReason
|
||
}
|
||
this.auditSubmitting = true
|
||
try {
|
||
await changeUserTaskReq(payload)
|
||
this.auditStatus = Number(status)
|
||
if (this.task && this.task.userTask) {
|
||
this.$set(this.task, 'userTask', Object.assign({}, this.task.userTask, {status: Number(status)}))
|
||
}
|
||
await this.toast('操作成功')
|
||
uni.navigateBack()
|
||
} catch (error) {
|
||
this.toast('操作失败')
|
||
} finally {
|
||
this.auditSubmitting = false
|
||
}
|
||
},
|
||
async handleAuditApprove() {
|
||
await this.submitAuditAction(2)
|
||
},
|
||
openAuditRejectPopup() {
|
||
this.auditRejectReason = ''
|
||
this.showAuditRejectPopup = true
|
||
},
|
||
closeAuditRejectPopup() {
|
||
this.showAuditRejectPopup = false
|
||
this.auditRejectReason = ''
|
||
},
|
||
async submitAuditRejectReason() {
|
||
const reason = (this.auditRejectReason || '').trim()
|
||
if (!reason) {
|
||
this.toast('请输入拒绝原因')
|
||
return
|
||
}
|
||
await this.submitAuditAction(3, reason)
|
||
this.closeAuditRejectPopup()
|
||
},
|
||
unwrapAuditUserList(data) {
|
||
if (Array.isArray(data)) return data
|
||
if (!data || typeof data !== 'object') return []
|
||
const keys = ['list', 'records', 'rows', 'items', 'data', 'result']
|
||
for (let i = 0; i < keys.length; i++) {
|
||
if (Array.isArray(data[keys[i]])) return data[keys[i]]
|
||
}
|
||
return []
|
||
},
|
||
normalizeAuditUser(item) {
|
||
const id = item.id ?? item.userId ?? item.uid
|
||
const nickname = item.nickname || item.nickName || ''
|
||
const realName = item.realName || item.real_name || item.name || ''
|
||
const phone = item.phone || item.mobile || item.tel || ''
|
||
const label = realName || nickname || phone || `用户${id || ''}`
|
||
return {
|
||
id,
|
||
label,
|
||
phone
|
||
}
|
||
},
|
||
openAuditCopyPopup() {
|
||
this.auditCopyKeyword = ''
|
||
this.auditCopyUsers = []
|
||
this.auditSelectedCopyUserId = ''
|
||
this.showAuditCopyPopup = true
|
||
},
|
||
closeAuditCopyPopup() {
|
||
this.showAuditCopyPopup = false
|
||
},
|
||
async searchAuditCopyUsers() {
|
||
const keyword = (this.auditCopyKeyword || '').trim()
|
||
if (!keyword) {
|
||
this.toast('请输入keyword')
|
||
return
|
||
}
|
||
this.auditCopySearching = true
|
||
try {
|
||
const {data} = await userSearchReq({keywords: keyword})
|
||
const list = this.unwrapAuditUserList(data)
|
||
this.auditCopyUsers = list
|
||
.map(item => this.normalizeAuditUser(item))
|
||
.filter(item => item.id !== undefined && item.id !== null && String(item.id).trim() !== '')
|
||
this.auditSelectedCopyUserId = this.auditCopyUsers[0] ? this.auditCopyUsers[0].id : ''
|
||
} catch (error) {
|
||
this.auditCopyUsers = []
|
||
this.toast('搜索失败')
|
||
} finally {
|
||
this.auditCopySearching = false
|
||
}
|
||
},
|
||
async submitAuditCopy() {
|
||
const targetUserId = this.auditSelectedCopyUserId
|
||
if (targetUserId === '' || targetUserId === null || targetUserId === undefined) {
|
||
this.toast('请选择用户')
|
||
return
|
||
}
|
||
const base = this.buildAuditBasePayload()
|
||
if (!base) return
|
||
try {
|
||
await copyUserTaskReq({
|
||
id: base.id,
|
||
taskId: base.taskId,
|
||
sourceUserId: base.userId,
|
||
userId: targetUserId,
|
||
isCopy: 1
|
||
})
|
||
this.toast('抄送成功')
|
||
this.closeAuditCopyPopup()
|
||
} catch (error) {
|
||
this.toast('抄送失败')
|
||
}
|
||
},
|
||
validateSubmit() {
|
||
if (this.isStoreTask) {
|
||
const list = Array.isArray(this.storeList) ? this.storeList : []
|
||
if (!list.length) {
|
||
this.toast('请至少新增一条巡店记录')
|
||
return false
|
||
}
|
||
for (let idx = 0; idx < list.length; idx += 1) {
|
||
const item = list[idx]
|
||
if (!item || !item.merchantId) {
|
||
this.toast(`请选择巡店${idx + 1}的门店`)
|
||
return false
|
||
}
|
||
if (!item.location || !item.lat || !item.lng) {
|
||
this.toast(`请完成巡店${idx + 1}定位`)
|
||
return false
|
||
}
|
||
const photos = Array.isArray(item.photos) ? item.photos.filter(Boolean) : []
|
||
if (!photos.length) {
|
||
this.toast(`请上传巡店${idx + 1}照片`)
|
||
return false
|
||
}
|
||
}
|
||
const summaryText = (this.summary || '').trim()
|
||
if (!summaryText) {
|
||
this.toast('请填写活动小结')
|
||
return false
|
||
}
|
||
const targetStoreNum = this.safeNumber(this.task && this.task.storeNum)
|
||
const completed = list.filter(item => this.isStoreItemCompleted(item)).length
|
||
if (targetStoreNum && completed < targetStoreNum) {
|
||
this.toast(`请完成${targetStoreNum}家门店巡店`)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
const clockList = Array.isArray(this.clockPhotos) ? this.clockPhotos : []
|
||
if (!clockList[1] || !clockList[1].url) {
|
||
this.toast('请至少完成下班打卡')
|
||
return false
|
||
}
|
||
if (!this.boothPhotos || this.boothPhotos.length === 0) {
|
||
this.toast('请上传门头摆台照')
|
||
return false
|
||
}
|
||
// if (!this.receiptPhotos.length) {
|
||
// this.toast('请上传小票原图')
|
||
// return false
|
||
// }
|
||
const summaryText = (this.summary || '').trim()
|
||
if (!summaryText) {
|
||
this.toast('请填写活动小结')
|
||
return false
|
||
}
|
||
const groups = Array.isArray(this.salesGroups) ? this.salesGroups : []
|
||
if (!groups.length) {
|
||
this.toast(this.manualSalesMode ? '请至少添加一项销量商品' : '暂无销量填写项')
|
||
return false
|
||
}
|
||
let hasChecked = false
|
||
for (let gIdx = 0; gIdx < groups.length; gIdx += 1) {
|
||
const items = Array.isArray(groups[gIdx].items) ? groups[gIdx].items : []
|
||
for (let iIdx = 0; iIdx < items.length; iIdx += 1) {
|
||
const item = items[iIdx]
|
||
if (item) {
|
||
// hasChecked = true
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// if (!hasChecked) {
|
||
// this.toast('请选择销量产品')
|
||
// return false
|
||
// }
|
||
return true
|
||
},
|
||
fillEmptySalesQtyWithZero() {
|
||
const groups = Array.isArray(this.salesGroups) ? this.salesGroups : []
|
||
groups.forEach((group, gIdx) => {
|
||
const items = Array.isArray(group.items) ? group.items : []
|
||
items.forEach((item, iIdx) => {
|
||
if (!item) return
|
||
const qty = item.qty
|
||
const emptyQty = qty === '' || qty === null || qty === undefined || (typeof qty === 'string' && qty.trim() === '')
|
||
if (!emptyQty) return
|
||
this.$set(this.salesGroups[gIdx].items, iIdx, Object.assign({}, item, {qty: 0}))
|
||
})
|
||
})
|
||
},
|
||
buildSalesGroups(list) {
|
||
const groups = Array.isArray(list) ? list : []
|
||
const {idTotals, nameTotals} = this.buildTaskNumMaps()
|
||
return groups.map((group, index) => {
|
||
const itemsRaw = Array.isArray(group.children) ? group.children : []
|
||
const items = itemsRaw.map(item => {
|
||
const specOptions = this.normalizeSpecOptions(item)
|
||
const rawSpec = item.spec || item.specification || item.standard || ''
|
||
const spec = rawSpec || (specOptions[0] || '规格')
|
||
const safeOptions = specOptions.length ? specOptions : [spec]
|
||
const taskNum = this.getTaskNumFromMaps(item, idTotals, nameTotals)
|
||
return {
|
||
productId: this.getSalesItemId(item),
|
||
name: this.getSalesItemName(item) || '药品',
|
||
spec,
|
||
specOptions: safeOptions,
|
||
qty: item.qty !== undefined && item.qty !== null
|
||
? item.qty
|
||
: (item.num !== undefined && item.num !== null ? item.num : ''),
|
||
unit: item.unit || '盒',
|
||
checked: !!item.checked,
|
||
taskNum,
|
||
taskIncome: this.resolveTaskIncome(item),
|
||
}
|
||
})
|
||
return {
|
||
title: group.makerAbb || group.title || group.name || `分组${index + 1}`,
|
||
open: true,
|
||
items
|
||
}
|
||
}).filter(group => group.items.length)
|
||
},
|
||
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 || categoryNode.makerAbb || categoryNode.title || categoryNode.name
|
||
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
|
||
},
|
||
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 || ''
|
||
return {
|
||
productId,
|
||
goodsCode: code,
|
||
name,
|
||
makerAbb: maker,
|
||
spec,
|
||
unit: item.unit || product.unit || '盒',
|
||
taskIncome: this.resolveTaskIncome(item),
|
||
specOptions: this.normalizeSpecOptions(Object.assign({}, product, item)),
|
||
qty: ''
|
||
}
|
||
},
|
||
buildSalesGroupsFromProductData(rawData) {
|
||
const tabs = this.buildCategoryTabs(rawData)
|
||
let rowSeed = 0
|
||
return 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 Object.assign({}, 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)
|
||
},
|
||
async getProductGroups() {
|
||
const chainName = this.productChainName
|
||
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.productChainName) {
|
||
uni.showToast({
|
||
title: '当前任务缺少连锁信息',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
if (this.productLoadedChainName !== this.productChainName || !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) {
|
||
const groups = Array.isArray(this.salesGroups) ? this.salesGroups : []
|
||
return groups.some(group => {
|
||
const items = Array.isArray(group.items) ? group.items : []
|
||
return items.some(item => {
|
||
if (product.productId && item.productId) {
|
||
return String(product.productId) === String(item.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 || '规格']
|
||
const item = Object.assign({}, product, {
|
||
groupTitle: product.groupTitle || '手动添加',
|
||
specOptions,
|
||
spec: product.spec || specOptions[0] || '规格',
|
||
qty: ''
|
||
})
|
||
const groupIndex = this.salesGroups.findIndex(group => group && group.manual)
|
||
if (groupIndex >= 0) {
|
||
const group = this.salesGroups[groupIndex]
|
||
const items = Array.isArray(group.items) ? group.items.concat(item) : [item]
|
||
this.$set(this.salesGroups, groupIndex, Object.assign({}, group, {items, open: true}))
|
||
} else {
|
||
this.salesGroups.push({
|
||
title: '手动添加',
|
||
open: true,
|
||
manual: true,
|
||
items: [item]
|
||
})
|
||
}
|
||
this.showProductPicker = false
|
||
this.salesKeyword = ''
|
||
this.scheduleSave()
|
||
},
|
||
removeManualSalesItem(groupIdx, itemIdx) {
|
||
const group = this.salesGroups[groupIdx]
|
||
if (!group || !Array.isArray(group.items)) return
|
||
const items = group.items.slice()
|
||
items.splice(itemIdx, 1)
|
||
if (items.length) {
|
||
this.$set(this.salesGroups, groupIdx, Object.assign({}, group, {items}))
|
||
} else {
|
||
this.salesGroups.splice(groupIdx, 1)
|
||
}
|
||
this.scheduleSave()
|
||
},
|
||
attachTaskNumToSalesGroups(groups, sourceGroups = []) {
|
||
const {idTotals, nameTotals} = this.buildTaskNumMaps()
|
||
const {idIncomeMap, nameIncomeMap} = this.buildTaskIncomeMaps(sourceGroups)
|
||
return groups.map(group => {
|
||
const items = Array.isArray(group.items) ? group.items : []
|
||
const nextItems = items.map(item => {
|
||
const taskNum = item && item.taskNum ? item.taskNum : this.getTaskNumFromMaps(item || {}, idTotals, nameTotals)
|
||
const taskIncome = this.hasDisplayValue(this.resolveTaskIncome(item))
|
||
? this.resolveTaskIncome(item)
|
||
: this.getTaskIncomeFromMaps(item || {}, idIncomeMap, nameIncomeMap)
|
||
return Object.assign({}, item, {taskNum, taskIncome})
|
||
})
|
||
return Object.assign({}, group, {items: nextItems})
|
||
})
|
||
},
|
||
mergeSalesGroupsWithProductGroups(savedGroups = [], sourceGroups = []) {
|
||
const saved = this.attachTaskNumToSalesGroups(Array.isArray(savedGroups) ? savedGroups : [], sourceGroups)
|
||
const base = this.buildSalesGroups(sourceGroups)
|
||
if (!base.length) return saved
|
||
if (!saved.length) return base
|
||
|
||
const matchedSavedGroupIndexes = new Set()
|
||
const merged = base.map((baseGroup, index) => {
|
||
const savedGroup = this.findSavedSalesGroup(baseGroup, saved, index, matchedSavedGroupIndexes)
|
||
if (!savedGroup) return baseGroup
|
||
const savedItemMaps = this.buildSavedSalesItemMaps(savedGroup.items)
|
||
const items = baseGroup.items.map(baseItem => {
|
||
const savedItem = this.findSavedSalesItem(baseItem, savedItemMaps)
|
||
return savedItem ? this.mergeSalesItem(baseItem, savedItem) : baseItem
|
||
})
|
||
return Object.assign({}, baseGroup, {
|
||
title: savedGroup.title || baseGroup.title,
|
||
open: savedGroup.open !== undefined ? savedGroup.open : baseGroup.open,
|
||
items
|
||
})
|
||
})
|
||
|
||
saved.forEach((group, index) => {
|
||
if (!matchedSavedGroupIndexes.has(index)) merged.push(group)
|
||
})
|
||
return this.attachTaskNumToSalesGroups(merged, sourceGroups)
|
||
},
|
||
findSavedSalesGroup(baseGroup, savedGroups, baseIndex, matchedIndexes) {
|
||
const title = String(baseGroup && baseGroup.title ? baseGroup.title : '').trim()
|
||
if (title) {
|
||
const titleIndex = savedGroups.findIndex((group, index) => {
|
||
if (matchedIndexes.has(index)) return false
|
||
return String(group && group.title ? group.title : '').trim() === title
|
||
})
|
||
if (titleIndex >= 0) {
|
||
matchedIndexes.add(titleIndex)
|
||
return savedGroups[titleIndex]
|
||
}
|
||
}
|
||
if (savedGroups[baseIndex] && !matchedIndexes.has(baseIndex)) {
|
||
matchedIndexes.add(baseIndex)
|
||
return savedGroups[baseIndex]
|
||
}
|
||
return null
|
||
},
|
||
buildSavedSalesItemMaps(items = []) {
|
||
const byId = new Map()
|
||
const byNameSpec = new Map()
|
||
const byName = new Map()
|
||
;(Array.isArray(items) ? items : []).forEach(item => {
|
||
if (!item) return
|
||
const id = this.getSalesItemId(item)
|
||
if (id !== undefined && id !== null && String(id).trim() !== '') {
|
||
byId.set(String(id), item)
|
||
}
|
||
const name = this.getSalesItemName(item)
|
||
const spec = item.spec || ''
|
||
if (name && spec) byNameSpec.set(`${name}__${spec}`, item)
|
||
if (name && !byName.has(name)) byName.set(name, item)
|
||
})
|
||
return {byId, byNameSpec, byName}
|
||
},
|
||
findSavedSalesItem(baseItem, maps) {
|
||
const id = this.getSalesItemId(baseItem)
|
||
if (id !== undefined && id !== null) {
|
||
const key = String(id)
|
||
if (maps.byId.has(key)) return maps.byId.get(key)
|
||
}
|
||
const name = this.getSalesItemName(baseItem)
|
||
const spec = baseItem && baseItem.spec ? baseItem.spec : ''
|
||
if (name && spec) {
|
||
const key = `${name}__${spec}`
|
||
if (maps.byNameSpec.has(key)) return maps.byNameSpec.get(key)
|
||
}
|
||
return name && maps.byName.has(name) ? maps.byName.get(name) : null
|
||
},
|
||
mergeSalesItem(baseItem, savedItem) {
|
||
const specOptions = Array.isArray(savedItem.specOptions) && savedItem.specOptions.length
|
||
? savedItem.specOptions
|
||
: baseItem.specOptions
|
||
return Object.assign({}, baseItem, savedItem, {
|
||
productId: baseItem.productId || savedItem.productId,
|
||
name: baseItem.name || savedItem.name,
|
||
spec: savedItem.spec || baseItem.spec,
|
||
specOptions,
|
||
unit: savedItem.unit || baseItem.unit,
|
||
qty: savedItem.qty !== undefined && savedItem.qty !== null ? savedItem.qty : baseItem.qty
|
||
})
|
||
},
|
||
getTaskNumFromMaps(item, idTotals, nameTotals) {
|
||
const id = this.getSalesItemId(item)
|
||
if (id !== undefined && id !== null) {
|
||
const total = idTotals.get(String(id))
|
||
if (total) return total
|
||
}
|
||
const name = this.getSalesItemName(item)
|
||
return name ? nameTotals.get(name) || 0 : 0
|
||
},
|
||
hasDisplayValue(value) {
|
||
return value !== undefined && value !== null && String(value).trim() !== ''
|
||
},
|
||
getSalesItemId(item) {
|
||
const product = item && item.product ? item.product : {}
|
||
return item.productId || item.id || item.goodsId || item.skuId || product.productId || product.id || ''
|
||
},
|
||
getSalesItemName(item) {
|
||
const product = item && item.product ? item.product : {}
|
||
return item.title || item.name || item.goodsName || item.productName || product.title || product.name || product.goodsName || ''
|
||
},
|
||
resolveTaskIncome(item) {
|
||
if (!item || typeof item !== 'object') return ''
|
||
const product = item.product && typeof item.product === 'object' ? item.product : {}
|
||
const candidates = [
|
||
item.taskIncome,
|
||
item.income,
|
||
item.productIncome,
|
||
item.reward,
|
||
item.bonus,
|
||
item.amount,
|
||
item.value,
|
||
product.taskIncome,
|
||
product.income,
|
||
product.productIncome,
|
||
product.reward
|
||
]
|
||
for (let i = 0; i < candidates.length; i += 1) {
|
||
const value = candidates[i]
|
||
if (this.hasDisplayValue(value)) return value
|
||
}
|
||
return ''
|
||
},
|
||
buildTaskIncomeMaps(groups) {
|
||
const idIncomeMap = new Map()
|
||
const nameIncomeMap = new Map()
|
||
;(Array.isArray(groups) ? groups : []).forEach(group => {
|
||
const items = Array.isArray(group && group.children) ? group.children : (Array.isArray(group && group.items) ? group.items : [])
|
||
items.forEach(item => {
|
||
const income = this.resolveTaskIncome(item)
|
||
if (!this.hasDisplayValue(income)) return
|
||
const id = this.getSalesItemId(item)
|
||
if (id !== undefined && id !== null && String(id).trim() !== '') {
|
||
idIncomeMap.set(String(id), income)
|
||
}
|
||
const name = this.getSalesItemName(item)
|
||
if (name) {
|
||
nameIncomeMap.set(name, income)
|
||
}
|
||
})
|
||
})
|
||
return {idIncomeMap, nameIncomeMap}
|
||
},
|
||
getTaskIncomeFromMaps(item, idIncomeMap, nameIncomeMap) {
|
||
const id = this.getSalesItemId(item)
|
||
if (id !== undefined && id !== null) {
|
||
const key = String(id)
|
||
if (idIncomeMap.has(key)) return idIncomeMap.get(key)
|
||
}
|
||
const name = this.getSalesItemName(item)
|
||
if (name && nameIncomeMap.has(name)) {
|
||
return nameIncomeMap.get(name)
|
||
}
|
||
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)
|
||
return Number.isFinite(num) ? num : String(value)
|
||
},
|
||
calcTotalTaskNum() {
|
||
if (this.task.cateId === 4) {
|
||
return this.safeNumber(this.task.storeNum) || (Array.isArray(this.storeList) ? this.storeList.length : 0)
|
||
}
|
||
const list = Array.isArray(this.task && this.task.productList) ? this.task.productList : []
|
||
if (list.length) {
|
||
return list.reduce((total, item) => {
|
||
const raw = item ? (item.num || item.taskNum || item.quantity || item.count || item.targetNum) : 0
|
||
return total + this.safeNumber(raw)
|
||
}, 0)
|
||
}
|
||
const map = new Map()
|
||
const groups = Array.isArray(this.salesGroups) ? this.salesGroups : []
|
||
groups.forEach(group => {
|
||
const items = Array.isArray(group.items) ? group.items : []
|
||
items.forEach(item => {
|
||
if (!item || !item.taskNum) return
|
||
const key = item.productId ? `id:${item.productId}` : `name:${item.name || ''}`
|
||
if (!key) return
|
||
const current = map.get(key) || 0
|
||
map.set(key, Math.max(current, this.safeNumber(item.taskNum)))
|
||
})
|
||
})
|
||
let total = 0
|
||
map.forEach(value => {
|
||
total += value
|
||
})
|
||
return total
|
||
},
|
||
calcTotalQty() {
|
||
if (this.isStoreTask) {
|
||
const list = Array.isArray(this.storeList) ? this.storeList : []
|
||
return list.filter(item => this.isStoreItemCompleted(item)).length
|
||
}
|
||
const groups = Array.isArray(this.salesGroups) ? this.salesGroups : []
|
||
return groups.reduce((total, group) => {
|
||
const items = Array.isArray(group.items) ? group.items : []
|
||
const sum = items.reduce((acc, item) => acc + this.safeNumber(item ? item.qty : 0), 0)
|
||
return total + sum
|
||
}, 0)
|
||
},
|
||
buildTaskNumMaps() {
|
||
const list = Array.isArray(this.task && this.task.productList) ? this.task.productList : []
|
||
const idTotals = new Map()
|
||
const nameTotals = new Map()
|
||
list.forEach(item => {
|
||
if (!item) return
|
||
const product = item.product || item.goods || {}
|
||
const rawNum = item.num || item.taskNum || item.quantity || item.count || item.targetNum
|
||
const num = this.safeNumber(rawNum)
|
||
if (!num) return
|
||
const id = item.productId || product.id || product.productId || item.goodsId || item.skuId || item.id
|
||
if (id !== undefined && id !== null) {
|
||
const key = String(id)
|
||
idTotals.set(key, (idTotals.get(key) || 0) + num)
|
||
}
|
||
const name = product.title || product.name || item.productName || item.goodsName || item.title || item.name
|
||
if (name) {
|
||
nameTotals.set(name, (nameTotals.get(name) || 0) + num)
|
||
}
|
||
})
|
||
return {idTotals, nameTotals}
|
||
},
|
||
safeNumber(value) {
|
||
const num = Number(value)
|
||
return Number.isFinite(num) ? num : 0
|
||
},
|
||
salesProgress(item) {
|
||
const num = this.safeNumber(item.taskNum)
|
||
if (!num) return 0
|
||
const qty = this.safeNumber(item.qty)
|
||
return Math.min(100, Math.round((qty / num) * 100))
|
||
},
|
||
salesProgressText(item) {
|
||
const num = this.safeNumber(item.taskNum)
|
||
if (!num) return ''
|
||
const qty = this.safeNumber(item.qty)
|
||
return `${qty}/${num}`
|
||
},
|
||
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)
|
||
}
|
||
return []
|
||
},
|
||
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}`
|
||
},
|
||
formatDateTime(date) {
|
||
return `${this.formatDate(date)} ${this.formatTime(date)}`
|
||
},
|
||
async save(options = {}) {
|
||
if (this.isAuditEntry) return
|
||
if (this.task.userTask.status === 1 || this.task.userTask.status === 2) return
|
||
let isClock = false
|
||
let clockIndex = -1
|
||
let silent = false
|
||
if (typeof options === 'boolean') {
|
||
isClock = options
|
||
} else if (options && typeof options === 'object') {
|
||
isClock = !!options.isClock
|
||
clockIndex = typeof options.clockIndex === 'number' ? options.clockIndex : -1
|
||
silent = !!options.silent
|
||
}
|
||
const summaryText = this.summary ? (this.summary || '').trim() : ''
|
||
const locationText = (this.currentAddress || '').trim()
|
||
const normalizedStoreList = this.sanitizeStoreList()
|
||
const firstStore = normalizedStoreList[0] || {}
|
||
const baseLat = this.isStoreTask ? (this.lat || firstStore.lat || null) : this.lat
|
||
const baseLng = this.isStoreTask ? (this.lng || firstStore.lng || null) : this.lng
|
||
const baseLocation = this.isStoreTask ? (locationText || firstStore.location || null) : (locationText || null)
|
||
if (this.clockPhotos.length && isClock && clockIndex > -1) {
|
||
this.clockPhotos[clockIndex].lat = this.lat
|
||
this.clockPhotos[clockIndex].lng = this.lng
|
||
this.clockPhotos[clockIndex].location = locationText
|
||
}
|
||
const hasClockPhotos = Array.isArray(this.clockPhotos) && this.clockPhotos.some(item => item && item.url)
|
||
const payload = {
|
||
id: this.task.userTask.id,
|
||
video: null,
|
||
lat: baseLat,
|
||
lng: baseLng,
|
||
rating: this.isStoreTask ? 0 : this.rating,
|
||
clockPhotos: this.isStoreTask ? null : (this.clockPhotos.length ? JSON.stringify(this.clockPhotos || []) : null),
|
||
boothPhotos: this.isStoreTask ? null : (this.boothPhotos.length ? JSON.stringify(this.boothPhotos || []) : null),
|
||
salesGroups: this.isStoreTask ? null : (this.salesGroups.length ? JSON.stringify(this.salesGroups || []) : null),
|
||
receiptPhoto: this.isStoreTask ? null : (this.receiptPhotos.length ? JSON.stringify(this.receiptPhotos || []) : null),
|
||
storeList: this.isStoreTask ? (normalizedStoreList.length ? JSON.stringify(normalizedStoreList) : null) : null,
|
||
summary: JSON.stringify(summaryText),
|
||
status: 0,
|
||
auditReason: ''
|
||
}
|
||
if (hasClockPhotos || this.isStoreTask) {
|
||
payload.location = baseLocation
|
||
}
|
||
const res = await changeUserTaskReq(payload).catch(err => {
|
||
this.toast(err.message)
|
||
if (isClock && clockIndex > -1) {
|
||
this.clockPhotos[clockIndex].url = ''
|
||
this.clockPhotos[clockIndex].time = ''
|
||
this.clockPhotos[clockIndex].date = ''
|
||
}
|
||
})
|
||
if (res && !silent) this.toast('保存成功')
|
||
if (res && isClock && clockIndex === 0) {
|
||
if (dayjs().isAfter(this.task.taskStartTime)) this.toast('您已迟到!')
|
||
}
|
||
},
|
||
async submit() {
|
||
if (this.isAuditEntry) return
|
||
if (!this.isStoreTask) {
|
||
this.fillEmptySalesQtyWithZero()
|
||
}
|
||
if (!this.validateSubmit()) return
|
||
const normalizedStoreList = this.sanitizeStoreList()
|
||
const imageList = this.isStoreTask
|
||
? normalizedStoreList.reduce((list, item) => list.concat(Array.isArray(item.photos) ? item.photos : []), [])
|
||
: [
|
||
...this.clockPhotos.filter(item => item.url).map(item => item.url),
|
||
...this.boothPhotos,
|
||
...this.receiptPhotos
|
||
]
|
||
const firstStore = normalizedStoreList[0] || {}
|
||
const submitLat = this.isStoreTask ? (this.lat || firstStore.lat || null) : this.lat
|
||
const submitLng = this.isStoreTask ? (this.lng || firstStore.lng || null) : this.lng
|
||
if (!submitLat || !submitLng) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '为了更好的提供服务,请开启定位服务',
|
||
success: ({confirm}) => {
|
||
if (confirm) uni.openSetting()
|
||
}
|
||
})
|
||
return
|
||
}
|
||
const summaryText = (this.summary || '').trim()
|
||
const locationText = (this.currentAddress || '').trim()
|
||
const submitLocation = this.isStoreTask ? (locationText || firstStore.location || null) : (locationText || null)
|
||
const res = await changeUserTaskReq({
|
||
id: this.task.userTask.id,
|
||
image: imageList.length ? JSON.stringify(imageList) : null,
|
||
video: null,
|
||
lat: submitLat,
|
||
lng: submitLng,
|
||
location: submitLocation,
|
||
rating: this.isStoreTask ? 0 : this.rating,
|
||
clockPhotos: this.isStoreTask ? null : JSON.stringify(this.clockPhotos || []),
|
||
boothPhotos: this.isStoreTask ? null : JSON.stringify(this.boothPhotos || []),
|
||
salesGroups: this.isStoreTask ? null : JSON.stringify(this.salesGroups || []),
|
||
receiptPhoto: this.isStoreTask ? null : JSON.stringify(this.receiptPhotos || []),
|
||
storeList: this.isStoreTask ? JSON.stringify(normalizedStoreList) : null,
|
||
summary: JSON.stringify(summaryText),
|
||
status: 1,
|
||
auditReason: ''
|
||
}).catch(e => {
|
||
this.toast(e.message)
|
||
})
|
||
if (res) {
|
||
await this.toast('提交成功,请等待审核')
|
||
uni.navigateBack()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.detail-page {
|
||
--task-submit-bar-height: calc(108rpx + env(safe-area-inset-bottom));
|
||
min-height: 100vh;
|
||
//padding: 0 20rpx 40rpx;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.page-hero {
|
||
//padding: 30rpx 6rpx 18rpx;
|
||
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat top center / 100% auto;
|
||
}
|
||
|
||
.page-navbar {
|
||
padding: 0 6rpx;
|
||
}
|
||
|
||
.block {
|
||
margin: 18rpx 20rpx 0;
|
||
}
|
||
|
||
.store-card {
|
||
margin: 10rpx 20rpx 0;
|
||
background: linear-gradient(135deg, #2fc6a7, #2fc6a7);
|
||
border-radius: 24rpx;
|
||
padding: 28rpx;
|
||
color: #ffffff;
|
||
box-shadow: 0 12rpx 24rpx rgba(37, 99, 235, 0.2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.store-tag {
|
||
align-self: flex-start;
|
||
padding: 8rpx 18rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 24rpx;
|
||
border: 2rpx solid rgba(255, 255, 255, 0.45);
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.store-title {
|
||
font-size: 36rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.store-chain {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
align-self: flex-start;
|
||
max-width: 100%;
|
||
padding: 8rpx 14rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(255, 255, 255, 0.18);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.34);
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.store-chain-label {
|
||
flex-shrink: 0;
|
||
font-size: 22rpx;
|
||
color: rgba(255, 255, 255, 0.78);
|
||
}
|
||
|
||
.store-chain-value {
|
||
min-width: 0;
|
||
max-width: 430rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.store-address {
|
||
font-size: 26rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
color: rgba(255, 255, 255, 0.92);
|
||
}
|
||
|
||
.detail-info-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.detail-info-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.detail-info-toggle-text {
|
||
font-size: 24rpx;
|
||
color: #8f9bb3;
|
||
}
|
||
|
||
.detail-info-toggle-arrow {
|
||
display: flex;
|
||
align-items: center;
|
||
transform: rotate(90deg);
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.detail-info-toggle-arrow.open {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.detail-info-body {
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.total-progress-card {
|
||
padding: 22rpx 24rpx;
|
||
}
|
||
|
||
.total-progress-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.total-progress-title {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.total-progress-value {
|
||
font-size: 24rpx;
|
||
color: #64748b;
|
||
}
|
||
|
||
.total-progress-bar {
|
||
height: 12rpx;
|
||
border-radius: 999rpx;
|
||
background: #e2e8f0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.total-progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #2fc6a7, #a1ded0);
|
||
}
|
||
|
||
.detail-info-title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.detail-requirement {
|
||
background: #ecfdf5;
|
||
border: 1rpx solid #a7f3d0;
|
||
border-radius: 24rpx;
|
||
padding: 20rpx 22rpx;
|
||
}
|
||
|
||
.detail-block-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.detail-icon {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border-radius: 12rpx;
|
||
//background: #a1ded0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.detail-block-title {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #2fc6a7;
|
||
}
|
||
|
||
.detail-block-text {
|
||
font-size: 26rpx;
|
||
color: #2fc6a7;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.detail-section {
|
||
margin-top: 26rpx;
|
||
}
|
||
|
||
.detail-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.detail-section-dot {
|
||
width: 10rpx;
|
||
height: 32rpx;
|
||
border-radius: 999rpx;
|
||
}
|
||
|
||
.detail-section-dot.orange {
|
||
background: #f97316;
|
||
}
|
||
|
||
.detail-section-dot.green {
|
||
background: #22c55e;
|
||
}
|
||
|
||
.detail-section-dot.blue {
|
||
background: #3b82f6;
|
||
}
|
||
|
||
.detail-section-card {
|
||
margin-top: 14rpx;
|
||
background: #f8fafc;
|
||
border-radius: 18rpx;
|
||
padding: 18rpx 20rpx;
|
||
color: #4b5563;
|
||
font-size: 26rpx;
|
||
border: 1rpx solid #edf2f7;
|
||
}
|
||
|
||
.location-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.location-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.location-label {
|
||
flex-shrink: 0;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.location-value {
|
||
flex: 1;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.section-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.section-title-row .section-title {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.section-extra {
|
||
color: #8b95a7;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.section-title.tight {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.section-sub {
|
||
font-size: 24rpx;
|
||
color: #8b95a7;
|
||
}
|
||
|
||
.section-title.warm::before {
|
||
background: linear-gradient(180deg, #ff8a4c, #ffb36b);
|
||
}
|
||
|
||
.clock-grid {
|
||
display: flex;
|
||
gap: 18rpx;
|
||
}
|
||
|
||
.clock-card {
|
||
flex: 1;
|
||
}
|
||
|
||
.clock-thumb {
|
||
position: relative;
|
||
height: 220rpx;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
background: linear-gradient(180deg, #1f2c3c, #151e29);
|
||
border: 2rpx solid #2b3950;
|
||
}
|
||
|
||
.clock-card.primary .clock-thumb {
|
||
border-color: #4c7bff;
|
||
}
|
||
|
||
.clock-card.warm .clock-thumb {
|
||
border-color: #f3a45c;
|
||
}
|
||
|
||
.clock-image,
|
||
.clock-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.clock-placeholder {
|
||
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: 20rpx;
|
||
}
|
||
|
||
.audit-empty-tip {
|
||
padding: 24rpx 0;
|
||
text-align: center;
|
||
color: #9aa3b2;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.visit-store-card {
|
||
background: #f8fafc;
|
||
border: 1rpx solid #edf2f7;
|
||
border-radius: 20rpx;
|
||
padding: 20rpx;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.visit-store-card:last-of-type {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.visit-store-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.visit-store-title {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.visit-store-time {
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.visit-store-remove,
|
||
.visit-store-action {
|
||
font-size: 24rpx;
|
||
color: #2fc6a7;
|
||
}
|
||
|
||
.visit-store-field {
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.visit-store-field:last-of-type {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.visit-store-label-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.visit-store-label {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.visit-store-picker {
|
||
width: 100%;
|
||
}
|
||
|
||
.visit-store-picker-inner {
|
||
min-height: 76rpx;
|
||
border-radius: 16rpx;
|
||
background: #ffffff;
|
||
border: 3px solid #ff8a4c;
|
||
padding: 0 18rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
color: #111827;
|
||
font-size: 26rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.visit-store-picker-inner.placeholder {
|
||
color: #9aa3b2;
|
||
}
|
||
|
||
.visit-store-empty {
|
||
width: 100%;
|
||
}
|
||
|
||
.visit-store-add {
|
||
margin-top: 18rpx;
|
||
height: 76rpx;
|
||
border-radius: 16rpx;
|
||
border: 2rpx dashed #2fc6a7;
|
||
color: #2fc6a7;
|
||
background: #f3fffb;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.merchant-picker {
|
||
height: 70vh;
|
||
padding: 20rpx 24rpx 30rpx;
|
||
}
|
||
|
||
.merchant-picker-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.picker-action {
|
||
font-size: 26rpx;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.picker-action.primary {
|
||
color: #2fc6a7;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.picker-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.merchant-picker-search {
|
||
margin: 6rpx 0 12rpx;
|
||
}
|
||
|
||
.merchant-search-input {
|
||
width: 100%;
|
||
background: #f3f4f6;
|
||
border-radius: 12rpx;
|
||
padding: 12rpx 16rpx;
|
||
font-size: 26rpx;
|
||
color: #1f2937;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.merchant-search-placeholder {
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.merchant-picker-loading {
|
||
padding: 20rpx 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.merchant-picker-view {
|
||
height: 90%;
|
||
}
|
||
|
||
.merchant-picker-item {
|
||
height: 44px;
|
||
line-height: 44px;
|
||
text-align: center;
|
||
font-size: 26rpx;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.photo-item {
|
||
width: calc((100% - 40rpx) / 2);
|
||
height: 200rpx;
|
||
border-radius: 18rpx;
|
||
background: #f6f7fb;
|
||
position: relative;
|
||
overflow: hidden;
|
||
border: 1rpx solid #edf1f7;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.photo-item.add {
|
||
border: 2rpx dashed #cfd5e1;
|
||
background: #fbfcfe;
|
||
}
|
||
|
||
.photo-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.photo-remove {
|
||
position: absolute;
|
||
top: 8rpx;
|
||
right: 8rpx;
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
color: #fff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.sales-group {
|
||
border-radius: 18rpx;
|
||
background: #f7f9fc;
|
||
//padding: 0 20rpx;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.sales-group:last-of-type {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.sales-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx;
|
||
background-color: #2fc6a7;
|
||
color: white;
|
||
border-radius: 18rpx 18rpx 0 0;
|
||
}
|
||
|
||
.sales-title {
|
||
font-size: 28rpx;
|
||
color: white;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sales-title-row {
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.sales-actions {
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.sales-add-btn {
|
||
width: 240rpx;
|
||
min-height: 76rpx;
|
||
border-radius: 999rpx;
|
||
background: linear-gradient(135deg, #2fc6a7, #1fb394);
|
||
box-shadow: 0 12rpx 24rpx rgba(47, 198, 167, 0.2);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10rpx;
|
||
color: #ffffff;
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.sales-remove {
|
||
flex-shrink: 0;
|
||
color: #ef4444;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.sales-arrow {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.sales-arrow.open {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.sales-items {
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 10rpx 18rpx;
|
||
margin-bottom: 20rpx;
|
||
box-shadow: 0 6rpx 16rpx rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.sales-item {
|
||
padding: 16rpx 0;
|
||
border-bottom: 1rpx solid #edf1f7;
|
||
}
|
||
|
||
.sales-item:last-of-type {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.sales-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.sales-check {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
border-radius: 6rpx;
|
||
border: 2rpx solid #cfd5e1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.sales-check.checked {
|
||
background: #22c55e;
|
||
border-color: #22c55e;
|
||
}
|
||
|
||
.sales-name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
font-size: 30rpx;
|
||
color: var(--text-main);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sales-target {
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
color: #f97316;
|
||
background: #fff7ed;
|
||
border: 1rpx solid #fed7aa;
|
||
}
|
||
|
||
.sales-progress {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin: 6rpx 0 12rpx;
|
||
}
|
||
|
||
.sales-progress-bar {
|
||
flex: 1;
|
||
height: 10rpx;
|
||
border-radius: 999rpx;
|
||
background: #e5e7eb;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sales-progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #22c55e, #60a5fa);
|
||
}
|
||
|
||
.sales-progress-text {
|
||
font-size: 26rpx;
|
||
color: #6b7280;
|
||
min-width: 84rpx;
|
||
text-align: right;
|
||
}
|
||
|
||
.sales-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.spec-picker {
|
||
flex: 1;
|
||
}
|
||
|
||
.spec-pill {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10rpx;
|
||
padding: 10rpx 16rpx;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid #e2e6ef;
|
||
background: #f7f9fc;
|
||
font-size: 24rpx;
|
||
color: #4a5568;
|
||
}
|
||
|
||
.qty-box {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
border: 5rpx solid #f97316;
|
||
border-radius: 12rpx;
|
||
padding: 6rpx 12rpx;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.qty-input {
|
||
flex: 1;
|
||
font-size: 30rpx;
|
||
color: var(--text-main);
|
||
text-align: right;
|
||
padding-right: 12rpx;
|
||
}
|
||
|
||
.qty-unit {
|
||
font-size: 28rpx;
|
||
color: #8b95a7;
|
||
border-left: 1rpx solid #edf1f7;
|
||
padding-left: 10rpx;
|
||
}
|
||
|
||
.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, #ecfdf5);
|
||
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: #059669;
|
||
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-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;
|
||
}
|
||
|
||
.receipt-upload {
|
||
border: 2rpx dashed #cfd5e1;
|
||
border-radius: 20rpx;
|
||
background: #f6f7fb;
|
||
padding: 30rpx;
|
||
min-height: 240rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.receipt-placeholder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
color: #8b95a7;
|
||
}
|
||
|
||
.receipt-icon {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 50%;
|
||
background: #e4e8ef;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.receipt-title {
|
||
font-size: 28rpx;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.receipt-desc {
|
||
font-size: 24rpx;
|
||
color: #9aa3b2;
|
||
}
|
||
|
||
.receipt-preview {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 240rpx;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.receipt-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.receipt-remove {
|
||
position: absolute;
|
||
top: 10rpx;
|
||
right: 10rpx;
|
||
width: 34rpx;
|
||
height: 34rpx;
|
||
border-radius: 50%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.summary-input {
|
||
width: 100%;
|
||
min-height: 160rpx;
|
||
padding: 20rpx;
|
||
background: #f6f7fb;
|
||
border-radius: 16rpx;
|
||
font-size: 26rpx;
|
||
color: var(--text-main);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.rating-row {
|
||
padding-top: 8rpx;
|
||
}
|
||
|
||
.task-submit-bar {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
height: var(--task-submit-bar-height);
|
||
padding: 20rpx;
|
||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
box-shadow: 0 -4rpx 16rpx rgba(15, 23, 42, 0.08);
|
||
}
|
||
|
||
.task-submit-placeholder {
|
||
height: var(--task-submit-bar-height);
|
||
}
|
||
|
||
.submit {
|
||
width: 100%;
|
||
margin: 0;
|
||
padding: 14rpx;
|
||
height: 68rpx;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.audit-action-bar {
|
||
margin: 18rpx 20rpx 0;
|
||
}
|
||
|
||
.audit-action-list {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.audit-action-btn {
|
||
flex: 1;
|
||
height: 68rpx;
|
||
border-radius: 14rpx;
|
||
border: 1rpx solid #2fc6a7;
|
||
color: #2fc6a7;
|
||
background: #f3fffb;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.audit-action-btn-danger {
|
||
border-color: #ef4444;
|
||
color: #ef4444;
|
||
background: #fff1f2;
|
||
}
|
||
|
||
.mt-12 {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.audit-popup-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
z-index: 90;
|
||
}
|
||
|
||
.audit-popup {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 40rpx;
|
||
transform: translateX(-50%);
|
||
width: 88%;
|
||
max-height: 70vh;
|
||
overflow: auto;
|
||
z-index: 100;
|
||
}
|
||
|
||
.audit-popup-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.close-btn {
|
||
color: #6b7280;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.audit-form-row {
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.audit-form-label {
|
||
font-size: 24rpx;
|
||
color: #4b5563;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.audit-form-input {
|
||
width: 100%;
|
||
height: 70rpx;
|
||
border-radius: 12rpx;
|
||
border: 1rpx solid #e5e7eb;
|
||
background: #f9fafb;
|
||
padding: 0 16rpx;
|
||
font-size: 26rpx;
|
||
color: #111827;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.audit-submit-btn {
|
||
margin-top: 16rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.audit-copy-search-row {
|
||
display: flex;
|
||
gap: 10rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
.audit-copy-search-input {
|
||
flex: 1;
|
||
height: 70rpx;
|
||
line-height: 70rpx;
|
||
border-radius: 12rpx;
|
||
background: #f3f4f6;
|
||
padding: 0 16rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.audit-copy-search-btn {
|
||
min-width: 110rpx;
|
||
height: 70rpx;
|
||
border-radius: 12rpx;
|
||
background: #2fc6a7;
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.audit-copy-loading,
|
||
.audit-copy-empty {
|
||
margin-top: 14rpx;
|
||
font-size: 24rpx;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.audit-copy-user-list {
|
||
margin-top: 14rpx;
|
||
max-height: 420rpx;
|
||
}
|
||
|
||
.audit-copy-user-item {
|
||
padding: 12rpx;
|
||
border-radius: 12rpx;
|
||
background: #f9fafb;
|
||
margin-bottom: 10rpx;
|
||
border: 1rpx solid #e5e7eb;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.audit-copy-user-item-active {
|
||
border-color: #2fc6a7;
|
||
background: #ecfdf5;
|
||
}
|
||
|
||
.audit-copy-user-name {
|
||
font-size: 26rpx;
|
||
color: #111827;
|
||
}
|
||
|
||
.audit-copy-user-phone {
|
||
font-size: 22rpx;
|
||
color: #6b7280;
|
||
}
|
||
</style>
|