Files
yxe-mp/pages/index/index.vue
T
weicw-dshsa e192296ea7 feat(UI): 首页与个人中心按原型改版,修复纯白图标在白底上不可见
一、首页两个入口按钮改版(用户反馈「太小了」)

原为「裸图标 + 单行文字」的矮按钮(min-height 76rpx),改为
「白色圆角图标块 + 右侧主副两行文字」的大卡片(padding 22rpx 20rpx):
- 自主填报:便捷填报,快速提交
- 填报记录:历史记录,随时查看
主按钮渐变由 #f97316→#ef4444 改为 #f9863c→#f2620f 并加橙色投影。
结构对齐 create.vue 已有的 sales-entry-* 卡片。

二、个人中心三张卡片改版(用户提供原型图)

- 积分卡:改为「绿底图标块 + 大号数字 + 我的积分」,右侧新增
  「积分明细 ›」胶囊按钮;去掉原先那条突兀的 20px 绿色粗顶边。
  数字字号 30rpx → 40rpx。
- 自主填报 / 客户拜访:由单行居中文字改为「图标块 + 主标题 + 副标题」,
  副标题分别为「及时上报、真实记录」「拜访记录、有效跟进」。
- 药审核:改为整行卡片(图标 + 药审核/待审核任务 + 红色数字徽标 + 箭头),
  徽标数据来自新增接口 /yxe/yxe-audit/pending-count(见 yxe-java 侧提交),
  仅 can_auth === 1 时拉取,退出登录时清零。

三、修复纯白图标在近白底上不可见(用户反馈「图片是白色的看不清」)

根因不在 opacity 或主题色,而是图源本身就是纯白图形 + 透明背景:
  文件                       颜色构成            内容占画布
  0fd31838....png (驻店)     透明 + #FFFFFF      75% x 50%
  f5fbb2e4....png (跑店)     白色及半透明白       88% x 88%
  9c6c9884....png (新任务)   纯白                90% x 90%
  eebe6469....png (已完成)   纯白                 -
而承载它们的底色分别是 #f8fafc(选择页卡片)与平均亮度 250/255 的
近白底图(首页 tab),白图标贴白底自然完全看不见。此类图标只适合
放在深色/彩色底上,同一张 0fd31838....png 在个人中心药审核卡里
配金色底时一直正常,故长期未暴露。

