style(rider): 优化骑手中心界面及功能入口样式
- 添加骑手订单统计数据,支持待取货、配送中、已完成与今日任务 - 增加刷新按钮,便捷更新统计数据 - 优化头像及用户信息展示,增加主题样式和背景装饰 - 新增快捷功能入口,展示待取货订单数量红点提醒 - 登录未授权时提示并引导登录 - 调整色彩风格,将多处组件背景色由浅蓝色调整为深蓝色,提高视觉一致性 - 管理页新增内容管理工具的基础UI布局 - 扫码确认页重构授权登录页面,改用科技风格设计,增加动态粒子动画和渐变光晕效果 - 协议打开方式改为跳转网页视图,移除旧版弹窗 - 修正部分组件样式,调整按钮及文本颜色,提升视觉效果和交互体验
This commit is contained in:
@@ -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
|
||||
}
|
||||
61
.workbuddy/memory/2026-04-10.md
Normal file
61
.workbuddy/memory/2026-04-10.md
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
19
src/api/app/appProduct/index.ts
Normal file
19
src/api/app/appProduct/index.ts
Normal 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));
|
||||
}
|
||||
51
src/api/app/appProduct/model/index.ts
Normal file
51
src/api/app/appProduct/model/index.ts
Normal 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',
|
||||
}
|
||||
47
src/api/app/notification/index.ts
Normal file
47
src/api/app/notification/index.ts
Normal 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));
|
||||
}
|
||||
41
src/api/app/notification/model/index.ts
Normal file
41
src/api/app/notification/model/index.ts
Normal 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;
|
||||
}
|
||||
54
src/api/app/ticket/index.ts
Normal file
54
src/api/app/ticket/index.ts
Normal 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));
|
||||
}
|
||||
64
src/api/app/ticket/model/index.ts
Normal file
64
src/api/app/ticket/model/index.ts
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
74
src/app.scss
74
src/app.scss
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>*/}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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%)' }}>
|
||||
|
||||
{/* 背景科技元素 */}
|
||||
|
||||
{/* 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-1 flex flex-col items-center justify-center px-8 -mt-20">
|
||||
{/* Logo */}
|
||||
<View className="w-20 h-20 mb-4">
|
||||
|
||||
<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 className="text-2xl font-bold text-gray-900 mb-2">websopy</Text>
|
||||
{/* 品牌名 - 渐变文字 */}
|
||||
<Text style={{
|
||||
fontSize: '28px',
|
||||
fontWeight: '700',
|
||||
background: 'linear-gradient(90deg, #a855f7, #6366f1, #3b82f6)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
marginBottom: '8px',
|
||||
}}>WebSopy</Text>
|
||||
|
||||
{/* 标语 */}
|
||||
<Text className="text-sm text-gray-500 tracking-widest">构建下一代 AI 应用</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>
|
||||
|
||||
{/* 底部操作区域 */}
|
||||
<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}>
|
||||
授权手机号登录
|
||||
{/* 登录卡片 */}
|
||||
<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 style={{
|
||||
textAlign: 'center',
|
||||
color: '#fff',
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
marginBottom: '8px',
|
||||
}}>授权登录</Text>
|
||||
|
||||
{/* 描述 */}
|
||||
<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)}
|
||||
>
|
||||
<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 && (
|
||||
<Checklist size="12" color="#fff" />
|
||||
<Text style={{ color: '#fff', fontSize: '12px' }}>✓</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className="text-xs text-gray-500">
|
||||
我已阅读并同意权大师的
|
||||
</View>
|
||||
<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 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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
5
src/passport/webview/index.config.ts
Normal file
5
src/passport/webview/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
navigationBarTitleText: '详情',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
navigationBarTextStyle: 'black',
|
||||
};
|
||||
22
src/passport/webview/index.tsx
Normal file
22
src/passport/webview/index.tsx
Normal 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;
|
||||
@@ -1,15 +1,59 @@
|
||||
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 { 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 [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 (
|
||||
<View className="bg-gray-100 min-h-screen p-4">
|
||||
@@ -25,10 +69,28 @@ 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">
|
||||
<View className="flex items-center">
|
||||
<Avatar
|
||||
size="50"
|
||||
src={getAvatarUrl()}
|
||||
@@ -36,28 +98,100 @@ const RiderIndex: React.FC = () => {
|
||||
className="mr-4"
|
||||
style={{ border: '2px solid rgba(255, 255, 255, 0.3)' }}
|
||||
/>
|
||||
<View className="flex-1 flex-col">
|
||||
<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>
|
||||
</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
|
||||
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="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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
src/user/apps/index.config.ts
Normal file
3
src/user/apps/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default defineAppConfig({
|
||||
navigationBarTitleText: '我的应用',
|
||||
})
|
||||
192
src/user/apps/index.tsx
Normal file
192
src/user/apps/index.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
src/user/notification/index.config.ts
Normal file
4
src/user/notification/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default defineAppConfig({
|
||||
navigationBarTitleText: '消息通知',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
})
|
||||
206
src/user/notification/index.tsx
Normal file
206
src/user/notification/index.tsx
Normal 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
|
||||
Reference in New Issue
Block a user