style(rider): 优化骑手中心界面及功能入口样式

- 添加骑手订单统计数据,支持待取货、配送中、已完成与今日任务
- 增加刷新按钮,便捷更新统计数据
- 优化头像及用户信息展示,增加主题样式和背景装饰
- 新增快捷功能入口,展示待取货订单数量红点提醒
- 登录未授权时提示并引导登录
- 调整色彩风格,将多处组件背景色由浅蓝色调整为深蓝色,提高视觉一致性
- 管理页新增内容管理工具的基础UI布局
- 扫码确认页重构授权登录页面,改用科技风格设计,增加动态粒子动画和渐变光晕效果
- 协议打开方式改为跳转网页视图,移除旧版弹窗
- 修正部分组件样式,调整按钮及文本颜色,提升视觉效果和交互体验
This commit is contained in:
2026-04-10 19:36:37 +08:00
parent d87e9d3f13
commit 4507cd484e
33 changed files with 1498 additions and 272 deletions

View File

@@ -11,7 +11,29 @@
"usedAt": 1775579277895,
"industryId": "all"
}
],
"c02960235460481c84382169b26bf90c": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775818515062,
"industryId": "all"
}
],
"5bc2686c5a3a4f6d98f74a1467d05be8": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775819465726,
"industryId": "all"
}
]
},
"lastUpdated": 1775586324678
"lastUpdated": 1775819525128
}

View File

@@ -0,0 +1,61 @@
# 2026-04-10 工作记录
## qr-confirm 登录页优化
- 修改文件:`src/passport/qr-confirm/index.tsx`
- 授权按钮改为全宽 52px 圆角大按钮inline style易于点击
- 勾选框外层包裹 padding:8px 的热区 View点击更容易命中
- 协议从内嵌弹窗改为跳转 WebView 页面打开网页版:
- 服务协议https://websopy.websoft.top/agreement
- 隐私政策https://websopy.websoft.top/privacy
- 新增通用 WebView 页面:`src/passport/webview/index.tsx`
- `src/app.config.ts` passport 子包新增 `webview/index` 路由
## qr-confirm 按钮颜色修复
- 问题NutUI `Button` 组件内置样式优先级高,覆盖了 `style` 中的 `background` 渐变色,按钮颜色不显示
- 修复:改用小程序原生 `button` 标签,`open-type="getPhoneNumber"` 照常有效,渐变背景正常显示
## template-5 vs PC端 功能对比分析
- 对比了三个项目template-5(小程序) vs websopy-pc(PC端/Nuxt3) vs websopy-java(后端)
- PC端定位SaaS平台门户 + 用户控制台 + 开发者中心 + 管理后台
- 小程序端定位:移动端入口 + 电商购物(首页偏营销展示)
- **关键发现**
- Bug: UserGrid.tsx 开发者中心入口跳转到 /rider/index骑手应修正
- 缺失核心功能:通知中心、工单系统、应用/产品订阅管理
- 购买断链:产品展示后无法在小程序完成购买
- admin 模块为空壳rider 功能极简
- PC端开发者专属功能(CI/CD/Git/源码)建议通过 WebView 桥接
- 输出完整修改计划报告4阶段预估7-12天
## 小程序端阶段一+二修改执行
### Bug 修复 + 代码清理
- **UserGrid.tsx**:修复"开发者中心"跳转到 /rider/index 的 Bug改为通过 WebView 加载 PC 端开发者中心
- **UserGrid.tsx**清理全部注释代码约60行代码整洁度大幅提升
- **admin/index.tsx**:从空壳"待开发"改为内容管理入口页,保留文章管理功能
### 骑手中心优化
- **rider/index.tsx**:增加今日统计概览(待取货/配送中/已完成/今日任务)
- 增加快捷功能入口、待取货/配送中数量徽标、刷新按钮、空状态展示
### 新增通知中心
- API层`src/api/app/notification/model/index.ts` + `src/api/app/notification/index.ts`
- 页面:`src/user/notification/index.tsx` - 支持分页加载、类型标签、已读标记、全部已读、空状态
### 新增工单系统3个页面
- API层`src/api/app/ticket/model/index.ts` + `src/api/app/ticket/index.ts`
- 列表页:`src/user/ticket/index.tsx` - 统计概览、状态筛选、分页加载
- 提交页:`src/user/ticket/create.tsx` - 标题/分类/优先级/描述表单
- 详情页:`src/user/ticket/detail.tsx` - 查看详情、回复、关闭工单
### 新增我的应用页面
- API层`src/api/app/appProduct/model/index.ts` + `src/api/app/appProduct/index.ts`
- 页面:`src/user/apps/index.tsx` - 应用列表、状态展示、管理后台/前台入口、到期时间
### UserGrid 新增入口
- 我的应用、我的工单、消息通知3个新图标入口
### 路由注册
- `app.config.ts` user 子包新增notification/index, ticket/index, ticket/create, ticket/detail, apps/index

View File

@@ -0,0 +1,33 @@
{
"name": "_auto_c02960235460481c84382169b26bf90c",
"leadAgentId": "c02960235460481c84382169b26bf90c",
"workspacePath": "/Users/gxwebsoft/VUE/template-5",
"createdAt": "2026-04-10T10:58:09.075Z",
"options": {
"workspacePath": "/Users/gxwebsoft/VUE/template-5",
"isAutoTeam": true
},
"members": [
{
"memberId": "team-lead@_auto_c02960235460481c84382169b26bf90c",
"name": "team-lead",
"role": "Team Lead - coordinates the team and interacts with the user"
},
{
"memberId": "miniapp-explorer@_auto_c02960235460481c84382169b26bf90c",
"name": "miniapp-explorer",
"role": "分析小程序端功能模块"
},
{
"memberId": "pc-explorer@_auto_c02960235460481c84382169b26bf90c",
"name": "pc-explorer",
"role": "分析PC端功能模块"
},
{
"memberId": "java-explorer@_auto_c02960235460481c84382169b26bf90c",
"name": "java-explorer",
"role": "分析Java后端API接口"
}
],
"isAutoTeam": true
}

View File

@@ -0,0 +1,11 @@
[
{
"id": "msg-1775818705614-am57es",
"from": "team-lead",
"to": "java-explorer",
"type": "message",
"content": "请继续执行你的分析任务,完成后把结果发给我。",
"timestamp": "2026-04-10T10:58:25.614Z",
"read": true
}
]

View File

@@ -0,0 +1,11 @@
[
{
"id": "msg-1775818702473-ypuevm",
"from": "team-lead",
"to": "miniapp-explorer",
"type": "message",
"content": "请继续执行你的分析任务,完成后把结果发给我。",
"timestamp": "2026-04-10T10:58:22.473Z",
"read": true
}
]

View File

@@ -0,0 +1,11 @@
[
{
"id": "msg-1775818704022-ii1chz",
"from": "team-lead",
"to": "pc-explorer",
"type": "message",
"content": "请继续执行你的分析任务,完成后把结果发给我。",
"timestamp": "2026-04-10T10:58:24.022Z",
"read": true
}
]

File diff suppressed because one or more lines are too long

View File