改法:统一垫一层绿色圆角底块反衬白色图形
- select.vue:新增 .type-icon-wrap(未选中 #2fc6a7→#1aa88c,
  选中加深为 #16a085→#0e7a66 + 投影,顺带强化选中态)
- index.vue:新增 .tab-icon-wrap(未激活绿圆底;激活时置
  background:none,避免两层绿有色差露出一圈描边)

顺带修一个隐藏问题:两张填报图源的内容占比差很多,同尺寸渲染会让
驻店图标明显偏小,故为 store 单独放大(.type-icon-store 92rpx)。

验证:用 Pillow 按 rpx→px 实际比例合成修复前后对比图逐张核对,
确认修复前未激活图标完全不可见、修复后清晰、激活态无色差边。
模板标签、style 括号、类名定义/引用均通过脚本校验。

注:f5fbb2e4....png 渲染出来实际是「时钟」形状,用在「跑店填报」
语义是否合适待确认,本次只解决可见性,未更换图标资源。
2026-10-12 16:11:58 +08:00

1467 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="home-page">
<view class="top-wrap">
<AppNavbar class="top-navbar" title="药小二" title-size="28rpx" gap="40rpx"/>
<view class="search-bar top-search">
<u-search showAction animation v-model="searchQuery" bgColor="white"
searchIcon="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/16f54dd102cacccfb5085533009b70fc.png"/>
</view>
<view v-if="bannersEnabled && banners.length" class="banner-area">
<swiper class="banner-swiper" autoplay :interval="4200" indicator-dots indicator-color="#bfeee4"
indicator-active-color="#1ac5a6">
<swiper-item v-for="item in banners" :key="item.id">
<image class="banner-img" :src="item.image" mode="aspectFill"/>
</swiper-item>
</swiper>
</view>
<!-- <view class="notice-bar">-->
<!-- <text class="notice-label">奖励:</text>-->
<!-- <swiper class="notice-swiper" vertical circular autoplay :interval="3200" :duration="400">-->
<!-- <swiper-item v-for="(notice, idx) in notices" :key="idx" class="notice-item">-->
<!-- <text class="notice-text">{{ notice }}</text>-->
<!-- </swiper-item>-->
<!-- </swiper>-->
<!-- <view class="notice-more">-->
<!-- <text class="notice-more-icon">></text>-->
<!-- </view>-->
<!-- </view>-->
<view class="self-report-entry">
<view class="entry-btn primary" @tap="openSelfReportCreate">
<view class="entry-icon">
<u-icon name="edit-pen" size="26" color="#f97316"/>
</view>
<view class="entry-text">
<text class="entry-title">自主填报</text>
<text class="entry-desc">便捷填报,快速提交</text>
</view>
</view>
<view class="entry-btn" @tap="openSelfReportRecord">
<view class="entry-icon">
<u-icon name="order" size="26" color="#f97316"/>
</view>
<view class="entry-text">
<text class="entry-title">填报记录</text>
<text class="entry-desc">历史记录,随时查看</text>
</view>
</view>
</view>
</view>
<view class="nav-wrap">
<view class="tabs">
<view v-for="tab in tabs" :key="tab" :class="['tab', activeTab === tab ? 'active' : '']"
@tap="activeTab = tab">
<view class="tab-main">
<view class="tab-icon-wrap">
<image v-if="tab === '新任务'" class="tab-icon"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/9c6c98843c2e30247355d86ea47552aa.png"
mode="aspectFit"/>
<image v-if="tab === '待执行'" class="tab-icon"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260127/f5fbb2e4be31bcc830156f174e7931b8.png"
mode="aspectFit"/>
<image v-if="tab === '已完成'" class="tab-icon"
src="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/eebe6469eb153413dc881f513063f821.png"
mode="aspectFit"/>
</view>
<text class="tab-text">{{ tab }}</text>
</view>
<view v-if="tab === '待执行' && pendingCount" class="tab-badge">{{ pendingCount }}</view>
<view v-if="activeTab === tab" class="tab-underline"/>
</view>
</view>
<view class="category-wrap">
<scroll-view class="category-bar" scroll-x show-scrollbar="false">
<view v-for="cat in categories" :key="cat" :class="['category-chip', category === cat.id ? 'active' : '']"
@tap="changeCate(cat.id)">
{{ cat.title }}
</view>
</scroll-view>
</view>
</view>
<view class="board">
<view class="filter-row">
<view class="location" @tap="toggleCity">
<u-icon name="map"/>
<text>{{ currentCity }}</text>
</view>
<view class="sort-options">
<view v-for="sort in sortOptions" :key="sort" :class="['sort-item', sortType === sort ? 'active' : '']"
@tap="sortType = sort">
<text class="sort-text">{{ sort }}</text>
<text class="sort-arrow">▼</text>
</view>
</view>
</view>
<view class="task-list">
<view v-if="getFilteredTasks().length === 0" class="empty">
<u-empty
icon="https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/ddcc1bbd561d97128ed19407b07e299b.png"
text="暂无符合条件的任务"/>
</view>
<view v-for="task in getFilteredTasks()" :key="task.id" class="task-card">
<view class="task-head">
<view class="date-row">
<text class="date">{{ formatTaskDateText(task) }}</text>
<view class="relative">{{ formatTaskDateSubText(task) }}</view>
<view v-if="task.cate && task.cate.title" class="cate-tag">{{ task.cate.title }}</view>
</view>
<view v-if="!isManualSalesTask(task)" class="reward">
<text class="currency">¥</text>
<text class="reward-number">{{ calReward(task) }}</text>
</view>
</view>
<view class="task-title-row">
<text class="task-title">{{ task.merchant.name }}</text>
<view v-if="task.chainText" class="chain-pill">{{ task.chainText }}</view>
<view :class="['type-pill', 'type-blue']">{{ task.cate.title }}</view>
</view>
<text class="addr">{{ task.title }}</text>
<view class="task-meta" v-if="task.merchant">
<text class="addr">{{ task.merchant.province }}{{ task.merchant.city }}{{
task.merchant.region
}}{{ task.merchant.address }}
</text>
<view class="distance">
<u-icon name="map"/>
<text>{{ task.distance }}</text>
</view>
</view>
<view v-if="task.userTask" class="total-progress-card">
<view class="total-progress-head">
<text class="total-progress-title">总进度</text>
<text class="total-progress-value">{{ totalProgressText(task) }}</text>
</view>
<view class="total-progress-bar">
<view class="total-progress-fill" :style="{ width: `${totalProgressPercent(task)}%` }"></view>
</view>
</view>
<view v-if="task.userTask && [1, 2, 3].includes(task.userTask.status)" class="audit-box">
<view class="flex flex-col justify-start items-start">
<view :class="['audit-status', auditClass(task.userTask.status)]">订单状态:
{{ auditLabel(task.userTask.status) }}
</view>
<text v-if="task.userTask.auditReason" class="audit-reason">原因: {{ task.userTask.auditReason }}</text>
</view>
<view class="info-action" @tap="showDetails(task)">查看详情</view>
</view>
<view class="actions" v-if="task.status === 0">
<view class="ghost-action" @tap="openDetailPopup(task)">查看详情</view>
<view class="primary-action" @tap="takeOver(task)">接单</view>
<!-- <view v-if="task.status === 'pending'" class="primary-action full" @tap="goExecute(task)">去执行</view>-->
</view>
<view class="actions" v-if="task.status === 1 && task.userTask && task.userTask.status === 0">
<view class="ghost-action" @tap="openDetailPopup(task)">查看详情</view>
<view class="action-btn" @tap="showDetails(task)">去执行</view>
</view>
</view>
</view>
</view>
<u-popup :show="detailPopupVisible" mode="bottom" round="36rpx" close-on-click-overlay @close="closeDetailPopup">
<view class="detail-popup" v-if="detailTask">
<view class="detail-popup-head">
<text class="detail-popup-title">任务详情</text>
<view class="detail-popup-close" @tap="closeDetailPopup">
<u-icon name="close" size="36rpx" color="#7a8495"/>
</view>
</view>
<view class="detail-merchant-card">
<view v-if="detailTask.chainText" class="detail-merchant-row">
<text class="detail-merchant-label">所属连锁</text>
<text class="detail-merchant-value">{{ detailTask.chainText }}</text>
</view>
<view v-if="detailTask.merchant && detailTask.merchant.name" class="detail-merchant-row">
<text class="detail-merchant-label">任务门店</text>
<text class="detail-merchant-value">{{ detailTask.merchant.name }}</text>
</view>
</view>
<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">{{ detailTask.detail }}</text>
</view>
<!-- <view class="detail-section" v-if="detailTask.cateId !== 4">-->
<!-- <view class="detail-section-title">-->
<!-- <view class="detail-section-dot orange"></view>-->
<!-- <text>重点爆破品种</text>-->
<!-- </view>-->
<!-- <view class="detail-section-card">{{-->
<!-- detailTask.productList.map(item => item.product.title).join('、')-->
<!-- }}-->
<!-- </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">{{ detailTask.policy }}</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">{{ formatTaskExecutePeriod(detailTask) }}</view>
</view>
</view>
</u-popup>
<AreaPicker
:show="showAreaPicker"
:default-province="currentProvince"
:default-city="currentCity"
@close="showAreaPicker = false"
@confirm="onAreaConfirm"
/>
<Login ref="Login" @done="handleLoginDone"/>
</view>
</template>
<script>
import {addUserTaskReq, taskCateListReq, taskDoneListReq, taskListReq} from "@/api/task";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import "dayjs/locale/zh-cn";
import {calcDistanceMeters, formatDistance, getLocationAction} from "@/util";
import {swiperListReq} from "@/api/common";
import AppNavbar from "@/components/AppNavbar.vue";
import AreaPicker from "@/components/AreaPicker.vue";
import Login from "@/components/Login.vue";
import {buildSharePath, getUserInfo} from "@/util/user";
dayjs.extend(relativeTime);
dayjs.locale("zh-cn");
export default {
components: {AppNavbar, AreaPicker, Login},
data() {
return {
bannersEnabled: true,
banners: [],
tabs: ['新任务', '待执行', '已完成'],
activeTab: '待执行',
searchQuery: '',
categories: [],
category: null,
sortOptions: ['综合排序', '距我最近', '热度最高'],
sortType: '综合排序',
currentCity: '定位中',
currentProvince: '',
showAreaPicker: false,
notices: [
// '李强已完成业绩 ¥12,000',
// '黄敏新接单 3 个门店任务',
// '周杰完成活动爆破业绩 ¥18,500',
// '团队今日累计业绩 ¥65,300'
],
tasks: [],
lat: null,
lng: null,
detailPopupVisible: false,
detailTask: null,
pendingLoginUrl: ''
}
},
computed: {
activeStatus() {
if (this.activeTab === '新任务') return 'new';
if (this.activeTab === '待执行') return 'pending';
return 'completed';
},
pendingCount() {
return this.tasks.filter(t => t.status === 'pending').length;
},
},
watch: {
activeTab() {
this.getTaskList()
}
},
async onShow() {
await this.refreshPageData()
},
async onPullDownRefresh() {
try {
await this.refreshPageData()
} finally {
uni.stopPullDownRefresh()
}
},
methods: {
async refreshPageData() {
const res = await getLocationAction().catch(() => {
uni.showToast({
title: '请允许位置授权',
icon: 'none'
})
})
if (res) {
this.lat = res.latitude
this.lng = res.longitude
this.currentCity = res.city_str
this.currentProvince = res.province_str || ''
this.updateTaskDistances()
}
await this.getSwiperList()
await this.getCateList()
await this.getNoticeList()
await this.getTaskList()
},
async getNoticeList() {
const {data} = await taskDoneListReq()
this.notices = data.map(item => {
return `${item.user.nickname}已完成业绩 ¥${item.reward}`
})
},
async getSwiperList() {
const {data} = await swiperListReq()
this.banners = data
},
async getCateList() {
const {data} = await taskCateListReq({
filter: true
});
this.categories = [{id: null, title: '全部'}, ...data]
},
async getTaskList() {
const params = {
province: this.currentProvince,
city: this.currentCity,
notExpired: true
}
if (this.category) params.cateId = this.category;
params.userTaskStatus = this.getUserTaskStatus()
const {data} = await taskListReq(params);
this.tasks = data.map(task => Object.assign({}, task, {
chainText: this.getTaskChainText(task)
}))
this.updateTaskDistances()
},
changeCate(id) {
this.category = id;
this.getTaskList()
},
toggleCity() {
this.showAreaPicker = true
},
onAreaConfirm({provinceName, cityName}) {
this.currentProvince = provinceName
this.currentCity = cityName
this.showAreaPicker = false
this.getTaskList()
},
getUserTaskStatus() {
if (this.activeTab === '新任务') return -1
if (this.activeTab === '待执行') return 0
return 1
},
distanceToNumber(distance) {
if (!distance) return 0;
if (distance.indexOf('km') > -1) return parseFloat(distance);
if (distance.indexOf('m') > -1) return parseFloat(distance) / 1000;
return parseFloat(distance);
},
formatRelativeDate(value) {
if (!value) return '';
const date = dayjs(value);
return date.isValid() ? date.fromNow() : value;
},
formatTaskDateText(task) {
const isPendingTab = this.activeTab === '待执行'
const isNewTab = this.activeTab === '新任务'
const startTime = task && task.taskStartTime ? task.taskStartTime : ''
const taskDate = task && task.date ? task.date : ''
if (isPendingTab && startTime) {
const date = dayjs(startTime)
if (date.isValid()) return date.format('MM月DD日')
}
if (isNewTab && taskDate) {
const date = dayjs(taskDate)
if (date.isValid()) return date.format('MM月DD日')
}
return task && task.date ? task.date : ''
},
formatTaskDateSubText(task) {
const isPendingTab = this.activeTab === '待执行'
const isNewTab = this.activeTab === '新任务'
const startTime = task && task.taskStartTime ? task.taskStartTime : ''
const taskDate = task && task.date ? task.date : ''
if (isPendingTab && startTime) {
const date = dayjs(startTime)
if (date.isValid()) {
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const weekday = weekdays[date.day()] || ''
const daysAfter = this.getDaysAfterToday(date)
const daysText = daysAfter < 0 ? '已过期' : (daysAfter === 0 ? '今天' : `${daysAfter}天后`)
return `${weekday} ${daysText}`.trim()
}
}
if (isNewTab && taskDate) {
const date = dayjs(taskDate)
if (date.isValid()) {
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
return weekdays[date.day()] || ''
}
}
return this.formatRelativeDate(task && task.date ? task.date : '')
},
getDaysAfterToday(dateValue) {
const target = dayjs(dateValue).startOf('day')
if (!target.isValid()) return 0
const today = dayjs().startOf('day')
return target.diff(today, 'day')
},
updateTaskDistances() {
if (!Number.isFinite(this.lat) || !Number.isFinite(this.lng)) return;
this.tasks = this.tasks.map(task => {
const merchant = task.merchant || {};
const lat = Number(merchant.lat);
const lng = Number(merchant.lng);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return task;
const meters = calcDistanceMeters(this.lat, this.lng, lat, lng);
return Object.assign({}, task, {
distance: formatDistance(meters)
});
});
},
getFilteredTasks(status) {
let list = this.tasks;
if (this.category) {
list = list.filter(item => item.cateId === this.category);
}
if (this.searchQuery) {
const keyword = this.searchQuery.trim();
list = list.filter(item => item.merchant.name.indexOf(keyword) > -1 || item.title.indexOf(keyword) > -1);
}
if (this.sortType === '热度最高') {
list = [...list].sort((a, b) => this.rewardToNumber(b) - this.rewardToNumber(a));
}
if (this.sortType === '距我最近') {
list = [...list].sort((a, b) => this.distanceToNumber(a.distance) - this.distanceToNumber(b.distance));
}
return list;
},
calReward(item) {
let reward = parseFloat(item.reward)
if (item.extraReward) reward += parseFloat(item.extraReward)
return reward.toFixed(2)
},
rewardToNumber(item) {
if (this.isManualSalesTask(item)) return 0;
const reward = Number(item && item.reward);
const extraReward = Number(item && item.extraReward);
return (Number.isFinite(reward) ? reward : 0) + (Number.isFinite(extraReward) ? extraReward : 0);
},
auditLabel(status) {
if (status === 2) return '审核通过';
if (status === 3) return '审核不通过';
return '待审核';
},
auditClass(status) {
if (status === 2) return 'audit-success';
if (status === 3) return 'audit-danger';
return 'audit-warn';
},
parseJsonMaybe(value) {
if (value === null || value === undefined || value === '') return null;
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch (err) {
return value;
}
}
return value;
},
safeNumber(value) {
const num = Number(value);
return Number.isFinite(num) ? num : 0;
},
getTaskProductList(task) {
const raw = task && task.productList ? task.productList : [];
const parsed = this.parseJsonMaybe(raw);
return Array.isArray(parsed) ? parsed : [];
},
getTaskSalesGroups(task) {
const userTask = task && task.userTask ? task.userTask : {};
const raw = userTask.salesGroups || userTask.sales_groups || task.salesGroups || task.sales_groups || [];
const parsed = this.parseJsonMaybe(raw);
return Array.isArray(parsed) ? parsed : [];
},
calcTotalTaskNum(task) {
const list = this.getTaskProductList(task);
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 = this.getTaskSalesGroups(task);
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(task) {
if (task.cateId === 4) {
return task.userTask.storeList ? JSON.parse(task.userTask.storeList).length : 0;
}
const groups = this.getTaskSalesGroups(task);
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);
},
totalProgressPercent(task) {
let totalTaskNum = this.calcTotalTaskNum(task);
if (task.cateId === 4) {
totalTaskNum = task.storeNum
}
if (!totalTaskNum) return 0;
const totalQty = this.calcTotalQty(task);
return Math.min(100, Math.round((totalQty / totalTaskNum) * 100));
},
totalProgressText(task) {
let totalTaskNum = this.calcTotalTaskNum(task);
if (task.cateId === 4) {
totalTaskNum = task.storeNum
}
const totalQty = this.calcTotalQty(task);
return `${totalQty}/${totalTaskNum || 0}`;
},
isManualSalesTask(task) {
if (!task || task.cateId === 4) return false;
const list = Array.isArray(task.productList) ? task.productList : [];
return list.length === 0;
},
getTaskChainText(task) {
const merchant = task && task.merchant ? task.merchant : {};
const chain = merchant.chain || merchant.chainName || merchant.chainAbb || task.chain || task.chainName || '';
return String(chain || '').trim();
},
showDetails(task) {
let str = `reward=${task.reward}&target=${task.cate.title}&id=${task.id}`
if (task.merchant) str += `&title=${encodeURIComponent(task.merchant.name)}`
uni.navigateTo({
url: `/pages/task/detail?${str}`
})
},
async takeOver(task) {
const res = await addUserTaskReq({
taskId: task.id
}).catch(e => {
uni.showToast({
title: e.message,
icon: 'none'
})
})
if (res) {
uni.showToast({
title: '接单成功',
icon: 'none'
})
this.activeTab = '待执行'
}
},
openDetailPopup(task) {
this.detailTask = task ? Object.assign({}, task, {
chainText: task.chainText || this.getTaskChainText(task)
}) : {};
this.detailPopupVisible = true;
},
openSelfReportCreate() {
// 「自主填报」先选填报类型(驻店填报 / 跑店填报),再进对应表单页
this.jumpWithLogin('/packageUser/self-report/select')
},
openSelfReportRecord() {
this.jumpWithLogin('/packageUser/self-report/records')
},
jumpWithLogin(url) {
if (!getUserInfo().token) {
this.pendingLoginUrl = url
this.$refs.Login.open()
return
}
uni.navigateTo({url})
},
handleLoginDone() {
if (!this.pendingLoginUrl) return
const url = this.pendingLoginUrl
this.pendingLoginUrl = ''
uni.navigateTo({url})
},
closeDetailPopup() {
this.detailPopupVisible = false;
},
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')
},
formatTaskExecutePeriod(task) {
const source = task || {}
const start = this.trimSecond(source.taskStartTime || '')
const end = this.trimSecond(source.taskEndTime || '')
if (start && end) return `${start} 至 ${end}`
if (start) return `${start} 至 -`
if (end) return `- 至 ${end}`
return '暂无时间段'
},
},
onShareTimeline() {
return {
title: '药动销'
}
},
onShareAppMessage() {
return {
title: '药动销',
path: buildSharePath('/pages/index/index')
}
}
}
</script>
<style lang="scss" scoped>
.home-page {
background: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
.top-wrap {
position: relative;
padding: 0 0 32rpx;
}
.top-navbar {
margin-bottom: 20rpx;
}
.top-search {
margin-bottom: 20rpx;
}
.search-bar {
width: calc(100vw - 70rpx);
z-index: 9999;
position: absolute;
display: flex;
align-items: center;
background: #ffffff;
border-radius: 999rpx;
padding: 5rpx 18rpx;
margin: 0 20rpx;
top: 170rpx;
box-shadow: 0 8rpx 18rpx rgba(0, 0, 0, 0.06);
}
.search-icon {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
}
.search-btn {
font-size: 26rpx;
font-weight: 700;
color: #111827;
padding: 6rpx 12rpx;
width: calc(100% - 40rpx);
}
.banner-area {
padding: 0;
overflow: hidden;
border-radius: 0;
//box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.08);
}
.banner-swiper {
height: 320rpx;
}
.banner-img {
width: 100%;
height: 320rpx;
display: block;
}
.board {
margin-top: -5rpx;
background: #f5f5f5;
border-radius: 28rpx 28rpx 0 0;
overflow: hidden;
}
.tabs {
display: flex;
padding: 10rpx 24rpx 18rpx;
//background: #f3fffb;
gap: 12rpx;
}
.tab {
flex: 1;
text-align: center;
padding: 12rpx 0;
color: #6b7280;
font-weight: 700;
position: relative;
font-size: 30rpx;
}
.tab-main {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 10rpx 18rpx;
border-radius: 999rpx;
}
.tab-text {
line-height: 1;
}
/* tab 图标是纯白图形 + 透明底,而 tab 底图接近纯白,
未激活时白图标贴白底看不清,所以垫一层浅绿圆底反衬。
激活态下 .tab-main 自己已填 #2fc6a7,两层绿会透出一圈色差边,
所以激活时改成完全透明,白图标直接坐在药丸底上。 */
.tab-icon-wrap {
flex-shrink: 0;
width: 34rpx;
height: 34rpx;
border-radius: 999rpx;
background: #2fc6a7;
display: flex;
align-items: center;
justify-content: center;
}
.tab.active .tab-icon-wrap {
background: none;
box-shadow: none;
}
.tab-icon {
width: 24rpx;
height: 24rpx;
}
.tab.active .tab-main {
background: #2fc6a7;
color: #ffffff;
}
.tab-badge {
position: absolute;
top: 4rpx;
right: 16rpx;
min-width: 30rpx;
padding: 4rpx 10rpx;
background: #ff3b30;
color: #fff;
border-radius: 999rpx;
font-size: 20rpx;
border: 2rpx solid #ffffff;
}
.tab-underline {
position: absolute;
bottom: 3rpx;
left: 50%;
width: 16rpx;
height: 16rpx;
background: #2fc6a7;
transform: translateX(-50%) rotate(45deg);
border-radius: 4rpx;
}
.tab.active {
color: #d9661f;
}
.search-input {
flex: 1;
font-size: 26rpx;
color: #0f2f52;
}
.search-placeholder {
color: #a1aabc;
}
.category-bar {
white-space: nowrap;
}
.notice-bar {
margin: 20rpx 24rpx 10rpx;
background: #ffffff;
border-radius: 999rpx;
padding: 12rpx 16rpx;
display: flex;
align-items: center;
gap: 10rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.06);
}
.notice-label {
font-size: 24rpx;
font-weight: 700;
color: #2fc6a7;
}
.notice-swiper {
height: 48rpx;
flex: 1;
}
.notice-item {
display: flex;
align-items: center;
}
.notice-text {
font-size: 24rpx;
color: #111827;
font-weight: 500;
}
.notice-more {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 1rpx solid #2fc6a7;
display: flex;
align-items: center;
justify-content: center;
color: #2fc6a7;
}
.notice-more-icon {
font-size: 22rpx;
line-height: 1;
}
.self-report-entry {
margin: 18rpx 24rpx 0;
display: flex;
gap: 14rpx;
}
.entry-btn {
flex: 1;
min-width: 0;
box-sizing: border-box;
padding: 22rpx 20rpx;
border-radius: 24rpx;
border: 1rpx solid #f6dcb4;
background: #fff5e2;
color: #e0631a;
display: flex;
align-items: center;
gap: 14rpx;
}
.entry-btn.primary {
background: linear-gradient(135deg, #f9863c, #f2620f);
border-color: transparent;
color: #ffffff;
box-shadow: 0 8rpx 20rpx rgba(242, 98, 15, 0.25);
}
.entry-icon {
flex-shrink: 0;
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.entry-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.entry-title {
font-size: 32rpx;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
}
.entry-desc {
font-size: 20rpx;
line-height: 1.2;
opacity: 0.72;
white-space: nowrap;
}
.category-wrap {
padding: 4rpx 24rpx 18rpx;
//background: #f3fffb;
}
.category-chip {
display: inline-flex;
align-items: center;
padding: 10rpx 20rpx;
margin-right: 16rpx;
border-radius: 999rpx;
font-size: 28rpx;
background: transparent;
color: #1f2937;
border: 1rpx solid transparent;
}
.category-chip.active {
background: #ff8a00;
color: #fff;
border-color: #ff8a00;
}
.filter-row {
margin: 0 24rpx 8rpx;
padding: 12rpx 0;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: space-between;
color: #5c6c84;
}
.location {
display: flex;
align-items: center;
gap: 8rpx;
font-weight: 700;
color: #111827;
}
.sort-options {
display: flex;
gap: 12rpx;
font-size: 26rpx;
color: #2fc6a7;
}
.sort-item {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 14rpx;
border-radius: 999rpx;
border: 1rpx solid #2fc6a7;
color: #2fc6a7;
font-size: 22rpx;
}
.sort-item.active {
background: #2fc6a7;
color: #ffffff;
}
.sort-arrow {
font-size: 18rpx;
line-height: 1;
}
.task-list {
padding: 12rpx 24rpx 32rpx;
background: #f5f5f5;
}
.task-card {
background: #ffffff;
border-radius: 24rpx;
padding: 22rpx 20rpx;
margin-bottom: 18rpx;
border: 1rpx solid #eef2f7;
box-shadow: 0 8rpx 20rpx rgba(37, 99, 235, 0.06);
}
.task-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.date-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.date {
font-weight: 700;
color: #1f2937;
font-size: 28rpx;
}
.relative {
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: #fff7ed;
color: #f97316;
font-size: 22rpx;
border: 1rpx solid #fdba74;
}
.cate-tag {
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: #eff6ff;
color: #2563eb;
font-size: 22rpx;
border: 1rpx solid #93c5fd;
}
.reward {
display: flex;
align-items: baseline;
font-weight: 800;
color: #ff3b30;
}
.currency {
font-size: 24rpx;
margin-right: 6rpx;
}
.reward-number {
font-size: 34rpx;
}
.task-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 12rpx;
gap: 12rpx;
}
.task-title {
font-size: 30rpx;
font-weight: 700;
color: #0f172a;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chain-pill {
max-width: 220rpx;
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: #ecfdf5;
color: #059669;
border: 1rpx solid #a7f3d0;
font-size: 22rpx;
font-weight: 700;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.type-pill {
display: none;
}
.type-blue {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
}
.type-purple {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
}
.type-orange {
background: linear-gradient(135deg, #f97316, #ea580c);
}
.task-meta {
margin-top: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
color: #6b7280;
font-size: 24rpx;
gap: 12rpx;
}
.addr {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.distance {
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 10rpx;
border-radius: 12rpx;
color: #4b5563;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-top: 12rpx;
}
.tag {
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: #f0f4ff;
color: var(--brand-primary);
font-size: 22rpx;
font-weight: 600;
border: 1rpx solid #d8e2ff;
}
.audit-box {
margin-top: 14rpx;
padding: 14rpx;
background: #f8fafc;
border-radius: 14rpx;
border: 1rpx solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
}
.audit-status {
display: inline-flex;
align-items: center;
padding: 8rpx 14rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: 700;
}
.audit-success {
background: #ecfdf3;
color: #15803d;
border: 1rpx solid #bbf7d0;
}
.audit-danger {
background: #fef2f2;
color: #dc2626;
border: 1rpx solid #fecdd3;
}
.audit-warn {
background: #fffbeb;
color: #d97706;
border: 1rpx solid #fde68a;
}
.audit-reason {
display: block;
margin-top: 8rpx;
color: #dc2626;
font-size: 22rpx;
}
.total-progress-card {
margin-top: 14rpx;
padding: 12rpx 14rpx;
background: #f8fafc;
border-radius: 14rpx;
border: 1rpx solid #e5e7eb;
}
.total-progress-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
}
.total-progress-title {
font-size: 26rpx;
font-weight: 700;
color: #0f172a;
}
.total-progress-value {
font-size: 22rpx;
color: #64748b;
}
.total-progress-bar {
height: 10rpx;
border-radius: 999rpx;
background: #e2e8f0;
overflow: hidden;
}
.total-progress-fill {
height: 100%;
background: linear-gradient(90deg, #2fc6a7, #a1ded0);
}
.actions {
margin-top: 16rpx;
display: flex;
gap: 12rpx;
}
.primary-action,
.action-btn,
.info-action,
.ghost-action {
text-align: center;
padding: 18rpx 16rpx;
border-radius: 999rpx;
font-weight: 800;
font-size: 26rpx;
}
.primary-action {
flex: 2;
background: #2fc6a7;
color: #ffffff;
box-shadow: 0 8rpx 18rpx rgba(47, 198, 167, 0.25);
}
.ghost-action {
flex: 1;
background: #ffffff;
color: #2fc6a7;
border: 1rpx solid #2fc6a7;
}
.info-action {
width: 100rpx;
padding: 10rpx 20rpx;
font-size: 24rpx;
border: 1px solid rgb(75, 85, 99);
color: rgb(75, 85, 99);
}
.action-btn {
flex: 1;
background: linear-gradient(to right, #f97316, #ef4444);
color: #ffffff;
border: none;
box-shadow: 0 8rpx 18rpx #ffedd5;
}
.primary-action.full,
.ghost-action.full {
flex: 1;
}
.empty {
padding: 120rpx 0;
text-align: center;
color: #9ca3af;
}
.empty-icon {
font-size: 64rpx;
margin-bottom: 16rpx;
}
.empty-text {
font-size: 26rpx;
}
.detail-popup {
padding: 36rpx 32rpx 28rpx;
max-height: 78vh;
overflow: auto;
}
.detail-popup-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 26rpx;
}
.detail-popup-title {
font-size: 38rpx;
font-weight: 800;
color: #0f172a;
}
.detail-popup-close {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
}
.detail-merchant-card {
margin-bottom: 22rpx;
background: #f8fafc;
border: 1rpx solid #e5e7eb;
border-radius: 20rpx;
padding: 18rpx 20rpx;
}
.detail-merchant-row {
display: flex;
align-items: center;
gap: 18rpx;
min-width: 0;
}
.detail-merchant-row + .detail-merchant-row {
margin-top: 14rpx;
}
.detail-merchant-label {
flex-shrink: 0;
width: 132rpx;
color: #64748b;
font-size: 24rpx;
}
.detail-merchant-value {
flex: 1;
min-width: 0;
color: #0f172a;
font-size: 28rpx;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.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;
}
.nav-wrap {
background: url("https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260127/a0fceca8f9e439969e0e0a9a799b4db2.png");
background-size: cover;
background-position: bottom center;
border-radius: 20rpx;
padding: 10rpx;
//position: absolute;
//left: 20rpx;
//top: 635rpx;
//width: calc(100vw - 60rpx);
margin: 0 20rpx 20rpx;
}
.detail-icon {
width: 44rpx;
height: 44rpx;
border-radius: 12rpx;
//background: #d1fae5;
display: flex;
align-items: center;
justify-content: center;
}
.detail-block-title {
font-size: 30rpx;
font-weight: 700;
color: #10b981;
}
.detail-block-text {
font-size: 26rpx;
color: #059669;
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: #10b981;
}
.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;
}
</style>