Files
yxe-mp/pages/index/index.vue
T
weicw-dshsa 945cf9c104 refactor(UI): 首页 tab 与自主填报入口图标改用 uview 矢量图标
e192296 把纯白透明底图片图标垫了深色底块反衬,解决了「白底上看不见」,
但本质是靠底色绕开图源问题,选中态也就只能靠加深底色表达。

改用 u-icon 矢量图标后可直接着色:
- 首页 tab:未选中灰色 #6b7280,选中白色;图标按语义取
  order / clock / checkmark-circle
- 自主填报入口:未选中浅绿底 + 绿色图标,选中深绿底 + 白色图标;
  store/visit 取 home-fill / car-fill

顺带删掉两处为图片补正尺寸的样式(.tab-icon-wrap 白圆底、
.type-icon/.type-icon-store 的宽高补正),矢量图标不需要。

图标名已对照 uview 图标库校验(u-icon 会自动补 uicon- 前缀)。
2026-10-12 18:22:25 +08:00

1447 lines
36 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">
<u-icon :name="tabIcon(tab)" size="15"
:color="activeTab === tab ? '#ffffff' : '#6b7280'"/>
<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: {
// tab 图标:新任务=任务清单、待执行=时钟、已完成=对勾
tabIcon(tab) {
if (tab === '新任务') return 'order'
if (tab === '待执行') return 'clock'
return 'checkmark-circle'
},
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 原先用纯白 PNG 图标,而 tab 底图接近纯白(平均亮度 250/255),
未激活时白图标贴白底完全看不清;且原图语义也不对(「新任务」是 NEW 徽章、
「待执行」是时钟)。改用 uview 矢量图标,可随激活态着色,无底色依赖。 */
.tab-icon {
width: 28rpx;
height: 28rpx;
}
.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>