@@ -1,34 +1,48 @@
import {useEffect} from 'react'
import {useUser} from "@/hooks/useUser";
import {Empty} from '@nutui/nutui-react-taro';
import {Text} from '@tarojs/components';
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { ConfigProvider, Grid } from '@nutui/nutui-react-taro'
import { useUser } from '@/hooks/useUser'
import { useThemeStyles } from '@/hooks/useTheme'
import { Edit } from '@nutui/icons-react-taro'
function Admin() {
const {
isAdmin
} = useUser();
useEffect(() => {
}, []);
const { isAdmin } = useUser()
const themeStyles = useThemeStyles()
if (!isAdmin()) {
return (
<Empty
description="您不是管理员"
imageSize={80}
style={{
backgroundColor: 'transparent',
height: 'calc(100vh - 200px)'
}}
>
</Empty>
);
<View className="bg-gray-100 min-h-screen flex items-center justify-center">
<View className="text-center">
<Text className="text-gray-400 text-sm">访</Text>
</View>
</View>
)
}
return (
<>
<Text>...</Text>
</>
<View className="bg-gray-100 min-h-screen">
<View className="px-4 py-6" style={themeStyles.primaryBackground}>
<Text className="text-white text-lg font-bold"></Text>
<View className="mt-1">
<Text className="text-white text-sm opacity-80"></Text>
</View>
</View>
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<Text className="font-semibold text-gray-800 mb-4 block"></Text>
<ConfigProvider>
<Grid columns={4}>
<Grid.Item text="文章管理" onClick={() => Taro.navigateTo({ url: '/admin/article/index' })}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Edit color="#3b82f6" size="20" />
</View>
</View>
</Grid.Item>
</Grid>
</ConfigProvider>
</View>
</View>
)
}

View File

@@ -0,0 +1,19 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { AppProduct } from './model';
const BASE = '/app/product';
/** 分页查询我的应用 */
export async function pageMyApps(params?: { current?: number; size?: number }) {
const res = await request.get<ApiResult<PageResult<AppProduct>>>(BASE + '/my/page', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取我参与的应用列表 */
export async function listJoinedApps(params?: { page?: number; limit?: number }) {
const res = await request.get<ApiResult<PageResult<AppProduct>>>(BASE + '/page', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,51 @@
export interface AppProduct {
productId?: number
productName?: string
productCode?: string
appType?: number
icon?: string
qrcode?: string
description?: string
domain?: string
homeUrl?: string
adminUrl?: string
version?: string
status?: number
statusText?: string
running?: number
expirationTime?: string
createTime?: string
updateTime?: string
publishStatus?: string
}
export const APP_TYPE_NAME: Record<number, string> = {
10: '网站',
20: '微信小程序',
30: '抖音小程序',
40: '百度小程序',
50: '支付宝小程序',
60: 'Android APP',
70: 'iOS APP',
80: 'macOS 应用',
90: 'Windows 应用',
100: '插件',
}
export const STATUS_NAME: Record<number, string> = {
0: '未开通',
1: '运行中',
2: '维护中',
3: '已关闭',
4: '已欠费',
5: '违规停机',
}
export const STATUS_COLOR: Record<number, string> = {
0: '#6b7280',
1: '#10b981',
2: '#f59e0b',
3: '#ef4444',
4: '#ef4444',
5: '#ef4444',
}

View File

@@ -0,0 +1,47 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Notification, NotificationParam, UnreadCountResult } from './model';
const BASE = '/app/notification';
/** 分页查询通知列表 */
export async function pageNotification(params: NotificationParam) {
const res = await request.get<ApiResult<PageResult<Notification>>>(BASE + '/page', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 查询最近通知 */
export async function listRecentNotification(params?: { type?: string; limit?: number }) {
const res = await request.get<ApiResult<Notification[]>>(BASE + '/recent', { params });
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取未读数量统计 */
export async function getUnreadCount() {
const res = await request.get<ApiResult<UnreadCountResult>>(BASE + '/unread-count');
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 标记单条通知为已读 */
export async function markNotificationRead(id: number) {
const res = await request.put<ApiResult<unknown>>(BASE + `/read/${id}`);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}
/** 标记全部已读 */
export async function markAllNotificationRead(data?: { type?: string }) {
const res = await request.put<ApiResult<unknown>>(BASE + '/read-all', data);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}
/** 删除单条通知 */
export async function removeNotification(id: number) {
const res = await request.del<ApiResult<unknown>>(BASE + '/' + id);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,41 @@
import type { PageParam } from '@/api/index';
/** 通知类型 */
export type NotificationType = 'ticket' | 'review' | 'system' | 'resource' | 'permission' | 'member' | 'payment';
/** 通知实体 */
export interface Notification {
id?: number;
userId?: number;
type?: NotificationType;
title?: string;
content?: string;
isRead?: number;
refId?: number;
refType?: string;
linkUrl?: string;
senderId?: number;
senderName?: string;
senderAvatar?: string;
tenantId?: number;
createTime?: string;
updateTime?: string;
}
/** 通知查询参数 */
export interface NotificationParam extends PageParam {
type?: NotificationType | '';
isRead?: number;
}
/** 未读统计 */
export interface UnreadCountResult {
total?: number;
ticket?: number;
review?: number;
system?: number;
resource?: number;
permission?: number;
member?: number;
payment?: number;
}

View File

@@ -0,0 +1,54 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { Ticket, TicketReply, TicketSubmitForm, TicketQueryParams, TicketStats } from './model';
const BASE = '/app/ticket';
/** 查询我的工单(分页) */
export async function myTickets(params: TicketQueryParams) {
const res = await request.get<ApiResult<PageResult<Ticket>>>(BASE + '/my', { params });
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 提交工单 */
export async function submitTicket(data: TicketSubmitForm) {
const res = await request.post<ApiResult<Ticket>>(BASE + '/submit', data);
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取工单详情 */
export async function getTicket(ticketId: number) {
const res = await request.get<ApiResult<Ticket>>(BASE + '/' + ticketId);
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 关闭工单 */
export async function closeTicket(ticketId: number) {
const res = await request.put<ApiResult<unknown>>(BASE + `/${ticketId}/close`);
if (res.code === 0) return res.message;
return Promise.reject(new Error(res.message));
}
/** 获取工单回复列表 */
export async function getTicketReplies(ticketId: number) {
const res = await request.get<ApiResult<TicketReply[]>>(BASE + `/${ticketId}/replies`);
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}
/** 提交工单回复 */
export async function replyTicket(data: { ticketId: number; content: string }) {
const res = await request.post<ApiResult<TicketReply>>(BASE + '/reply', data);
if (res.code === 0) return res.data;
return Promise.reject(new Error(res.message));
}
/** 获取工单统计 */
export async function getTicketStats() {
const res = await request.get<ApiResult<TicketStats>>(BASE + '/stats');
if (res.code === 0 && res.data) return res.data;
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,64 @@
export type TicketPriority = 'low' | 'normal' | 'high' | 'urgent'
export type TicketStatus = 'pending' | 'assigned' | 'processing' | 'resolved' | 'closed' | 'rejected'
export type TicketCategory = 'bug' | 'feature' | 'consultation' | 'complaint' | 'other'
export interface Ticket {
ticketId?: number
ticketNo?: string
title?: string
content?: string
productId?: number
productName?: string
category?: TicketCategory
priority?: TicketPriority
status?: TicketStatus
attachments?: string[]
submitUserId?: number
submitUserName?: string
submitUserAvatar?: string
assigneeId?: number
assigneeName?: string
assigneeAvatar?: string
createTime?: string
updateTime?: string
resolvedTime?: string
closedTime?: string
replyCount?: number
hasUnread?: boolean
}
export interface TicketReply {
replyId?: number
ticketId?: number
content?: string
attachments?: string[]
userId?: number
userName?: string
userAvatar?: string
isStaff?: boolean
createTime?: string
}
export interface TicketSubmitForm {
title: string
content: string
productId?: number
category: TicketCategory
priority: TicketPriority
}
export interface TicketQueryParams {
status?: TicketStatus
category?: TicketCategory
keywords?: string
page?: number
limit?: number
}
export interface TicketStats {
total?: number
pending?: number
processing?: number
resolved?: number
closed?: number
}

View File

@@ -18,7 +18,8 @@ export default {
"phone-auth/index",
'qr-login/index',
'qr-confirm/index',
'unified-qr/index'
'unified-qr/index',
'webview/index'
]
},
{
@@ -65,7 +66,9 @@ export default {
"chat/conversation/index",
"chat/message/index",
"chat/message/add",
"chat/message/detail"
"chat/message/detail",
"notification/index",
"apps/index"
]
},
{

View File

@@ -44,16 +44,12 @@ button[open-type="getPhoneNumber"] {
padding: 0 !important;
margin: 0 !important;
border: none !important;
line-height: inherit !important;
border-radius: 0 !important;
}
button[open-type="chooseAvatar"] {
background: none !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
line-height: inherit !important;
border-radius: 0 !important;
}
.buy-btn{
@@ -101,3 +97,73 @@ image {
.admin-feature-item:active {
transform: scale(0.95);
}
/* 科技风格页面动画 */
/* 粒子动画 */
@keyframes float {
0%, 100% {
transform: translateY(0) scale(1);
opacity: 1;
}
50% {
transform: translateY(-15px) scale(1.1);
opacity: 0.7;
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.5);
opacity: 0.5;
}
}
@keyframes scanline {
0% {
top: 0;
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
top: 100%;
opacity: 0;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes glow {
0%, 100% {
box-shadow: 0 0 20px rgba(168, 85, 247, 0.4), 0 0 40px rgba(168, 85, 247, 0.2);
}
50% {
box-shadow: 0 0 30px rgba(168, 85, 247, 0.6), 0 0 60px rgba(168, 85, 247, 0.3);
}
}
/* 粒子元素样式 */
.particle {
animation: float 3s ease-in-out infinite, pulse 2s ease-in-out infinite;
}
/* 按钮发光效果 */
.authorize-btn:not([disabled]) {
animation: glow 2s ease-in-out infinite;
}

View File

@@ -30,7 +30,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
title: '统一扫码',
description: '扫码登录和核销一体化功能',
icon: <Scan className="text-blue-500" size="24" />,
color: 'bg-blue-50 border-blue-200',
color: 'bg-blue-500 border-blue-200',
onClick: () => {
navTo('/passport/unified-qr/index', true);
onClose?.();

View File

@@ -211,7 +211,7 @@ const DealerIndex: React.FC = () => {
>
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>

View File

@@ -137,7 +137,7 @@ const InviteStatsPage: React.FC = () => {
</View>
) : inviteStats ? (
<View className="grid grid-cols-2 gap-4">
<View className="text-center p-4 bg-blue-50 rounded-xl">
<View className="text-center p-4 bg-blue-500 rounded-xl">
<ArrowUp size="24" className="text-blue-500 mx-auto mb-2" />
<Text className="text-2xl font-bold text-blue-600">
{inviteStats.totalInvites || 0}

View File

@@ -211,7 +211,7 @@ const DealerIndex: React.FC = () => {
>
<Grid.Item text="配送订单" onClick={() => navigateToPage('/rider/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>

View File

@@ -7,13 +7,12 @@ import {
Location,
Tips,
Ask,
// Dongdong,
People,
// AfterSaleService,
Logout,
Shop,
Jdl,
Service
Service,
Message,
Apps
} from '@nutui/icons-react-taro'
import {useUser} from "@/hooks/useUser";
@@ -26,7 +25,6 @@ const UserCell = () => {
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 useUser hook 的 logoutUser 方法
logoutUser();
Taro.reLaunch({
url: '/pages/index/index'
@@ -54,7 +52,7 @@ const UserCell = () => {
{hasRole('store') && (
<Grid.Item text="门店中心" onClick={() => navTo('/store/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shop color="#3b82f6" size="20"/>
</View>
</View>
@@ -62,10 +60,10 @@ const UserCell = () => {
)}
{hasRole('developer') && (
<Grid.Item text="开发者中心" onClick={() => navTo('/rider/index', true)}>
<Grid.Item text="开发者中心" onClick={() => navTo('/passport/webview/index?url=https://websopy.websoft.top/developer', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Jdl color="#3b82f6" size="20"/>
<View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Apps color="#6366f1" size="20"/>
</View>
</View>
</Grid.Item>
@@ -81,6 +79,22 @@ const UserCell = () => {
</Grid.Item>
)}
<Grid.Item text="我的应用" onClick={() => navTo('/user/apps/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-violet-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Apps color="#8b5cf6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text="消息通知" onClick={() => navTo('/user/notification/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-rose-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Message color="#f43f5e" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text="收货地址" onClick={() => navTo('/user/address/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">
@@ -89,15 +103,14 @@ const UserCell = () => {
</View>
</Grid.Item>
<Grid.Item text={'常见问题'} onClick={() => navTo('/user/help/index')}>
<Grid.Item text="常见问题" onClick={() => navTo('/user/help/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Ask className={'text-cyan-500'} size="20"/>
<Ask className="text-cyan-500" size="20"/>
</View>
</View>
</Grid.Item>
{/* 修改联系我们为微信客服 */}
<Grid.Item text="联系我们">
<Button
open-type="contact"
@@ -105,13 +118,13 @@ const UserCell = () => {
hover-class="none"
style={{border: 'none'}}
>
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Service color="#67C23A" size="20"/>
</View>
</Button>
</Grid.Item>
<Grid.Item text={'实名认证'} onClick={() => navTo('/user/userVerify/index', true)}>
<Grid.Item text="实名认证" onClick={() => navTo('/user/userVerify/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<ShieldCheck color="#10b981" size="20"/>
@@ -119,7 +132,7 @@ const UserCell = () => {
</View>
</Grid.Item>
<Grid.Item text={'推广邀请'} onClick={() => navTo('/dealer/team/index', true)}>
<Grid.Item text="推广邀请" onClick={() => navTo('/dealer/team/index', true)}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#8b5cf6" size="20"/>
@@ -127,35 +140,18 @@ const UserCell = () => {
</View>
</Grid.Item>
{/*<Grid.Item text={'我的邀请码'} onClick={() => navTo('/dealer/qrcode/index', true)}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Dongdong color="#f59e0b" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/*</Grid.Item>*/}
{/*<Grid.Item text={'管理中心'} onClick={() => navTo('/admin/index', true)}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <AfterSaleService className={'text-red-500'} size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/*</Grid.Item>*/}
<Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>
<Grid.Item text="关于我们" onClick={() => navTo('/user/about/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Tips className={'text-amber-500'} size="20"/>
<Tips className="text-amber-500" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'安全退出'} onClick={onLogout}>
<Grid.Item text="安全退出" onClick={onLogout}>
<View className="text-center">
<View className="w-12 h-12 bg-pink-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Logout className={'text-pink-500'} size="20"/>
<Logout className="text-pink-500" size="20"/>
</View>
</View>
</Grid.Item>
@@ -163,53 +159,6 @@ const UserCell = () => {
</Grid>
</ConfigProvider>
</View>
{/*<View className="bg-white mx-4 mt-4 rounded-xl">*/}
{/* <View className="font-semibold text-gray-800 pt-4 pl-4">账号管理</View>*/}
{/* <ConfigProvider>*/}
{/* <Grid*/}
{/* columns={4}*/}
{/* className="no-border-grid"*/}
{/* style={{*/}
{/* '--nutui-grid-border-color': 'transparent',*/}
{/* '--nutui-grid-item-border-width': '0px',*/}
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/* >*/}
{/* <Grid.Item text="账号安全" onClick={() => navTo('/user/profile/profile', true)}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <ShoppingAdd color="#3b82f6" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text="切换主题" onClick={() => navTo('/user/theme/index', true)}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Location color="#3b82f6" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={'关于我们'} onClick={() => navTo('/user/about/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Tips className={'text-amber-500'} size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={'安全退出'} onClick={onLogout}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-pink-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Logout className={'text-pink-500'} size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* </Grid>*/}
{/* </ConfigProvider>*/}
{/*</View>*/}
</>
)
}

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { View, Text } from '@tarojs/components';
import { Loading, Card, Button } from '@nutui/nutui-react-taro';
import { Success, Failure, Tips, User, Checklist } from '@nutui/icons-react-taro';
import { Success, Failure, Tips, User } from '@nutui/icons-react-taro';
import Taro, { useRouter } from '@tarojs/taro';
import { confirmQRLogin } from '@/api/passport/qr-login';
import { loginByOpenId } from '@/api/passport/wx-login';
@@ -51,7 +51,7 @@ interface LoginResponse {
message: string;
}
// 协议弹窗类型
// 协议类型
type AgreementType = 'service' | 'privacy' | null;
const QRConfirmPage: React.FC = () => {
@@ -65,7 +65,6 @@ const QRConfirmPage: React.FC = () => {
const [needAuth, setNeedAuth] = useState(false); // 是否需要手机号授权
const [authLoading, setAuthLoading] = useState(false); // 授权中状态
const [agreementChecked, setAgreementChecked] = useState(false); // 协议勾选状态
const [showAgreementPopup, setShowAgreementPopup] = useState<AgreementType>(null); // 协议弹窗
useEffect(() => {
// 从 URL 参数中获取 token
@@ -104,7 +103,7 @@ const QRConfirmPage: React.FC = () => {
/**
* 自动确认登录URL 扫码场景)
*/
const handleAutoConfirm = async (loginToken: string) => {
const handleAutoConfirm = async (_: string) => {
try {
setLoading(true);
@@ -406,140 +405,288 @@ const QRConfirmPage: React.FC = () => {
});
};
// 打开协议弹窗
// 打开协议页面(使用网页版)
const openAgreement = (type: AgreementType) => {
setShowAgreementPopup(type);
const urlMap = {
service: 'https://websopy.websoft.top/agreement',
privacy: 'https://websopy.websoft.top/privacy',
};
if (!type) return;
const targetUrl = encodeURIComponent(urlMap[type]);
Taro.navigateTo({
url: `/passport/webview/index?url=${targetUrl}`
});
};
// 关闭协议弹窗
const closeAgreement = () => {
setShowAgreementPopup(null);
};
// 渲染协议弹窗内容
const renderAgreementContent = () => {
if (showAgreementPopup === 'service') {
return (
<View className="p-4">
<View className="text-center mb-4">
<Text className="text-lg font-bold"></Text>
</View>
<View className="text-sm text-gray-600 leading-relaxed max-h-64 overflow-y-auto">
<Text className="block mb-2">使</Text>
<Text className="block mb-2">1. </Text>
<Text className="block mb-2">使</Text>
<Text className="block mb-2">2. </Text>
<Text className="block mb-2">使</Text>
<Text className="block mb-2">3. 使</Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">4. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">5. </Text>
<Text className="block mb-2"></Text>
</View>
<Button
className="mt-4 bg-orange-500 text-white rounded-full"
onClick={closeAgreement}
>
</Button>
</View>
);
}
if (showAgreementPopup === 'privacy') {
return (
<View className="p-4">
<View className="text-center mb-4">
<Text className="text-lg font-bold"></Text>
</View>
<View className="text-sm text-gray-600 leading-relaxed max-h-64 overflow-y-auto">
<Text className="block mb-2"></Text>
<Text className="block mb-2">1. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">2. 使</Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">3. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">4. </Text>
<Text className="block mb-2"></Text>
<Text className="block mb-2">5. </Text>
<Text className="block mb-2"></Text>
</View>
<Button
className="mt-4 bg-orange-500 text-white rounded-full"
onClick={closeAgreement}
>
</Button>
</View>
);
}
return null;
};
// 授权登录页面(参考权大师风格)
// 授权登录页面 - 科技风格
const renderAuthPage = () => {
return (
<View className="min-h-screen bg-gray-50 flex flex-col">
<View className="min-h-screen relative overflow-hidden" style={{ background: 'linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)' }}>
{/* Logo 区域 */}
<View className="flex-1 flex flex-col items-center justify-center px-8 -mt-20">
{/* Logo */}
<View className="w-20 h-20 mb-4">
{/* 背景科技元素 */}
{/* 1. 网格背景 */}
<View style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: `
linear-gradient(rgba(138, 43, 226, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(138, 43, 226, 0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
opacity: 0.5,
}} />
{/* 2. 渐变光晕 - 左上 */}
<View style={{
position: 'absolute',
top: '-30%',
left: '-20%',
width: '60%',
height: '60%',
background: 'radial-gradient(circle, rgba(147, 51, 234, 0.3) 0%, transparent 70%)',
filter: 'blur(40px)',
}} />
{/* 3. 渐变光晕 - 右下 */}
<View style={{
position: 'absolute',
bottom: '-20%',
right: '-20%',
width: '50%',
height: '50%',
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.25) 0%, transparent 70%)',
filter: 'blur(50px)',
}} />
{/* 4. 动态粒子光点 */}
{[
{ top: '15%', left: '20%', size: 4, delay: '0s' },
{ top: '25%', left: '80%', size: 3, delay: '0.5s' },
{ top: '40%', left: '15%', size: 5, delay: '1s' },
{ top: '35%', left: '85%', size: 3, delay: '1.5s' },
{ top: '55%', left: '10%', size: 4, delay: '2s' },
{ top: '60%', left: '90%', size: 3, delay: '0.3s' },
{ top: '75%', left: '25%', size: 4, delay: '1.2s' },
{ top: '80%', left: '75%', size: 5, delay: '0.8s' },
].map((particle, index) => (
<View
key={index}
className="particle"
style={{
position: 'absolute',
top: particle.top,
left: particle.left,
width: particle.size,
height: particle.size,
borderRadius: '50%',
background: index % 2 === 0 ? '#a855f7' : '#3b82f6',
boxShadow: index % 2 === 0
? '0 0 10px rgba(168, 85, 247, 0.8), 0 0 20px rgba(168, 85, 247, 0.4)'
: '0 0 10px rgba(59, 130, 246, 0.8), 0 0 20px rgba(59, 130, 246, 0.4)',
animationDelay: particle.delay,
}}
/>
))}
{/* 5. 扫描线效果 */}
<View style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '2px',
background: 'linear-gradient(90deg, transparent, rgba(168, 85, 247, 0.6), transparent)',
animation: 'scanline 3s ease-in-out infinite',
}} />
{/* 主内容区域 */}
<View className="relative z-10 flex flex-col items-center justify-center min-h-screen px-8">
{/* Logo 区域 */}
<View className="flex flex-col items-center mb-12">
{/* 科技感 Logo 容器 */}
<View style={{
width: '100px',
height: '100px',
borderRadius: '24px',
background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.2) 0%, rgba(59, 130, 246, 0.2) 100%)',
border: '1px solid rgba(168, 85, 247, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '20px',
position: 'relative',
overflow: 'hidden',
}}>
{/* Logo 内光效 */}
<View style={{
position: 'absolute',
top: '-50%',
left: '-50%',
width: '200%',
height: '200%',
background: 'conic-gradient(from 0deg, transparent, rgba(168, 85, 247, 0.1), transparent, rgba(59, 130, 246, 0.1), transparent)',
animation: 'rotate 4s linear infinite',
}} />
{/* Logo 图标 */}
<Text style={{ fontSize: '48px', position: 'relative', zIndex: 1 }}>🔐</Text>
</View>
{/* 品牌名 - 渐变文字 */}
<Text style={{
fontSize: '28px',
fontWeight: '700',
background: 'linear-gradient(90deg, #a855f7, #6366f1, #3b82f6)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
marginBottom: '8px',
}}>WebSopy</Text>
{/* 标语 */}
<Text style={{
fontSize: '14px',
color: 'rgba(255, 255, 255, 0.6)',
letterSpacing: '2px',
}}> · </Text>
{/* 分割线 */}
<View style={{
width: '60px',
height: '3px',
background: 'linear-gradient(90deg, transparent, #a855f7, transparent)',
marginTop: '20px',
borderRadius: '2px',
}} />
</View>
{/* 品牌名 */}
<Text className="text-2xl font-bold text-gray-900 mb-2">websopy</Text>
{/* 登录卡片 */}
<View style={{
width: '100%',
maxWidth: '320px',
background: 'rgba(255, 255, 255, 0.05)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
borderRadius: '20px',
border: '1px solid rgba(255, 255, 255, 0.1)',
padding: '24px',
marginBottom: '24px',
}}>
{/* 提示图标 */}
<View style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px',
}}>
<View style={{
width: '48px',
height: '48px',
borderRadius: '50%',
background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.3), rgba(59, 130, 246, 0.3))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid rgba(168, 85, 247, 0.4)',
}}>
<Text style={{ fontSize: '24px' }}>📱</Text>
</View>
</View>
{/* 标 */}
<Text className="text-sm text-gray-500 tracking-widest"> AI </Text>
</View>
{/* 标 */}
<Text style={{
textAlign: 'center',
color: '#fff',
fontSize: '18px',
fontWeight: '600',
marginBottom: '8px',
}}></Text>
{/* 底部操作区域 */}
<View className="px-6 pb-12">
{/* 主按钮 - 手机号授权登录 */}
<div className={'flex flex-col w-full text-white rounded-full justify-between items-center my-2'} style={{ background: 'linear-gradient(to right, #7e22ce, #9333ea)'}}>
<Button open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
{/* 描述 */}
<Text style={{
textAlign: 'center',
color: 'rgba(255, 255, 255, 0.6)',
fontSize: '13px',
lineHeight: '1.5',
}}>使</Text>
</View>
{/* 主按钮 - 渐变发光按钮 */}
<View
style={{
width: '100%',
maxWidth: '320px',
background: 'linear-gradient(135deg, #7c3aed 0%, #a855f7 50%, #6366f1 100%)',
borderRadius: '30px',
padding: '2px',
boxShadow: '0 0 30px rgba(168, 85, 247, 0.4), 0 0 60px rgba(168, 85, 247, 0.2)',
}}
>
<Button
className="authorize-btn"
open-type="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}
disabled={authLoading}
style={{
width: '100%',
height: '52px',
fontSize: '17px',
fontWeight: '600',
color: '#fff',
background: authLoading ? 'rgba(255, 255, 255, 0.1)' : 'transparent',
borderRadius: '28px',
border: 'none',
padding: '0',
boxSizing: 'border-box',
lineHeight: '52px',
textAlign: 'center',
}}
>
{authLoading ? '授权中...' : '微信手机号登录'}
</Button>
</div>
{/*<button*/}
{/* className="w-full h-12 bg-orange-500 text-white text-base font-medium rounded-lg flex items-center justify-center mb-4 border-0"*/}
{/* open-type="getPhoneNumber"*/}
{/* onGetPhoneNumber={handleGetPhoneNumber}*/}
{/* disabled={authLoading}*/}
{/*>*/}
{/* {authLoading ? '授权中...' : '手机号授权登录'}*/}
{/*</button>*/}
</View>
{/* 取消按钮 */}
<View
className="w-full h-12 flex items-center justify-center text-gray-500 text-base cursor-pointer"
className="w-full max-w-80 flex items-center justify-center mt-4 cursor-pointer"
style={{ height: '44px' }}
onClick={handleCancel}
>
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '14px' }}></Text>
</View>
{/* 协议勾选 */}
<View className="flex items-center justify-center mt-6">
<View
className={`w-4 h-4 rounded-full border-2 flex items-center justify-center mr-2 ${
agreementChecked ? 'bg-orange-500 border-orange-500' : 'border-gray-300'
}`}
style={{ padding: '8px', marginRight: '4px' }}
onClick={() => setAgreementChecked(!agreementChecked)}
>
{agreementChecked && (
<Checklist size="12" color="#fff" />
)}
<View
style={{
width: '18px',
height: '18px',
borderRadius: '4px',
border: agreementChecked ? 'none' : '1px solid rgba(255, 255, 255, 0.3)',
background: agreementChecked ? 'linear-gradient(135deg, #7c3aed, #a855f7)' : 'transparent',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{agreementChecked && (
<Text style={{ color: '#fff', fontSize: '12px' }}></Text>
)}
</View>
</View>
<Text className="text-xs text-gray-500">
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '12px' }}>
</Text>
<Text
className="text-xs text-orange-500 ml-1"
style={{ color: '#a855f7', fontSize: '12px', padding: '4px 2px' }}
onClick={(e) => {
e.stopPropagation();
openAgreement('service');
@@ -547,9 +694,9 @@ const QRConfirmPage: React.FC = () => {
>
</Text>
<Text className="text-xs text-gray-500"></Text>
<Text style={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: '12px' }}></Text>
<Text
className="text-xs text-orange-500 ml-1"
style={{ color: '#a855f7', fontSize: '12px', padding: '4px 2px' }}
onClick={(e) => {
e.stopPropagation();
openAgreement('privacy');
@@ -558,16 +705,34 @@ const QRConfirmPage: React.FC = () => {
</Text>
</View>
</View>
{/* 协议弹窗 */}
{showAgreementPopup && (
<View className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 px-6">
<View className="bg-white rounded-xl w-full max-w-sm">
{renderAgreementContent()}
</View>
{/* 底部装饰 */}
<View style={{
position: 'absolute',
bottom: '40px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
<View style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: '#a855f7',
boxShadow: '0 0 10px rgba(168, 85, 247, 0.8)',
}} />
<Text style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: '11px' }}>
</Text>
<View style={{
width: '6px',
height: '6px',
borderRadius: '50%',
background: '#3b82f6',
boxShadow: '0 0 10px rgba(59, 130, 246, 0.8)',
}} />
</View>
)}
</View>
</View>
);
};
@@ -781,7 +946,7 @@ const QRConfirmPage: React.FC = () => {
{/* Token 信息 */}
{token && !loading && !confirmed && !needAuth && (
<View className="bg-blue-50 rounded-lg p-3 mb-4">
<View className="bg-blue-500 rounded-lg p-3 mb-4">
<Text className="text-xs text-blue-600">
{token.substring(0, 20)}...{token.substring(token.length - 10)}
</Text>

View File

@@ -292,7 +292,7 @@ const UnifiedQRPage: React.FC = () => {
)}
{/* 功能说明 */}
<Card className="m-4 bg-blue-50 border border-blue-200">
<Card className="m-4 bg-blue-500 border border-blue-200">
<View className="p-4">
<View className="flex items-start">
<Tips className="text-blue-600 mr-2 mt-1" size="16" />

View File

@@ -0,0 +1,5 @@
export default {
navigationBarTitleText: '详情',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
};

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { WebView } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
/**
* 通用 WebView 页面
* 通过 url 参数指定要加载的网页地址
* 用于显示注册协议、隐私政策等网页内容
*/
const WebViewPage: React.FC = () => {
const router = useRouter();
const url = decodeURIComponent(router.params.url || '');
if (!url) {
Taro.navigateBack();
return null;
}
return <WebView src={url} />;
};
export default WebViewPage;

View File

@@ -1,14 +1,58 @@
import React from 'react'
import React, { useCallback, useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {Avatar, Button} from '@nutui/nutui-react-taro'
import {User} from '@nutui/icons-react-taro'
import {useThemeStyles} from '@/hooks/useTheme'
import {useUser} from '@/hooks/useUser'
import { View, Text } from '@tarojs/components'
import { Avatar, Button, ConfigProvider, Grid, Empty } from '@nutui/nutui-react-taro'
import { User, Location, Order, Refresh } from '@nutui/icons-react-taro'
import { useThemeStyles } from '@/hooks/useTheme'
import { useUser } from '@/hooks/useUser'
import { listShopOrder } from '@/api/shop/shopOrder'
import type { ShopOrder } from '@/api/shop/shopOrder/model'
const RiderIndex: React.FC = () => {
const themeStyles = useThemeStyles()
const {isLoggedIn, loading: userLoading, getAvatarUrl, getDisplayName} = useUser()
const { isLoggedIn, loading: userLoading, getAvatarUrl, getDisplayName } = useUser()
const [stats, setStats] = useState({ pending: 0, delivering: 0, completed: 0, today: 0 })
const [loading, setLoading] = useState(false)
const loadStats = useCallback(async () => {
const userId = Taro.getStorageSync('UserId')
if (!userId) return
setLoading(true)
try {
// 查询待配送和配送中的订单
const [pendingRes, deliveringRes, completedRes] = await Promise.allSettled([
listShopOrder({ riderId: Number(userId), orderStatus: 30, page: 1, limit: 1 }),
listShopOrder({ riderId: Number(userId), orderStatus: 40, page: 1, limit: 1 }),
listShopOrder({ riderId: Number(userId), orderStatus: 50, page: 1, limit: 1 }),
])
const pending = pendingRes.status === 'fulfilled' ? (pendingRes.value?.count || 0) : 0
const delivering = deliveringRes.status === 'fulfilled' ? (deliveringRes.value?.count || 0) : 0
const completed = completedRes.status === 'fulfilled' ? (completedRes.value?.count || 0) : 0
setStats({ pending, delivering, completed, today: pending + delivering })
} catch (e) {
console.error('加载骑手统计失败', e)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
if (isLoggedIn) {
loadStats()
}
}, [isLoggedIn, loadStats])
const navigateToPage = (url: string) => {
if (!isLoggedIn) {
Taro.showToast({ title: '请先登录', icon: 'none', duration: 1500 })
return
}
Taro.navigateTo({ url })
}
if (!isLoggedIn && !userLoading) {
return (
@@ -16,7 +60,7 @@ const RiderIndex: React.FC = () => {
<View className="bg-white rounded-xl p-4">
<Text className="text-gray-700"></Text>
<View className="mt-3">
<Button type="primary" onClick={() => Taro.navigateTo({url: '/passport/login'})}>
<Button type="primary" onClick={() => Taro.navigateTo({ url: '/passport/login' })}>
</Button>
</View>
@@ -25,39 +69,129 @@ const RiderIndex: React.FC = () => {
)
}
const statCards = [
{ label: '待取货', value: stats.pending, color: 'bg-orange-50', textColor: 'text-orange-600' },
{ label: '配送中', value: stats.delivering, color: 'bg-blue-50', textColor: 'text-blue-600' },
{ label: '已完成', value: stats.completed, color: 'bg-green-50', textColor: 'text-green-600' },
{ label: '今日任务', value: stats.today, color: 'bg-purple-50', textColor: 'text-purple-600' },
]
return (
<View className="bg-gray-100 min-h-screen">
{/* 头部信息 */}
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
<View
className="absolute w-32 h-32 rounded-full"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.1)', top: '-16px', right: '-16px' }}
></View>
<View
className="absolute w-24 h-24 rounded-full"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.08)', bottom: '-12px', left: '-12px' }}
></View>
<View className="flex items-center justify-between relative z-10">
<Avatar
size="50"
src={getAvatarUrl()}
icon={<User />}
className="mr-4"
style={{border: '2px solid rgba(255, 255, 255, 0.3)'}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1">{getDisplayName()}</View>
<View className="text-sm" style={{color: 'rgba(255, 255, 255, 0.8)'}}>
<View className="flex items-center">
<Avatar
size="50"
src={getAvatarUrl()}
icon={<User />}
className="mr-4"
style={{ border: '2px solid rgba(255, 255, 255, 0.3)' }}
/>
<View>
<View className="text-white text-lg font-bold mb-1">{getDisplayName()}</View>
<View className="text-sm" style={{ color: 'rgba(255, 255, 255, 0.8)' }}>
</View>
</View>
</View>
<Button
size="small"
style={{
background: 'rgba(255, 255, 255, 0.18)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.25)'
}}
loading={loading}
onClick={loadStats}
>
<Refresh size="14" />
</Button>
</View>
</View>
<View className="p-4">
<View className="bg-white rounded-xl p-4">
<Text className="text-gray-800 font-semibold"></Text>
<View className="mt-3">
<Button type="primary" block onClick={() => Taro.navigateTo({url: '/rider/orders/index'})}>
</Button>
</View>
{/* 数据统计卡片 */}
<View className="mx-4 -mt-6 rounded-xl bg-white p-4 relative z-10">
<View className="font-semibold text-gray-400 text-sm mb-3"></View>
<View className="grid grid-cols-4 gap-2">
{statCards.map((card) => (
<View key={card.label} className={`${card.color} rounded-lg p-3 text-center`}>
<View className={`text-2xl font-bold ${card.textColor}`}>{card.value}</View>
<View className="text-xs text-gray-500 mt-1">{card.label}</View>
</View>
))}
</View>
</View>
{/* 快捷功能入口 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={3}
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="待取货" onClick={() => navigateToPage('/rider/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Order color="#f97316" size="20" />
</View>
{stats.pending > 0 && (
<View className="absolute top-0 right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{stats.pending > 9 ? '9+' : stats.pending}
</View>
)}
</View>
</Grid.Item>
<Grid.Item text="配送中" onClick={() => navigateToPage('/rider/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Location color="#3b82f6" size="20" />
</View>
{stats.delivering > 0 && (
<View className="absolute top-0 right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{stats.delivering > 9 ? '9+' : stats.delivering}
</View>
)}
</View>
</Grid.Item>
<Grid.Item text="全部订单" onClick={() => navigateToPage('/rider/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Order color="#6b7280" size="20" />
</View>
</View>
</Grid.Item>
</Grid>
</ConfigProvider>
</View>
{/* 提示信息 */}
{stats.today === 0 && !loading && (
<View className="mx-4 mt-4">
<Empty description="暂无配送任务" imageSize={60} />
</View>
)}
<View className="h-20"></View>
</View>
)
}
export default RiderIndex

View File

@@ -240,7 +240,7 @@ const StoreIndex: React.FC = () => {
>
<Grid.Item text="门店订单" onClick={() => navigateToPage('/store/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<View className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20" />
</View>
</View>

View File

@@ -0,0 +1,3 @@
export default defineAppConfig({
navigationBarTitleText: '我的应用',
})

192
src/user/apps/index.tsx Normal file
View File

@@ -0,0 +1,192 @@
import { useCallback, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Empty, InfiniteLoading, PullToRefresh, Tag } from '@nutui/nutui-react-taro'
import { useThemeStyles } from '@/hooks/useTheme'
import { listJoinedApps } from '@/api/app/appProduct'
import type { AppProduct } from '@/api/app/appProduct/model'
import { APP_TYPE_NAME, STATUS_NAME, STATUS_COLOR } from '@/api/app/appProduct/model'
import dayjs from 'dayjs'
const MyAppsPage = () => {
const themeStyles = useThemeStyles()
const [list, setList] = useState<AppProduct[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [current, setCurrent] = useState(1)
const loadApps = useCallback(async (isRefresh = false) => {
if (loading) return
setLoading(true)
try {
const page = isRefresh ? 1 : current
const res = await listJoinedApps({ page, limit: 20 })
const newList = res?.list || []
if (isRefresh) {
setList(newList)
setCurrent(2)
} else {
setList(prev => [...prev, ...newList])
setCurrent(prev => prev + 1)
}
setHasMore(newList.length >= 20)
} catch (e) {
console.error('加载应用失败', e)
} finally {
setLoading(false)
}
}, [loading, current])
useDidShow(() => {
loadApps(true)
})
const handleEnterAdmin = (app: AppProduct) => {
if (!app.adminUrl) {
Taro.showToast({ title: '暂无管理入口', icon: 'none' })
return
}
const url = app.adminUrl.startsWith('http') ? app.adminUrl : `https://${app.adminUrl}`
Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(url)}` })
}
const handleEnterHome = (app: AppProduct) => {
if (!app.homeUrl && !app.domain) {
Taro.showToast({ title: '暂无访问地址', icon: 'none' })
return
}
const url = (app.homeUrl || app.domain || '').startsWith('http')
? (app.homeUrl || app.domain)
: `https://${app.homeUrl || app.domain}`
Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(url)}` })
}
return (
<View className="bg-gray-100 min-h-screen">
{/* 头部 */}
<View className="px-4 py-6" style={themeStyles.primaryBackground}>
<Text className="text-white text-lg font-bold"></Text>
<View className="mt-1">
<Text className="text-white text-sm opacity-80"></Text>
</View>
</View>
<PullToRefresh onRefresh={() => loadApps(true)} className="mt-4">
<View className="mx-4 rounded-xl overflow-hidden">
{list.length === 0 && !loading ? (
<View className="bg-white rounded-xl">
<Empty description="暂无应用请前往PC端开通" imageSize={80} className="py-16" />
<View className="px-4 pb-6">
<View
className="text-center text-sm text-blue-500"
onClick={() => Taro.navigateTo({
url: '/passport/webview/index?url=' + encodeURIComponent('https://websopy.websoft.top/products')
})}
>
>
</View>
</View>
</View>
) : (
list.map((app) => {
const statusName = STATUS_NAME[app.status ?? 0] || '未知'
const statusColor = STATUS_COLOR[app.status ?? 0] || '#6b7280'
const typeName = APP_TYPE_NAME[app.appType ?? 10] || '应用'
const isExpired = app.expirationTime && dayjs(app.expirationTime).isBefore(dayjs())
return (
<View
key={app.productId}
className="bg-white mx-0 mb-3 rounded-xl p-4"
>
<View className="flex items-start gap-3">
{/* 图标 */}
<View className="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center flex-shrink-0 overflow-hidden">
{app.icon ? (
<image src={app.icon} className="w-full h-full" mode="aspectFit" />
) : (
<Text className="text-lg text-gray-400">{typeName[0]}</Text>
)}
</View>
{/* 信息 */}
<View className="flex-1 min-w-0">
<View className="flex items-center gap-2 mb-1">
<Text className="font-semibold text-gray-900 text-sm truncate flex-1">
{app.productName || '未命名应用'}
</Text>
<Tag
plain
style={{
fontSize: '10px',
padding: '0 4px',
borderColor: statusColor,
color: statusColor,
}}
>
{isExpired ? '已过期' : statusName}
</Tag>
</View>
<View className="flex items-center gap-2 mb-1">
<Text className="text-xs text-gray-400">{typeName}</Text>
{app.version && (
<Text className="text-xs text-gray-300">v{app.version}</Text>
)}
</View>
{app.description && (
<Text className="text-xs text-gray-500 line-clamp-2 mb-2">{app.description}</Text>
)}
{/* 操作按钮 */}
<View className="flex gap-2 mt-2">
{app.adminUrl && (
<View
className="px-3 py-2 bg-blue-500 rounded-lg text-center"
onClick={() => handleEnterAdmin(app)}
>
<Text className="text-xs text-blue-600"></Text>
</View>
)}
{(app.homeUrl || app.domain) && (
<View
className="px-3 py-2 bg-green-50 rounded-lg text-center"
onClick={() => handleEnterHome(app)}
>
<Text className="text-xs text-green-600">访</Text>
</View>
)}
{app.expirationTime && (
<View className="flex items-center ml-auto">
<Text className={`text-xs ${isExpired ? 'text-red-400' : 'text-gray-300'}`}>
{dayjs(app.expirationTime).format('YYYY-MM-DD')}
</Text>
</View>
)}
</View>
</View>
</View>
</View>
)
})
)}
</View>
<InfiniteLoading
hasMore={hasMore}
onLoadMore={() => loadApps(false)}
loading={loading}
>
<View className="py-4 text-center">
<Text className="text-xs text-gray-400">
{loading ? '加载中...' : hasMore ? '下拉加载更多' : '没有更多了'}
</Text>
</View>
</InfiniteLoading>
</PullToRefresh>
</View>
)
}
export default MyAppsPage

View File

@@ -160,7 +160,7 @@ const GiftCardRedeem = () => {
{!redeemSuccess ? (
<>
{/* 兑换说明 */}
<View className="bg-blue-50 mx-4 mt-4 p-4 rounded-xl border border-blue-200">
<View className="bg-blue-500 mx-4 mt-4 p-4 rounded-xl border border-blue-200">
<View className="flex items-center mb-2">
<Gift size="20" className="text-blue-600 mr-2" />
<Text className="font-semibold text-blue-800"></Text>

View File

@@ -264,7 +264,7 @@ const GiftCardUse = () => {
</Text>
{gift.contactInfo && (
<View className="bg-blue-50 p-4 rounded-lg mb-6 border border-blue-200">
<View className="bg-blue-500 p-4 rounded-lg mb-6 border border-blue-200">
<Text className="text-blue-800 font-semibold mb-1"></Text>
<Text className="text-blue-700">{gift.contactInfo}</Text>
</View>

View File

@@ -0,0 +1,4 @@
export default defineAppConfig({
navigationBarTitleText: '消息通知',
navigationBarBackgroundColor: '#ffffff',
})

View File

@@ -0,0 +1,206 @@
import { useCallback, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Cell, CellGroup, Empty, InfiniteLoading, PullToRefresh, Tag, Button } from '@nutui/nutui-react-taro'
import { useThemeStyles } from '@/hooks/useTheme'
import { pageNotification, getUnreadCount, markNotificationRead, markAllNotificationRead } from '@/api/app/notification'
import type { Notification } from '@/api/app/notification/model'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
/** 通知类型配置 */
const TYPE_CONFIG: Record<string, { label: string; color: string }> = {
system: { label: '系统', color: '#3b82f6' },
ticket: { label: '工单', color: '#f59e0b' },
review: { label: '审核', color: '#8b5cf6' },
resource: { label: '资源', color: '#10b981' },
permission: { label: '权限', color: '#ef4444' },
member: { label: '成员', color: '#06b6d4' },
payment: { label: '支付', color: '#ec4899' },
}
const NotificationPage = () => {
const themeStyles = useThemeStyles()
const [list, setList] = useState<Notification[]>([])
const [loading, setLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [page, setPage] = useState(1)
const [unreadTotal, setUnreadTotal] = useState(0)
const loadNotifications = useCallback(async (isRefresh = false) => {
if (loading) return
setLoading(true)
try {
const currentPage = isRefresh ? 1 : page
const res = await pageNotification({ page: currentPage, limit: 20 })
const newList = res?.list || []
if (isRefresh) {
setList(newList)
setPage(2)
} else {
setList(prev => [...prev, ...newList])
setPage(prev => prev + 1)
}
setHasMore(newList.length >= 20)
} catch (e) {
console.error('加载通知失败', e)
} finally {
setLoading(false)
}
}, [loading, page])
const loadUnreadCount = useCallback(async () => {
try {
const res = await getUnreadCount()
setUnreadTotal(res?.total || 0)
} catch {
// ignore
}
}, [])
useDidShow(() => {
loadNotifications(true)
loadUnreadCount()
})
const handleMarkRead = async (item: Notification) => {
if (item.isRead === 1 || !item.id) return
try {
await markNotificationRead(item.id)
setList(prev => prev.map(n => n.id === item.id ? { ...n, isRead: 1 } : n))
loadUnreadCount()
} catch (e) {
console.error('标记已读失败', e)
}
}
const handleMarkAllRead = async () => {
try {
await markAllNotificationRead()
setList(prev => prev.map(n => ({ ...n, isRead: 1 })))
setUnreadTotal(0)
Taro.showToast({ title: '已全部标记为已读', icon: 'success' })
} catch (e) {
console.error('标记全部已读失败', e)
}
}
const handleClick = (item: Notification) => {
handleMarkRead(item)
if (item.linkUrl) {
// 外链通过 WebView 打开
Taro.navigateTo({ url: `/passport/webview/index?url=${encodeURIComponent(item.linkUrl)}` })
}
}
const formatTime = (time?: string) => {
if (!time) return ''
return dayjs(time).fromNow()
}
if (list.length === 0 && !loading) {
return (
<View className="bg-gray-100 min-h-screen">
<View className="px-4 py-6" style={themeStyles.primaryBackground}>
<Text className="text-white text-lg font-bold"></Text>
</View>
<Empty description="暂无通知" imageSize={80} className="mt-20" />
</View>
)
}
return (
<View className="bg-gray-100 min-h-screen">
{/* 头部 */}
<View className="px-4 py-6" style={themeStyles.primaryBackground}>
<View className="flex items-center justify-between">
<View>
<Text className="text-white text-lg font-bold"></Text>
{unreadTotal > 0 && (
<View className="mt-1">
<Text className="text-white text-sm opacity-80">{unreadTotal} </Text>
</View>
)}
</View>
{unreadTotal > 0 && (
<Button
size="small"
style={{
background: 'rgba(255, 255, 255, 0.18)',
color: '#fff',
border: '1px solid rgba(255, 255, 255, 0.25)'
}}
onClick={handleMarkAllRead}
>
</Button>
)}
</View>
</View>
<PullToRefresh onRefresh={() => loadNotifications(true)}>
{/* 通知列表 */}
<View className="mx-4 mt-4 rounded-xl overflow-hidden bg-white">
{list.map((item) => {
const typeConf = TYPE_CONFIG[item.type || ''] || TYPE_CONFIG.system
return (
<View
key={item.id}
className={`px-4 py-3 border-b border-gray-50 ${item.isRead === 0 ? 'bg-blue-500' : ''}`}
onClick={() => handleClick(item)}
>
<View className="flex items-start gap-3">
{/* 未读标记 */}
<View className="mt-2">
{item.isRead === 0 ? (
<View className="w-2 h-2 bg-blue-500 rounded-full" />
) : (
<View className="w-2 h-2 bg-transparent" />
)}
</View>
{/* 内容 */}
<View className="flex-1 min-w-0">
<View className="flex items-center gap-2 mb-1">
<Tag type="primary" plain style={{ fontSize: '10px', padding: '0 4px' }}>
{typeConf.label}
</Tag>
<Text className={`text-sm ${item.isRead === 0 ? 'font-semibold text-gray-900' : 'text-gray-600'} truncate`}>
{item.title || '系统通知'}
</Text>
</View>
{item.content && (
<Text className="text-xs text-gray-400 line-clamp-2">{item.content}</Text>
)}
<View className="mt-1">
<Text className="text-xs text-gray-300">{formatTime(item.createTime)}</Text>
</View>
</View>
</View>
</View>
)
})}
</View>
{/* 加载更多 */}
<InfiniteLoading
hasMore={hasMore}
onLoadMore={() => loadNotifications(false)}
loading={loading}
>
<View className="py-4 text-center">
<Text className="text-xs text-gray-400">
{loading ? '加载中...' : hasMore ? '下拉加载更多' : '没有更多了'}
</Text>
</View>
</InfiniteLoading>
</PullToRefresh>
</View>
)
}
export default NotificationPage