Compare commits

...

195 Commits

Author SHA1 Message Date
xingyy
e72ce59fca Merge branch 'xingyy' into dev
# Conflicts:
#	app/components/AppFooter.vue
2025-02-11 15:40:38 +08:00
xingyy
50fe1248e3 refactor(liveRoom): 优化直播室组件和支付功能
- 移除 paymentInput 组件中的冗余属性
- 在 liveMinWindow 组件中添加路由监听,实现自动关闭功能
- 删除 PaymentInput 组件中的无用日志输出
2025-02-11 15:39:22 +08:00
xingyy
def6a6b71e refactor(live): 重构直播相关功能和状态管理
-将 fullLive 状态从 goodStore 移动到 liveStore
- 优化 liveRoom 页面逻辑,移除不必要的 props
- 更新 AppHeader 组件,使用 liveStore 中的 fullLive 状态
- 删除 floatingVideo 组件
- 调整 liveMinWindow 组件,增加 onClick 事件处理
- 更新 home 页面,使用 liveStore 中的 fullLive 状态
- 优化 liveRoom 中的 PaymentInput 和 SideButton 组件
2025-02-11 15:27:31 +08:00
xingyy
48108c5e84 12 2025-02-11 14:56:19 +08:00
xingyy
3c69236caa feat(live): 实现直播间最小化窗口功能
- 新增最小化窗口组件 liveMinWindow
- 在 liveRoom 页面中集成最小化窗口功能
- 优化 liveStore,增加相关状态和方法支持最小化窗口
- 调整
2025-02-11 14:55:25 +08:00
xingyy
9334819414 refactor(liveRoom): 删除未使用的加密相关代码
- 移除了 cryptConfig、generateKey、decrypt 和 decryptData 等未使用的加密相关函数
- 优化了代码结构,提高了代码的可读性和维护性
2025-02-11 11:36:36 +08:00
xingyy
86198811aa feat(live): 优化直播间功能并添加画中画支持
- 更新直播源地址
- 添加画中画功能,支持视频拖动和缩放
- 实现直播加密密钥生成和数据解密
- 优化直播房间组件,支持全屏和缩略图模式
- 新增日志发送接口
2025-02-11 11:34:24 +08:00
xingyy
10cba595b0 refactor(live): 优化直播室功能和交互
- 修改艺术品状态更新逻辑,仅在未售出时允许改变状态
- 添加支付按钮点击后跳转到签名协议页面
- 优化侧边按钮组件,增加支付按钮的显示逻辑
2025-02-10 17:02:58 +08:00
xingyy
eb02645658 feat(api-collect-code): 新增离线二维码删除功能并优化相关页面
- 新增 offlineQrcodeDelete 函数用于删除离线二维码
- 在我的收藏码页面添加删除功能,支持单个二维码删除
2025-02-10 16:26:54 +08:00
xingyy
69ad600f0d feat(collectCode): 优化收款二维码功能
- 新增二维码生成和预览功能
- 添加拍品号验证逻辑
- 优化对话框样式和交互
-调整 API 调用方式
2025-02-10 15:47:26 +08:00
xingyy
34add8d226 feat(signature): 添加签名功能相关页面
- 新增签名面板页面,用于用户签名
- 新增个人信息页面,用于填写个人相关资料- 新增协议页面,用于展示拍卖规则等协议内容
-优化直播页面竞拍结束提示逻辑
- 修复国家区域选择页面路由问题
- 优化个人主页下拉刷新功能
2025-02-10 10:56:38 +08:00
xingyy
e8a89b184e refactor(live): 优化直播页面消息提示和界面显示
- 移除不必要的空行和逗号
- 统一消息提示的展示时间
- 为艺术品结束提示添加背景色和边框色
- 在竞价信息中添加货币符号
- 调整侧边按钮弹窗的动画效果
2025-02-08 17:40:54 +08:00
xingyy
f1bd2b183f perf(tangPopup): 优化滚动定位逻辑
- 移除了 setTimeout 包裹,直接执行滚动定位逻辑
- 保留了原有的滚动定位功能,没有引入新功能
2025-02-08 17:10:23 +08:00
xingyy
ad19345db6 feat(x-popup): 增加弹窗内列表自动定位功能- 在 x-popup 组件中添加 list-container 类名,用于后续定位操作
- 在 tangPopup.vue 中实现 scrollToCurrentItem 函数,用于滚动到当前选中的物品
- 监听弹窗显示状态,当弹窗显示时调用 scrollToCurrentItem 函数
2025-02-08 17:00:09 +08:00
xingyy
7e4fbc84ad refactor(components): 优化消息组件样式和动画
- 修正了 live/index.js 中的样式对象- 优化了 x-message 组件的样式计算逻辑
- 改进了 tangPopup 组件的动画效果,使过渡更快且不完全消失
2025-02-08 16:34:03 +08:00
xingyy
a791248752 refactor(app): 优化代码结构和功能
- 移除多处 console.log 语句,清理无用代码
- 在消息组件中添加 info 类型配置并实现 info 消息显示功能
- 优化 WebSocket 消息处理逻辑,提高代码可读性
2025-02-08 16:24:26 +08:00
xingyy
c86e449d52 style(tangPopup): 为正在竞拍的标识添加闪烁动画效果
- 在 tangPopup.vue 文件中,为正在竞拍的标识添加了 blink 类名
- 在样式部分新增了 .blink 类的样式,使用 keyframes 创建 fade 动
2025-02-08 15:31:02 +08:00
xingyy
36793c5c5a feat(live): 实现直播间竞拍功能并优化相关页面
- 新增 artworkBuy API 实现艺术品购买功能
- 重构 WebSocket连接逻辑,优化消息处理- 更新直播间页面,支持实时竞拍和消息提示
-调整艺术详情和用户中心页面样式
- 优化消息组件样式和展示逻辑
2025-02-08 15:21:00 +08:00
xingyy
47aa573641 refactor/artDetail: 重构艺术品详情页面
- 移除 goodStore 的使用,改为直接调用 userArtwork API
- 通过路由参数获取艺术品 UUID,动态加载详情数据
- 更新模板,使用新获取的数据渲染页面
- 修改 profile 页面跳转到艺术品详情的方式,传递 UUID 参数
2025-02-08 10:16:54 +08:00
xingyy
aec3825a3b feat(component): 优化消息组件并添加新功能
- 重构 x-message 组件,支持更多自定义选项
- 添加 artDetail 页面用于展示艺术品详情
- 修改 liveRoom 页面,接入新的消息提示功能- 优化 profile 页面布局,增加去支付按钮
- 调整 home 页面,集成新的消息系统
- 修改 websocket 插件,支持携带 token 认证
2025-02-08 10:06:21 +08:00
xingyy
5d645a8106 refactor(home): 优化首页艺术品列表功能
- 移除了 x-image 组件中的多余属性
- 更新了 ItemList 组件中的 LOT 编号显示逻辑
-将 home 页面中的 v-show 改为 v-if
- 重构了 tangPopup 组件,添加了下拉刷新和上拉加载更多功能
2025-02-06 16:29:17 +08:00
xingyy
510b839a1b feat(liveRoom): 实现直播间拍卖数据实时更新和显示
- 从 liveStore 中获取 auctionData,用于展示当前拍卖信息
- 在模板中添加拍卖数据的动态显示,包括当前价、下口价等信息
- 实现拍卖状态的实时更新和对应 UI 的变化
- 优化竞拍列表的展示逻辑,根据不同的拍卖状态显示相应内容
- 在弹窗中添加当前拍卖作品的标识和状态
2025-02-06 15:43:23 +08:00
xingyy
bd56b05e60 refactor(collect-code): 重构收藏码功能
- 修改了 API 接口命名和路径,以适应新的业务逻辑
- 优化了登录流程,增加了验证码登录方式
- 重构了个人中心页面,增加了新的功能组件
- 新增了支付相关页面和逻辑- 优化了代码结构和命名,提高了可维护性
2025-02-06 14:03:09 +08:00
xingyy
8d01653dac feat(collectCode): 新增签名功能相关页面
- 添加签名面板页面,实现签名提交和清除功能
- 更新个人信息页面布局,增加下一步按钮
- 新增协议页面,实现支付前协议展示和同意签字功能
- 在 uno.config.js 中添加自定义样式规则,用于覆盖默认按钮样式
2025-02-06 10:20:11 +08:00
xingyy
2e08e6efcb feat(collect-code): 新增收款二维码功能
- 添加新的 API接口和相关组件
- 实现用户认证和艺术品列表展示- 新增个人资料填写页面- 优化首页和登录页面样式
2025-02-05 17:00:22 +08:00
xingyy
41ad9aeed8 style:优化支付结果对话框样式
- 移除全局样式覆盖,提高代码可维护性- 调整对话框样式为局部应用,避免不必要的样式冲突
- 优化 HTML 结构,提升代码可读性
2025-01-23 20:13:06 +08:00
xingyy
ff053a5a8c feat(image): 添加 webp 图片格式支持
- 在 x-image组件中添加 webp 格式支持
- 在 nuxt.config.js 中配置 ipx 图片处理- 添加 sharp 库以支持图片格式转换
- 修改 live store 中的 show1 变量初始值
- 调整 PaymentResults 组件中 price 的默认值
2025-01-23 20:02:20 +08:00
xingyy
e30b993601 refactor: 将导入路径从波浪号(~)改为 @- 修改了多个文件中的导入路径,将 ~/ 替换为 @/
- 这个改动统一了项目中的导入路径格式,提高了代码的一致性和可维护性
2025-01-23 19:43:45 +08:00
xingyy
b876aac28a fix(components): 修复手机端点击事件导致按钮状态异常
- 在 x-button 组件中添加 event.stopPropagation() 以防止事件冒泡- 更新 SideButton 组件中的事件处理方式,确保点击事件正确触发
2025-01-23 19:41:09 +08:00
xingyy
7916b009e6 refactor(liveRoom): 重构直播室功能
- 移除不必要的导入和未使用的变量
- 优化拍卖数据获取逻辑
- 添加 WebSocket 连接和消息处理功能
- 更新侧边按钮组件,显示实时拍卖数据
-增加拍品详情弹窗功能
2025-01-23 19:29:29 +08:00
xingyy
65f8d2d1e9 feat(websocket): 实现 WebSocket通信功能
- 新增 useWebSocket 组合式函数,用于在组件中使用 WebSocket
- 添加 WebSocket 插件,提供全局 WebSocket 实例和相关方法
- 实现了连接、重连、发送消息、关闭连接等功能
- 通过自定义事件实现消息广播,供组件监听
2025-01-23 16:37:11 +08:00
xingyy
e2b5e6bcbf feat(env): 添加 websocket URL 并更新直播室相关功能
- 在 .env.test 中添加 NUXT_PUBLIC_SOCKET_URL 配置项
- 在直播室页面中实现 websocket连接和消息监听
-优化拍卖详情获取逻辑
- 更新首页和详情页相关组件
2025-01-23 16:34:34 +08:00
xingyy
9f9e96dc0c feat(home): 获取拍卖详情并优化首页组件
- 在 home 页面中调用 getAuctionDetail 方法获取拍卖详情
- 更新 Cescribe组件以显示拍卖详情信息和图片
- 在 ItemList 组件中添加 v-memo 指令以优化性能
- 调整 liveRoom 组件中的视频播放器样式
2025-01-23 15:43:48 +08:00
xingyy
2d909b276b perf(components): 添加 v-memo指令以优化性能
- 在 AppFooter 组件中,为最外层 div 添加 v-memo="[active]" 指令- 在 AppHeader 组件中,为 VanNavBar 组件添加 v-memo="[title, fullLive, showLeftArrow, subTitle]" 指令
- 移除了 AppHeader 组件中的 console.log 语句
2025-01-23 15:28:31 +08:00
xingyy
a6a5c20705 refactor(app): 移除颜色模式相关代码
- 删除了 app.vue 中的颜色模式相关代码
- 移除了 nuxt.config.js 中的 colorMode 配置
- 删除了 package.json 中的 @nuxtjs/color-mode 依赖
- 移除了 unocss 示例页面
- 注释掉了 liveRoom 页面中的视频播放代码
2025-01-23 15:24:49 +08:00
xingyy
107966dabc feat(app): 添加暗黑模式支持并优化主题样式
- 新增暗黑模式支持,使用 colorMode 模块管理主题切换
- 添加全局样式和默认主题样式文件
- 优化 app.vue 中的样式和布局
- 更新 i18n 插件配置,使用 TypeScript 类型
- 调整 nuxt.config.js 中的模块顺序和配置
2025-01-23 14:59:20 +08:00
xingyy
88d1dea0d2 refactor(app): 调整主页相关组件和页面的代码- 在 AppHeader 组件中添加调试日志,用于检查路由元数据
- 优化 home 页面的代码结构,移除未使用的导入和变量
- 在主页面添加 definePageMeta 以设置页面元数据
2025-01-23 14:34:51 +08:00
xingyy
6fc63427de Merge remote-tracking branch 'origin/main' 2025-01-23 14:27:07 +08:00
xingyy
8cb35f4f1b 12 2025-01-23 14:27:02 +08:00
xingyy
579c6df57b refactor(i18n):重命名文件并优化国际化插件代码
- 将 i18n.ts 重命名为 i18n.js,统一文件类型
- 移除了未使用的类型导入 TypeLocale
- 添加了对 vant国际化语言包的导入- 简化了 setLocale 调用,移除了不必要的类型转换
- 优化了代码格式和注释
2025-01-23 14:25:30 +08:00
xingyy
0d1342414b refactor(goods): 优化商品列表获取逻辑
- 移除了不必要的 console.log 语句
- 更新了 artworkList API 的导入路径
- 删除了 Home 组件中的冗余模板代码
2025-01-23 14:22:25 +08:00
xingyy
fbac90a177 refactor(app/config): 修改路由名称
- 将 useAppFooterRouteNames 中的 'home'路由改为 'index'
-保持 useAppHeaderRouteNames 不变
2025-01-23 14:17:49 +08:00
xingyy
44b593966e Merge remote-tracking branch 'origin/main' 2025-01-23 14:16:45 +08:00
xingyy
7ae4899e17 refactor(itemDetail): 移除未使用的代码
- 删除了未使用的 images 数组和 clickSwipe函数
- 注释掉了不再使用的 van-swipe 组件
2025-01-23 14:16:39 +08:00
scout
8b144a270c Merge branch 'qb' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-23 14:15:24 +08:00
scout
d8b880cf47 扫码 2025-01-23 14:11:41 +08:00
scout
88e8170755 feat(route): 添加collectCode相关路由配置 2025-01-23 14:11:21 +08:00
scout
0d087c0bc3 chore(deps): 添加@fingerprintjs/fingerprintjs依赖 2025-01-23 14:11:21 +08:00
scout
f4df2d0a78 feat(auth): 添加fingerprint存储 2025-01-23 14:11:20 +08:00
xingyy
f80f9c1651 feat(styles): 初始化默认主题样式并支持暗黑模式
- 新增 default-theme.css 文件,定义基础样式和暗黑模式样式
-移除 index.css 文件中的重复样式
- 在 nuxt.config.js 中引入 default-theme.css
- 优化了颜色变量和布局样式
2025-01-23 14:10:15 +08:00
xingyy
e541d0b21d refactor(app): 重构应用配置和样式
- 移除 colorMode 相关代码
- 删除全局样式文件
- 更新 nuxt 配置:
  - 添加 runtimeConfig
  - 更新 css 配置
  - 优化 vite构建配置
  - 新增 image 模块配置
- 更新路由配置
- 调整组件实现
- 更新环境变量加载方式
2025-01-23 14:04:34 +08:00
xingyy
3b8bd623c0 refactor(app): 重构应用配置和样式
- 移除 colorMode 相关代码
- 删除全局样式文件
- 更新 nuxt 配置:
  - 添加 runtimeConfig
  - 更新 css 配置
  - 优化 vite构建配置
  - 新增 image 模块配置
- 更新路由配置
- 调整组件实现
- 更新环境变量加载方式
2025-01-23 13:56:18 +08:00
xingyy
fb8a72b47a feat(ItemList): 优化下拉刷新组件
- 添加刷新成功状态显示
- 设置刷新成功状态持续时间为 700ms
- 在刷新成功时显示图标和提示文本
2025-01-23 12:05:13 +08:00
xingyy
a423a7f801 feat(home): 重构首页布局并添加瀑布流布局
- 移除 Column 组件,使用 vue-masonry-wall 实现瀑布流布局- 更新 ItemList 组件,集成瀑布流布局和新的详情弹窗
- 修改 DetailPopup 组件,使用新的详情信息结构
- 更新 itemDetail 组件,适配新的详情信息数据
- 在项目中添加 vue-masonry-wall 依赖
2025-01-23 11:08:54 +08:00
xingyy
e6fdd0354a perf(home): 为 Column 组件中的图片添加懒加载
- 在 Column 组件的图片标签中添加 loading="lazy" 属性
- 这个修改可以提高页面加载性能,实现图片的懒加载
2025-01-22 16:59:00 +08:00
xingyy
3225d91ecb refactor(components): 重构弹窗组件和详情页展示逻辑
- 修改实名认证详情页布局- 优化 item 详情展示方式
- 调整弹窗组件样式
- 重命名 ItemDetailSheet 为 DetailPopup
- 更新相关组件引用
2025-01-22 16:56:44 +08:00
xingyy
a36a98c576 style(x-image): 添加 object-fit: cover 样式
- 在 x-image 组件的 img 标签中添加 object-fit: cover 样式
- 确保图片在容器中等比例填充,保持图片的宽高比
2025-01-22 16:33:53 +08:00
xingyy
d3cb4d55b4 refactor(components): 优化多个组件的结构和功能
- 为 x-button 和 x-popup 组件添加注释说明
- 在 x-image 组件中添加 lazy 加载属性
- 优化 profile 页面的我的拍品列表展示
- 更新 tang
2025-01-22 16:23:48 +08:00
xingyy
331b4a73b2 refactor(app): 重构 LiveRoom 组件
- 将 LiveRoom 相关组件和文件重命名,统一使用小写开头
- 新增 x-button、x-image 和 x-popup 组件,替代原有 PressableButton 和 ImagePreview
-优化 SideButton 组件,使用新的 x-button 和 tangPopup 组件- 更新 LiveRoom 组件中的引用和使用方式
- 调整 tangPopup 组件,使用 goodStore 替代静态数据
2025-01-22 15:44:50 +08:00
xingyy
c74ba7bcb3 refactor(home): 移除首页中的拍品列表弹窗
- 删除了首页组件中的 x-popup组件引用
- 移除了首页模板中的拍品列表弹窗内容
- 新增 tang.vue 组件,用于在直播间展示拍品列表
2025-01-22 14:04:49 +08:00
xingyy
c445901806 1231 2025-01-22 14:02:22 +08:00
xingyy
8f38870c33 refactor(components): 新增 x-popup 组件并优化首页弹窗- 新增 x-popup 组件,用于替换原有的 van-popup
- 在首页引入并使用新的 x-popup组件
- 优化弹窗内容的展示结构
- 调整弹窗样式,增加标题插槽和关闭按钮
2025-01-22 14:01:13 +08:00
xingyy
05cd427430 refactor(app): 优化多个组件和 API 请求处理
- 移除不必要的 Promise.reject
- 修正请求体参数名
- 移除未使用的 import
- 优化首页布局和弹窗组件
- 添加文本溢出样式
2025-01-22 13:28:21 +08:00
xingyy
63e24791f2 refactor(api): 重构请求函数并优化代码格式
- 重构了 http.js 中的 request 函数,使用解构赋值简化参数
- 更新了 auth 和 goods模块中的请求函数,采用新的 request 函数格式
- 优化了代码格式,包括缩进、换行和空格
2025-01-22 11:34:36 +08:00
xingyy
635bca0fb6 refactor(api): 重构 HTTP 请求模块
- 新增请求拦截和响应拦截功能
- 添加 HTTP状态码映射和错误处理- 优化请求配置,增加超时和重试设置
- 新增 request 工具函数简化请求操作
- 更新 goods API 使用新的 request 函数
2025-01-22 11:24:41 +08:00
xingyy
6daf34856e refactor(goods): 重构艺术品列表和详情功能
- 优化艺术品列表加载和刷新逻辑
- 添加艺术品详情获取功能
-修复滚动文本组件属性
-优化代码结构和命名
2025-01-22 10:44:43 +08:00
xingyy
59269a7547 1231 2025-01-22 09:44:44 +08:00
xingyy
fc3e833605 fix(home): 修复 home 页面无限滚动加载问题
- 调整 pageRef 的初始值,将 page 设置为 0
- 在 ItemList 组件中添加完成加载的判断条件
- 优化 getDetail 函数,使用 currentItem.value.uuid 替代 artworkUuid
-移除首页初始化数据加载逻辑
2025-01-22 09:32:24 +08:00
xingyy
ad9899d716 refactor(app): 优化代码结构和性能
- 移除 default.vue 中的 keep-alive 组件,以提高页面切换性能
- 在 ItemList 组件中增加对返回数据的数组判断,提升代码健壮性
- 删除未使用的 goodStore 导入,清理冗余代码
2025-01-21 19:54:00 +08:00
xingyy
77e08232f7 build:优化环境变量设置和 VConsole 配置
- 使用 cross-env 替代直接设置 NODE_ENV,提高跨平台兼容性- 移除 vconsole.js服务端代码,避免 SSG/SSR 问题
- 优化 vconsole.client.ts,确保仅在客户端启用 VConsole
2025-01-21 16:43:04 +08:00
scout
ddbe15cfb1 Merge branch 'qb' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-21 16:27:42 +08:00
scout
934906bb75 vconsole添加 2025-01-21 16:27:26 +08:00
xingyy
7a59995bba feat(debug): 添加 vconsole 并优化相关功能
- 在 package.json 中添加 vconsole 依赖
- 新增 vconsole.js 插件,仅在客户端启用 vconsole- 优化 homepage 组件中的数据处理逻辑
- 调整 LiveRoom 组件的模板结构- 修复 login 页面中的倒计时逻辑
2025-01-21 15:59:59 +08:00
xingyy
21b2ca5a07 123 2025-01-21 15:11:54 +08:00
xingyy
19e1c3b1f6 refactor(LiveRoom): 优化直播页面全屏逻辑和过渡效果
- 添加 fullLive1 变量用于控制全屏状态
- 使用 watch 监听 fullLive属性变化,延迟更新 fullLive1
- 调整过渡效果持续时间,提升用户体验
2025-01-21 15:11:26 +08:00
xingyy
dee871759e feat(goods): 获取用户艺术品并优化首页布局
- 在 goods API 中添加 userArtworks 函数- 更新 auth store,将 userInfo 默认值改为对象
- 优化 LiveRoom 页面布局,添加安全区域支持
- 修改 AppFooter 组件,修复路由判断逻辑
- 更新首页 Column 组件,调整图片显示样式
- 在 Profile 页面添加用户信息展示
2025-01-21 14:16:54 +08:00
xingyy
e23fed8c74 perf(layout): 使用 keep-alive优化页面性能
- 在 default.vue 文件中添加 keep-alive 组件,以提高页面渲染效率
- 此修改可以保留页面状态,减少重复渲染,提升用户体验
2025-01-21 12:00:40 +08:00
xingyy
f3d5a3440c refactor(login): 调整登录页面验证码功能
- 启用验证码发送功能
- 注释掉模拟数据发送代码
-优化代码结构,提高可读性
2025-01-21 11:46:47 +08:00
xingyy
7a968f1fe6 feat(api): 添加 401 状态码处理逻辑
- 在 http.js 中添加了对 401 状态码的处理,重定向到登录页面
- 更新了 LiveRoom 组件,暂时注释掉了 initializePlayer 方法
- 在 goods store 中添加了 currentItem 和 artWorkDetail 两个状态
- 更新了 message 组件,添加了对错误信息的处理
-调整了首页布局,移除了多余的 transition 标签
- 更新了登录页面的默认验证码
2025-01-21 11:43:27 +08:00
xingyy
1b996eafca feat(api): 新增 artworkDetail接口并更新相关页面
- 在 goods API 中添加 artworkDetail函数,用于获取艺术品详情
- 在首页的 ItemList 组件中调用 artworkDetail 获取详情信息- 移除首页中未使用的 useRect 和 itemDetail 组件
2025-01-20 16:57:22 +08:00
xingyy
bad45f8d82 refactor(goods): 重构商品相关功能
- 修改测试环境配置
- 重命名 homeStore为 goodStore
- 优化直播房间组件导入路径
- 重构商品列表和详情功能
- 新增 ItemList 组件
-调整首页布局和功能
2025-01-20 16:17:49 +08:00
xingyy
efc47a27fd refactor(app): 优化登录功能并添加新组件
- 在 Column 组件中使用可选链操作符,提高代码健壮性- 添加 ItemList 组件,但未提供具体实现
- 在 login 组件中:
  - 引入消息提示功能
  -优化登录流程,添加 loading 状态
  - 修复登录后的页面跳转
  - 为登录按钮添加 loading 状态和文本
2025-01-20 13:59:50 +08:00
xingyy
bda4666782 fix(home): 修复首页拍卖商品成交价显示错误
- 将显示成交价的字段从 transactionPrice 改为 soldPrice
-修复了成交价显示逻辑,确保正确展示拍卖商品的最终成交价格
2025-01-20 11:46:01 +08:00
xingyy
0f92a99b32 feat(home): 更新首页布局和数据获取逻辑
- 移除多余的组件和模板代码
- 新增拍卖详情和艺术品列表获取功能- 优化首页数据展示和布局结构
- 调整登录页面样式和逻辑
2025-01-20 11:42:25 +08:00
xingyy
9e6788dbb2 123 2025-01-20 09:00:20 +08:00
xingyy
6ec8c46c55 Merge branch 'xingyy' 2025-01-17 16:58:12 +08:00
xingyy
6f67273a9a refactor(store): 重构 home store 并改名为 goods store
- 将 home store 重命名为 goods store,以更准确地反映其用途
- 更新了相关文件中的导入路径和引用
-调整了首页布局和组件以适应新的 goods store 结构
- 新增了 goods store 中的 actionDetails 和 itemList 属性
2025-01-17 16:56:13 +08:00
xingyy
fbbb30040e feat(auth): 实现用户登录和实名认证功能
- 新增用户登录接口和相关逻辑
- 实现实名认证页面,包括表单填写和提交功能
- 添加用户信息存储和展示
- 优化页面样式和交互
2025-01-17 14:07:19 +08:00
scout
49f908b174 Merge branch 'main' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-17 11:42:13 +08:00
scout
0beaa774e9 feat(transition): 添加页面切换滑动动画效果 2025-01-17 11:42:12 +08:00
xingyy
d345b66026 123 2025-01-17 11:06:19 +08:00
xingyy
a43bdaf157 refactor(home): 将首页数据移至 store 并优化页面逻辑
- 将 list 数据从组件内部移动到 home store 中
- 优化了 fullLive 的切换逻辑,改为 toggleFullLive 方法
- 更新了模板,使用新的 store 数据
- 添加了 slide-up 过渡动画,替换了原有的 fade动画
2025-01-17 10:19:01 +08:00
xingyy
b7c38704f5 Merge remote-tracking branch 'origin/main' 2025-01-17 09:55:15 +08:00
xingyy
2e73eb7e34 feat(env): 添加生产环境和测试环境配置
- 新增 .env.prod 和 .env.test 文件,分别用于生产环境和测试环境
- 配置文件中包含了 API基础 URL、WebSocket URL 和阿里云播放器配置等信息- 添加了首页组件 Column 和首页主组件,用于展示拍卖品列表
2025-01-17 09:55:09 +08:00
xingyy
bbf3466085 feat(login): 添加短信验证码倒计时功能
- 实现了一个倒计时函数 startCountdown,用于在发送验证码后启动 60 秒倒计时
- 在登录表单中添加了倒计时显示,当倒计时大于 0 时禁用重新发送按钮
- 成功发送验证码后启动倒计时
2025-01-16 16:28:21 +08:00
xingyy
4ab0a5ca81 refactor(api): 重构 HTTP模块并优化错误处理
- 将 http.ts 重命名为 http.js
- 添加全局错误处理逻辑
- 更新 API 调用和错误处理方式
- 优化登录页面的验证码发送逻辑
- 调整消息组件的显示位置
2025-01-16 16:18:38 +08:00
xingyy
0107ff6217 feat(api): 重构 HTTP 请求模块
- 新增 ofetch 作为 HTTP 客户端,替换原有的 axios
- 实现全局的 HTTP 状态码处理器
- 添加请求和响应的拦截器
-重构登录页面,使用 van-swipe 组件实现页面切换
-移除旧的请求模块,简化代码结构
2025-01-16 15:40:06 +08:00
xingyy
89a3652003 refactor(api): 移除 HTTP 模块
移除了 HTTP模块的实现,包括:
- 删除了 http.ts 文件,其中包含 HTTP 客户端的配置和错误处理逻辑
- 删除了 plugins/http.ts 文件,用于在 Nuxt 中初始化 HTTP 客户端- 删除了 prose.ts 文件,其中包含获取散文数据的 API调用

此次更改清除了不再需要的 HTTP 模块相关代码,简化了代码库结构。
2025-01-16 11:51:10 +08:00
xingyy
7dfda1a3d3 feat(service): 添加自定义请求模块并优化认证逻辑
- 新增 request 目录,实现自定义请求类和拦截器
- 添加全局状态管理,用于存储认证信息
- 优化 token 刷新逻辑,提高请求安全性
- 移除不必要的 console.log 语句- 更新项目依赖,添加 axios库
2025-01-16 11:48:12 +08:00
xingyy
5315b0fc0c refactor(home): 优化首页布局和国际化配置
- 添加默认布局和国际化配置
- 调整全局样式和组件样式
- 优化代码结构,提高可维护性
2025-01-16 11:27:12 +08:00
xingyy
e86c8dbf3b refactor(app): 重构首页组件
- 优化了拍品列表的渲染逻辑,使用计算属性生成左右两列数据
- 移除了冗余代码和注释,
2025-01-16 11:23:46 +08:00
xingyy
9ca23ceca7 refactor(LiveRoom): 优化出价按钮组件并引入 PressableButton
- 引入 PressableButton 组件用于出价按钮
2025-01-16 11:12:52 +08:00
xingyy
38e0cfcdb6 feat(LiveRoom): 实现支付功能并优化直播页面
- 添加支付输入和支付结果组件
- 集成环境变量配置- 优化直播播放器配置- 调整对话框样式
2025-01-16 11:07:38 +08:00
xingyy
f37f283f09 refactor(app): 修改首页全屏直播默认状态为关闭 2025-01-15 16:57:26 +08:00
xingyy
8cbb9f3e76 Merge branch 'xingyy' 2025-01-15 16:55:20 +08:00
xingyy
b6bd978a7a refactor(LiveRoom): 重构直播间页面并添加广播组件
- 移除直播间页面中的静态广播信息
- 新增广播组件以动态显示广播消息
- 优化拍品信息按钮样式
2025-01-15 16:45:29 +08:00
xingyy
df440e49e5 refactor(live): 重构直播间界面并添加支付功能
-移除原有的直播界面组件,改为使用新的 sideButton 组件
- 新增支付对话框,允许用户输入支付金额
- 使用 Pinia 创建 liveStore 来管理直播相关状态
- 优化了出价开关逻辑,现在通过 liveStore进行管理
- 调整了侧边按钮的样式和布局,提高了用户体验
2025-01-15 16:10:28 +08:00
scout
bad576f5f3 fix(header): 修正header组件配置引用路径从footer改为header 2025-01-15 13:10:12 +08:00
scout
6836b6e60b fix(footer): 添加v-if条件控制底部导航栏显示 2025-01-15 13:10:12 +08:00
scout
569e805249 Merge branch 'main' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt into qb 2025-01-15 13:06:01 +08:00
scout
21194812c4 fix: 注释掉测试代码 2025-01-15 13:05:07 +08:00
scout
bc94c51e2d chore(config): 修改devServer host配置为0.0.0.0 2025-01-15 13:05:07 +08:00
scout
4e03cb3f6d fix(config): 修复AppHeader路由白名单配置 2025-01-15 13:05:07 +08:00
scout
c93a7fcb93 feat(auth): 新增实名认证页面,支持大陆居民和非大陆居民认证 2025-01-15 13:05:07 +08:00
scout
d27e5268f3 feat(country): 新增国家地区选择功能,支持常用国家快速选择 2025-01-15 13:05:07 +08:00
scout
00188a9d48 feat(login): 实现登录页面功能,包含手机号输入和验证码验证 2025-01-15 13:05:06 +08:00
scout
4d92cc13fc feat(i18n): 添加登录、实名认证等页面的多语言支持 2025-01-15 13:05:06 +08:00
xingyy
3c6a3b8370 Merge branch 'main' into xingyy 2025-01-15 12:00:24 +08:00
xingyy
2f2066f3e5 refactor(layouts): 调整默认布局和直播室页面样式
- 移除 default.vue 中的 fullLive 条件判断- 优化 LiveRoom 页面布局- 更新 home store 中的 fullLive 默认值为 true
-调整首页 changeLive 扩展样式
- 删除 specialMessage 组件
2025-01-15 12:00:19 +08:00
xingyy
7026357c09 feat(LiveRoom): 添加特殊信息组件并调整直播页面逻辑
- 在 LiveRoom 直播页面中引入 specialMessage 组件
-移除 config 的引入,简化页面逻辑
- 新增 specialMessage组件文件,提供特殊信息展示功能
2025-01-15 11:54:37 +08:00
xingyy
433c3af1ff refactor(LiveRoom): 删除未使用的导入语句
删除了 index.client.vue 文件中的未使用导入语句,包括:
- aliyun
2025-01-15 11:49:24 +08:00
xingyy
824ffca63b feat(LiveRoom): 优化直播间页面布局和功能
-调整了直播间的布局结构,增加了出价和支付相关功能- 添加了竞拍记录的显示,包括领先者、竞价时间等信息
- 优化了开启出价按钮的样式和提示文字- 调整了播放器的初始化配置,提高了稳定性
2025-01-15 11:45:46 +08:00
xingyy
03d22960f0 feat(home): 优化直播间展开效果
- 修改直播间展开逻辑,点击后始终展开
- 添加淡入淡出动画效果
- 优化直播间样式,增加自定义 CSS 类
2025-01-15 10:55:22 +08:00
xingyy
8c57db5764 style(home): 移除首页背景颜色属性
- 删除了 index.vue 文件中 div 元素的 bg-#fff 类,以移除白色背景
- 此修改旨在调整首页的视觉效果,可能为后续样式添加做准备
2025-01-15 09:32:25 +08:00
xingyy
68935e03a2 refactor(components): 优化 AppFooter 组件并修复首页布局问题- 移除 AppFooter 组件中的冗余 v-if 指令
- 修复首页直播模块展开时页面布局错乱的问题- 优化直播模块展开和收起的动画效果
2025-01-15 09:30:29 +08:00
xingyy
3c8cd2c782 refactor(app): 移除 KeepAlive 功能
- 删除了 KeepAlive 相关的代码和组件
- 移除了 Pinia相关的代码和组件
- 更新了 README 文档,删除了 Pinia 相关的说明- 移除了国际化文件中与 KeepAlive 相关的翻译项
2025-01-15 09:18:43 +08:00
xingyy
f54058b0ec 1231 2025-01-15 09:10:11 +08:00
xingyy
3a174276ce 1231 2025-01-14 09:49:10 +08:00
xingyy
7d483d45a7 1231 2025-01-14 09:16:01 +08:00
xingyy
d27e6bc0c5 feat(home): 实现首页直播间全屏功能
- 新增 fullLive 全局状态管理直播间的全屏状态
- 修改 AppHeader组件,根据 fullLive 状态控制返回按钮的显示
- 更新 default 布局,根据 fullLive 状态控制 AppFooter 的显示- 调整 LiveRoom组件,支持全屏模式下的布局变化
- 修改 home 页面,实现直播间全屏和列表之间的切换
2025-01-13 20:59:25 +08:00
xingyy
16bc0d6acc feat(LiveRoom): 优化直播室页面并添加新功能
- 重构了 LiveRoom 页面的代码结构,提高了可维护性
- 添加了新的竞拍功能,包括开启出价和确认出价按钮
- 优化了播放器配置,增加了预加载、自动播放等设置
- 增加了播放器错误处理机制,提高了用户体验
- 调整了页面布局,使其更加适应不同屏幕尺寸
2025-01-13 16:52:59 +08:00
xingyy
f5746e695b refactor(app): 优化代码结构和开发配置
- 移除了 LiveRoom 组件中的冗余控制台日志输出
-调整了 LiveRoom 组件的样式,使其占据剩余空间
- 隐藏了 Prism Player 的控制条
- 简化了 nuxt.config.js 中的导入语句
- 在 package.json 中添加了新的开发和构建脚本
2025-01-13 16:30:56 +08:00
xingyy
512a1a58ce build: 更新项目构建脚本和依赖
- 移除 cross-env 依赖
- 更新脚本命令,使用 nuxt 命令替代 cross-env
- 添加 generate、postinstall、typecheck 和 release脚本
2025-01-13 15:30:01 +08:00
xingyy
f1422cdf46 feat(LiveRoom): 新增直播间功能并优化项目配置
- 添加 Aliplayer 直播播放器- 实现竞拍功能和界面
- 更新项目依赖,包括 aliyun-aliplayer 和 cross-env
- 调整环境变量配置方式
- 移除未使用的消息提示功能
2025-01-13 15:28:22 +08:00
xingyy
24803f978a build:移除 ESLint配置项
- 从 nuxt.config.js 中删除了 eslint配置对象
- 此更改简化了项目配置,减少了不必要的设置
2025-01-13 14:04:38 +08:00
xingyy
c0700dcae7 Merge remote-tracking branch 'origin/main' 2025-01-13 14:03:26 +08:00
xingyy
0c721c03d8 feat(LiveRoom): 添加当前价和下口价滚动文本
- 在 LiveRoom 组件中添加当前价和下口价的滚动文本效果
- 使用 van-rolling-text 组件实现数字滚动动画
- 通过 CSS 调整滚动文本的样式,使其宽度适应内容
2025-01-13 14:03:12 +08:00
scout
c92cfa672f Merge branch 'main' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-13 14:00:37 +08:00
scout
593c55f2af perf(mobile): 优化移动端视口配置和PWA支持 2025-01-13 14:00:35 +08:00
scout
b943b3561f feat(i18n): 添加国家选择器的多语言翻译 2025-01-13 14:00:35 +08:00
scout
76194063a6 feat(i18n): 支持多语言切换的国家选择器组件 2025-01-13 14:00:35 +08:00
scout
d642228daa feat(i18n): 添加日语和繁体中文的国家名称数据 2025-01-13 14:00:35 +08:00
xingyy
493a30f4d5 feat(LiveRoom): 添加直播间页面并配置开发服务器
- 新增 LiveRoom 页面,包含视频播放器和直播间功能按钮
- 修改 nuxt.config.js,将开发服务器主机设置为 localhost,端口保持3000
2025-01-13 13:55:32 +08:00
xingyy
096df06e5d Merge remote-tracking branch 'origin/main' 2025-01-13 11:33:21 +08:00
xingyy
b9aaef6b47 style:移除国家和地区页面中 van-index-bar 组件的多余 class 属性
- 删除了 van-index-bar 组件中的 class 属性,以简化代码结构
-此修改不会影响页面功能,仅优化了代码的可读性和维护性
2025-01-13 11:33:15 +08:00
scout
f5a83e6c3d Merge branch 'main' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-13 11:31:05 +08:00
scout
20769f0fd7 Merge branch 'qb' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-13 11:30:53 +08:00
xingyy
cd427c8353 Merge branch 'xingyy' 2025-01-13 11:30:40 +08:00
xingyy
1e1e7c320f feat(login): 设置登录页面布局- 在登录页面组件中添加了 definePageMeta 以设置登录布局
- 新增 layout 属性,值为 'login',以应用登录布局样式
2025-01-13 11:30:20 +08:00
scout
2397f86d58 feat(i18n): 添加个人资料页面翻译文案 2025-01-13 11:25:37 +08:00
scout
36611ab0e9 feat(header): 实现动态标题和副标题显示 2025-01-13 11:25:37 +08:00
scout
7fb3599feb feat(types): 添加路由元数据subTitle字段定义 2025-01-13 11:25:37 +08:00
xingyy
b102c6308f Merge branch 'xingyy' 2025-01-13 11:22:49 +08:00
xingyy
c32a64456e refactor(layouts): 优化默认布局结构
- 调整 default.vue 中的主容器样式,使其更加灵活
- 在 home/index.vue 和 profile/index.vue 中应用 flex 布局,提高页面结构的适应性
- 优化代码格式和缩进,提高可读性
2025-01-13 11:12:11 +08:00
xingyy
873fb1aa32 feat(profile): 添加用户个人中心页面
- 设计并实现用户个人中心页面布局
- 添加用户信息展示区域
- 实现我的拍品列表展示
- 优化页面样式,添加背景图和样式调整
2025-01-13 10:55:10 +08:00
xingyy
37b7147eb3 feat(i18n): 添加主页国际化支持并优化页面布局
- 在 AppHeader 组件中添加国际化支持,根据路由元数据动态显示标题
- 在主页页面中添加页面元数据,包括布局、标题和国际化标识
- 优化 AppHeader 组件结构,提高可维护性
2025-01-13 09:57:40 +08:00
xingyy
e5fdd4e7ef refactor(components): 将拍品详情移至独立组件
-将拍品详情从首页组件中抽离,创建独立的 itemDetail 组件
- 在首页中引入并使用新的 itemDetail 组件- 优化代码结构,提高组件的可复用性和可维护性
2025-01-13 09:40:19 +08:00
xingyy
8235b9a779 feat(itemDetail): 新增拍品详情页面组件
- 添加 itemDetail 页面的基本结构和样式
- 在 home 页面中添加拍品详情弹窗的打开事件
-调整拍品详情弹窗的内容和样式
2025-01-13 09:37:34 +08:00
xingyy
32594d035c Merge branch 'xingyy'
# Conflicts:
#	app/layouts/default.vue
#	app/pages/home/index.vue
#	app/pages/profile/index.vue
2025-01-10 16:57:28 +08:00
xingyy
f146f2fb45 2312 2025-01-10 16:55:43 +08:00
xingyy
c04297d20a feat(AppHeader): 添加自定义标题和副标题
- 在 AppHeader 组件中添加了自定义标题和副标题
- 使用 flex布局实现了标题和副标题的居中显示
- 设置了不同的文本颜色和字体大小,提高了可读性
2025-01-10 16:52:58 +08:00
xingyy
aedc187a96 style(layout): 调整默认布局样式并更新个人资料页面- 移除 default.vue 中的多余内边距
- 更新 profile 页面的显示内容
2025-01-10 16:47:27 +08:00
xingyy
c132816a9f 1 2025-01-10 16:45:18 +08:00
xingyy
0b9cf3c48e feat(layout): 添加顶部导航栏并更新页面样式
- 在 default.vue 中添加 van-nav-bar 组件作为顶部导航栏
- 更新 home、profile 和 prose 页面的样式
- 移除 prose 页面的原有内容
- 添加 vueuse 依赖
2025-01-10 16:32:55 +08:00
xingyy
c734e4ffd5 refactor(app): 优化首页拍品详情样式
- 调整拍品详情背景颜色
- 优化轮播图样式
- 增加拍品信息、起拍价和竞价表等内容- 优化样式和布局
2025-01-10 15:50:57 +08:00
xingyy
ea89149d42 feat(home): 点击图片全屏预览
- 修改 clickSwipe 函数,使用 showImagePreview 方法实现全屏图片预览
- 在 van-swipe-item 中添加 @click 事件,传递图片索引给 clickSwipe 函数
2025-01-10 15:06:48 +08:00
xingyy
a5f914ac81 12 2025-01-10 15:02:49 +08:00
xingyy
635a1b90bb 12 2025-01-10 15:00:58 +08:00
xingyy
07c7dfaa78 12 2025-01-10 14:59:54 +08:00
xingyy
2db6406531 refactor(home): 优化首页布局计算
- 移除了未使用的 leftColumn 计算属性
- 删除了多余的注释和空行,提高代码可读性
2025-01-10 14:06:13 +08:00
xingyy
1bd7cc04f6 1 2025-01-10 14:03:51 +08:00
xingyy
40efacf65a feat(login): 添加国家地区选择功能并创建实名认证页面
- 在登录页面添加国家地区选择功能,点击可跳转到国家地区选择页面
- 新增实名认证页面,包括大陆居民和非大陆居民两个选项卡
- 实名认证页面采用vant组件库,样式进行了自定义
2025-01-10 14:00:42 +08:00
xingyy
afe57c16f7 1231 2025-01-10 13:23:11 +08:00
xingyy
b9fb752bb0 feat(country-region): 添加国家和地区选择功能
- 新增国家和地区数据文件,包含世界各国的名称、代码和区号
- 实现按拼音首字母分组的国家列表展示
- 添加搜索功能和回到顶部按钮
- 优化页面样式和交互
2025-01-10 13:06:36 +08:00
xingyy
64536663d7 feat(login): 实现手机号登录功能
- 新增手机号登录页面组件
- 添加手机号输入和验证码获取功能
- 实现验证码输入和登录功能
- 优化页面样式和布局
2025-01-10 09:56:19 +08:00
xingyy
9092b59027 refactor(app): 重构应用配置和页面组件
- 更新 AppFooter 组件中的路由名称引用
- 重命名 config 文件夹下的 index.ts 为 index.js
- 删除 NationalMap 组件
- 新增 login 页面组件
- 更新 home 页面组件,移除 masonry 样式
- 更新 nuxt 配置,启用 pages:extend钩子
- 移除 package.json 中的 vue-masonry-wall依赖
2025-01-09 19:57:50 +08:00
xingyy
a8e2faae0f refactor(layouts): 优化默认布局结构
- 移除 AppHeader 组件
- 调整页面主体的样式和布局refactor(components): 优化 XMessage 组件样式

- 调整样式属性顺序
- 优化模板结构

refactor(pages):重构首页布局

- 添加新的 LiveBroadcast 组件
- 实现拍品列表的瀑布流布局
- 优化拍品卡片样式

refactor(config): 更新项目配置

- 移除 pages:extend钩子
- 更新兼容性日期
2025-01-09 19:37:03 +08:00
xingyy
530c5b1026 Merge branch 'main' into xingyy 2025-01-09 19:02:35 +08:00
xingyy
525cec428f feat(layout): 重构首页布局并添加新功能
- 重写首页模板,使用新的拍卖列表组件
- 添加下拉刷新和加载更多功能
- 新增拍卖说明页面
- 使用自定义图标替换默认图标
- 优化消息提示组件
2025-01-09 19:01:35 +08:00
xingyy
6d815b5adb Merge branch 'main' into xingyy 2025-01-08 16:21:12 +08:00
scout
d47c6d9cec Merge branch 'main' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-08 16:06:45 +08:00
scout
c7f4bf34bd feat(i18n): add keywords translations for all supported languages 2025-01-08 16:06:43 +08:00
scout
a214b6c8a6 feat(seo): add keywords meta tag to app head 2025-01-08 16:06:43 +08:00
xingyy
ef5fe41848 Merge branch 'main' into xingyy
# Conflicts:
#	app/components/AppFooter.vue
2025-01-08 15:48:05 +08:00
xingyy
44dfbf0fc1 test 2025-01-08 15:36:30 +08:00
scout
9081080035 Merge branch 'main' of https://gitea-inner.fontree.cn/scout666/liveh5-nuxt 2025-01-08 15:34:08 +08:00
scout
2006b4cddb 国际化配置文件 2025-01-08 15:34:07 +08:00
scout
51d7a131d8 docs(license): 删除MIT许可证文件 2025-01-08 15:32:55 +08:00
scout
fe58ca59d8 feat(ui): 添加页面切换过渡动画效果 2025-01-08 15:32:55 +08:00
scout
e2b4805b55 feat(app): 更新应用名称和描述为豐和拍卖会 2025-01-08 15:32:54 +08:00
scout
d5dac69760 feat(i18n): 添加日语和繁体中文支持 2025-01-08 15:32:54 +08:00
xingyy
861ba5118d Merge branch 'dev' into xingyy
# Conflicts:
#	app/components/AppFooter.vue
2025-01-08 15:30:34 +08:00
xingyy
c8486bf4cc Merge branch 'xingyy' 2025-01-08 15:29:36 +08:00
xingyy
e3239b5368 test 2025-01-08 15:28:31 +08:00
xingyy
bbb8e51648 test 2025-01-08 15:26:58 +08:00
130 changed files with 8855 additions and 3637 deletions

1
.env
View File

@ -1 +0,0 @@
NUXT_PUBLIC_API_BASE=https://easyapi.devv.zone

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Charlie Wang ✨
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -39,9 +39,6 @@
- 🔥 `<script setup>` 语法
- 🌍 [国际化支持](./i18n/locales)
- 🍍 [使用 Pinia 进行状态管理](https://github.com/vuejs/pinia),查看 [./app/composables/counter.ts](./app/composables/counter.ts)
- 📑 [布局系统](./app/layouts)
- 📥 API 自动导入 - 用于 Composition API 和自定义组合式函数
@ -57,8 +54,6 @@
- [i18n](https://github.com/nuxt-modules/i18n) - Nuxt 的国际化模块
- [ColorMode](https://github.com/nuxt-modules/color-mode) - 支持自动检测的深色和浅色模式
- [UnoCSS](https://github.com/unocss/unocss) - 即时按需原子化 CSS 引擎
- [Pinia](https://github.com/vuejs/pinia) - 直观、类型安全、轻量且灵活的 Vue 状态管理
- [Pinia Persistedstate](https://github.com/prazdevs/pinia-plugin-persistedstate) - 可配置的 Pinia 存储持久化和重新激活
- [DevTools](https://github.com/nuxt/devtools) - 释放 Nuxt 开发者体验
## IDE

View File

@ -0,0 +1,23 @@
import { request } from '@/api/http.js'
export async function checkPhone(data) {
return await request({
url:'/api/v1/common/check/phone',
method: 'POST',
data
})
}
export async function userSend(data) {
return await request( {
url:'/api/v1/m/user/send',
method: 'POST',
data
})
}
export async function mobileLogin(data) {
return await request( {
url:'/api/v1/m/user/mobile/login',
method: 'POST',
data
})
}

View File

@ -0,0 +1,31 @@
import { request } from '@/api/http.js'
export async function offlineQrcodeList(data) {
return await request( {
url:'/api/v1/offlineQrcode/query',
method: 'POST',
data
})
}
export async function offlineQrcodeCreate(data) {
return await request ({
url:'/api/v1/offlineQrcode/create',
method: 'POST',
data
})
}
export async function offlineQrcodeDelete(data) {
return await request ({
url:'/api/v1/offlineQrcode/delete',
method: 'POST',
data
})
}
export async function userArtworks(data) {
return await request( {
url:'/api/v1/m/user/artworks',
method: 'POST',
data
})
}

View File

@ -0,0 +1,128 @@
import {useRuntimeConfig} from '#app'
import {ofetch} from 'ofetch'
import {message} from '@/components/x-message/useMessage.js'
import {codeAuthStore} from "@/stores-collect-code/auth/index.js"
let httpStatusErrorHandler
let http
// HTTP 状态码映射
const HTTP_STATUS_MAP = {
400: '请求参数错误',
401: '未授权或登录过期',
403: '访问被禁止',
404: '请求的资源不存在',
500: '服务器内部错误',
502: '网关错误',
503: '服务暂时不可用',
504: '网关超时'
}
export function setupHttp() {
if (http) return http
const {token}= codeAuthStore()
const config = useRuntimeConfig()
const baseURL = config.public.NUXT_PUBLIC_API_COLLECT_CODE
const router = useRouter()
const defaultOptions = {
baseURL,
headers: { 'Content-Type': 'application/json' },
timeout: 15000, // 15秒超时
retry: 3,
retryDelay: 1000,
}
http = ofetch.create({
...defaultOptions,
// 请求拦截
async onRequest({ options, request }) {
// 添加 token
options.headers = {
...options.headers,
Authorization: token.value
}
// GET 请求添加时间戳防止缓存
if (request.toLowerCase().includes('get')) {
options.params = {
...options.params,
_t: Date.now()
}
}
},
// 响应拦截
async onResponse({ response }) {
const data = response._data
// 处理业务错误
if (data.status === 1) {
message.error(data.msg || '操作失败')
}
// 处理登录失效
if (data.status === 401) {
message.error('登录已过期,请重新登录')
token.value = '' // 清除 token
router.replace('/collectCode/login')
}
return response
},
// 响应错误处理
async onResponseError({ response, request }) {
// 网络错误
if (!response) {
message.error('网络连接失败,请检查网络设置')
return Promise.reject(new Error('网络错误'))
}
const status = response.status
const data = response._data
// 处理 HTTP 状态错误
const errorMessage = data.msg || HTTP_STATUS_MAP[status] || '请求失败'
if (Array.isArray(data.msg)) {
data.msg.forEach(item => {
httpStatusErrorHandler?.(item, status)
})
} else {
httpStatusErrorHandler?.(errorMessage, status)
}
message.error(errorMessage)
return Promise.reject(data)
},
})
return http
}
export function createAbortController() {
return new AbortController()
}
export function injectHttpStatusErrorHandler(handler) {
httpStatusErrorHandler = handler
}
export function getHttp() {
if (!http) {
throw new Error('HTTP client not initialized. Call setupHttp first.')
}
return http
}
// 导出请求工具函数
export async function request({url,...options}) {
const http = getHttp()
try {
return await http(url, {...options,body:options.data})
} catch (error) {
throw error
}
}

25
app/api/auth/index.js Normal file
View File

@ -0,0 +1,25 @@
import { request } from '@/api/http.js'
export async function senCode(data) {
return await request({
url:'/api/v1/m/user/send',
method: 'POST',
data
})
}
export async function userLogin(data) {
return await request( {
url:'/api/v1/m/user/login',
method: 'POST',
data
})
}
export async function userUpdate(data) {
return await request( {
url:'/api/v1/m/user/update',
method: 'POST',
data
})
}

56
app/api/goods/index.js Normal file
View File

@ -0,0 +1,56 @@
import { request } from '@/api/http.js'
export async function artworkList(data) {
return await request( {
url:'/api/v1/m/auction/default/artwork/list',
method: 'POST',
data
})
}
export async function defaultDetail(data) {
return await request ({
url:'/api/v1/m/auction/default/detail',
method: 'POST',
data
})
}
export async function artworkDetail(data) {
return await request( {
url:'/api/v1/m/artwork/detail',
method: 'POST',
data,
})
}
export async function userArtworks(data) {
return await request( {
url:'/api/v1/m/user/artworks',
method: 'POST',
data
})
}
export async function userArtwork(data) {
return await request( {
url:'/api/v1/m/user/artwork',
method: 'POST',
data
})
}
export async function artworkBuy(data) {
return await request( {
url:'/api/v1/m/artwork/buy',
method: 'POST',
data
})
}
export async function logSendlog(data) {
return await request( {
url:'/api/v1/m/auction/log/sendlog',
method: 'POST',
data
})
}

128
app/api/http.js Normal file
View File

@ -0,0 +1,128 @@
import {useRuntimeConfig} from '#app'
import {ofetch} from 'ofetch'
import {message} from '@/components/x-message/useMessage.js'
import {authStore} from "@/stores/auth/index.js"
let httpStatusErrorHandler
let http
// HTTP 状态码映射
const HTTP_STATUS_MAP = {
400: '请求参数错误',
401: '未授权或登录过期',
403: '访问被禁止',
404: '请求的资源不存在',
500: '服务器内部错误',
502: '网关错误',
503: '服务暂时不可用',
504: '网关超时'
}
export function setupHttp() {
if (http) return http
const config = useRuntimeConfig()
const baseURL = config.public.NUXT_PUBLIC_API_BASE
const { token } = authStore()
const router = useRouter()
const defaultOptions = {
baseURL,
headers: { 'Content-Type': 'application/json' },
timeout: 15000, // 15秒超时
retry: 3,
retryDelay: 1000,
}
http = ofetch.create({
...defaultOptions,
// 请求拦截
async onRequest({ options, request }) {
// 添加 token
options.headers = {
...options.headers,
Authorization: token.value
}
// GET 请求添加时间戳防止缓存
if (request.toLowerCase().includes('get')) {
options.params = {
...options.params,
_t: Date.now()
}
}
},
// 响应拦截
async onResponse({ response }) {
const data = response._data
// 处理业务错误
if (data.status === 1) {
message.error(data.msg || '操作失败')
}
// 处理登录失效
if (data.status === 401) {
message.error('登录已过期,请重新登录')
token.value = '' // 清除 token
router.replace('/login')
}
return response
},
// 响应错误处理
async onResponseError({ response, request }) {
// 网络错误
if (!response) {
message.error('网络连接失败,请检查网络设置')
return Promise.reject(new Error('网络错误'))
}
const status = response.status
const data = response._data
// 处理 HTTP 状态错误
const errorMessage = data.msg || HTTP_STATUS_MAP[status] || '请求失败'
if (Array.isArray(data.msg)) {
data.msg.forEach(item => {
httpStatusErrorHandler?.(item, status)
})
} else {
httpStatusErrorHandler?.(errorMessage, status)
}
message.error(errorMessage)
return Promise.reject(data)
},
})
return http
}
export function createAbortController() {
return new AbortController()
}
export function injectHttpStatusErrorHandler(handler) {
httpStatusErrorHandler = handler
}
export function getHttp() {
if (!http) {
throw new Error('HTTP client not initialized. Call setupHttp first.')
}
return http
}
// 导出请求工具函数
export async function request({url,...options}) {
const http = getHttp()
try {
return await http(url, {...options,body:options.data})
} catch (error) {
throw error
}
}

View File

@ -1,55 +0,0 @@
import type { $Fetch } from 'ofetch'
import { useRuntimeConfig } from '#app'
import { ofetch } from 'ofetch'
type HttpStatusErrorHandler = (message: string, statusCode: number) => void
let httpStatusErrorHandler: HttpStatusErrorHandler
let http: $Fetch
export function setupHttp() {
if (http)
return http
const config = useRuntimeConfig()
const baseURL = config.public.apiBase as string
http = ofetch.create({
baseURL,
headers: { 'Content-Type': 'application/json' },
async onRequest({ options }) {
const token = localStorage.getItem('token')
options.headers = {
...options.headers,
...(token && { Authorization: `Bearer ${token}` }),
}
},
async onResponseError({ response }) {
const { message } = response._data
if (Array.isArray(message)) {
message.forEach((item) => {
httpStatusErrorHandler?.(item, response.status)
})
}
else {
httpStatusErrorHandler?.(message, response.status)
}
return Promise.reject(response._data)
},
retry: 3,
retryDelay: 1000,
})
}
export function injectHttpStatusErrorHandler(handler: HttpStatusErrorHandler) {
httpStatusErrorHandler = handler
}
export function getHttp() {
if (!http) {
throw new Error('HTTP client not initialized. Call setupHttp first.')
}
return http
}

View File

@ -1,8 +0,0 @@
import { getHttp } from './http'
export async function getProse() {
const http = getHttp()
return await http('/api/prose', {
method: 'GET',
})
}

View File

@ -1,28 +1,86 @@
<script setup lang="ts">
import type { ConfigProviderTheme } from 'vant'
import useKeepalive from '~/composables/keepalive'
import { appName } from '~/constants'
<script setup>
import {useI18n} from 'vue-i18n'
import {message} from '@/components/x-message/useMessage.js'
// message.success('success')
useHead({
title: appName,
title: useI18n().t('appSetting.appName'),
meta: [
{name: 'description', content: useI18n().t('appSetting.appDescription')},
{name: 'keywords', content: useI18n().t('appSetting.appKeyWords')},
],
})
const color = useColorMode()
const mode = computed(() => {
return color.value as ConfigProviderTheme
//
const router = useRouter()
const route = useRoute()
const slideDirection = ref('slide-left')
//
const routeHistory = ref([])
router.beforeEach((to, from) => {
//
routeHistory.value.push(from.path)
//
if (routeHistory.value.includes(to.path)) {
slideDirection.value = 'slide-right'
//
const index = routeHistory.value.indexOf(to.path)
routeHistory.value = routeHistory.value.slice(0, index)
} else {
slideDirection.value = 'slide-left'
}
})
const keepAliveRouteNames = computed(() => {
return useKeepalive().routeCaches as string[]
})
//
provide('slideDirection', slideDirection)
</script>
<template>
<VanConfigProvider :theme="mode">
<NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
<VanConfigProvider>
<NuxtLoadingIndicator
color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)"/>
<NuxtLayout>
<NuxtPage :keepalive="{ include: keepAliveRouteNames }" />
<NuxtPage :transition="{
name: slideDirection
}"/>
</NuxtLayout>
</VanConfigProvider>
</template>
<style>
:root:root {
--van-dialog-radius: 8px
}
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: absolute;
width: 100%;
}
.slide-left-enter-from {
transform: translateX(100%);
}
.slide-left-leave-to {
transform: translateX(-100%);
}
.slide-right-enter-from {
transform: translateX(-100%);
}
.slide-right-leave-to {
transform: translateX(100%);
}
:root {
--safe-area-inset-bottom: env(safe-area-inset-bottom);
}
</style>

View File

@ -1,30 +1,38 @@
<script setup lang="ts">
import { useAppFooterRouteNames as names } from '~/config'
<script setup>
import { useAppFooterRouteNames as names } from '@/config/index.js'
import MyIcon from "@/components/icons/MyIcon.vue";
import HomeIcon from "@/components/icons/HomeIcon.vue";
const route = useRoute()
const active = ref(0)
const show = computed(() => {
if (route.name && names.includes(route.name))
return true
return false
})
const initData=()=>{
active.value=route.path==='/profile'?1:0
}
watchEffect(initData)
onMounted(()=>{
initData()
})
</script>
<template>
<van-tabbar v-if="show" v-model="active" route placeholder fixed>
<van-tabbar-item replace to="/">
<span>{{ $t('tabbar.home') }}</span>
<template #icon>
<div class="i-carbon:home" />
</template>
</van-tabbar-item>
<van-tabbar-item replace to="/profile">
<span>{{ $t('tabbar.profile') }}</span>
<template #icon>
<div class="i-carbon:user" />
</template>
</van-tabbar-item>
</van-tabbar>
<div v-if="show" v-memo="[active]" >
<van-tabbar v-model="active" route placeholder fixed>
<van-tabbar-item replace to="/">
<span>{{ $t('tabbar.home') }}</span>
<template #icon>
<HomeIcon :active="active===0"></HomeIcon>
</template>
</van-tabbar-item>
<van-tabbar-item replace to="/profile">
<span>{{ $t('tabbar.profile') }}</span>
<template #icon>
<MyIcon :active="active===1"></MyIcon>
</template>
</van-tabbar-item>
</van-tabbar>
</div>
</template>

View File

@ -1,10 +1,14 @@
<script setup lang="ts">
import { useAppFooterRouteNames as routeWhiteList } from '~/config'
<script setup>
import { useAppHeaderRouteNames as routeWhiteList } from '@/config'
import { liveStore } from "@/stores/live/index.js";
const { fullLive } = liveStore()
const route = useRoute()
const router = useRouter()
function onBack() {
if (fullLive.value){
fullLive.value=false
return
}
if (window.history.state.back)
history.back()
else
@ -14,19 +18,34 @@ function onBack() {
const { t } = useI18n()
const title = computed(() => {
if (!route.meta)
return ''
return route.meta.i18n ? t(route.meta.i18n) : (route.meta.title || '')
})
const subTitle = computed(() => {
if (!route.meta)
return ''
return route.meta.subTitle ? t(route.meta.subTitle) : ''
})
const showLeftArrow = computed(() => route.name && routeWhiteList.includes(route.name))
</script>
<template>
<VanNavBar
v-memo="[title,fullLive,showLeftArrow,subTitle]"
:title="title"
:left-arrow="!showLeftArrow"
:left-arrow="!showLeftArrow||fullLive"
placeholder clickable fixed
@click-left="onBack"
/>
>
<template #title v-if="route.meta.i18n==='menu.goods'">
<div class="flex flex-col items-center justify-center">
<div class="text-#000000 text-17px mb-5px font-600">{{ title }}</div>
<div class="text-#939393 text-10px line-height-none font-100">{{subTitle}}</div>
</div>
</template>
</VanNavBar>
</template>

View File

@ -0,0 +1,181 @@
<template>
<div class="signature-pad-container">
<canvas
ref="canvasRef"
class="signature-pad"
:style="{
width: '100%',
height: '100%',
backgroundColor: '#fff',
border: '1px solid #e5e5e5',
borderRadius: '4px'
}"
@touchstart="handleStart"
@touchmove="handleMove"
@touchend="handleEnd"
@mousedown="handleStart"
@mousemove="handleMove"
@mouseup="handleEnd"
@mouseleave="handleEnd"
></canvas>
<div class="signature-controls">
<van-button
type="default"
size="small"
@click="clearCanvas"
>清除</van-button>
<van-button
type="primary"
size="small"
@click="handleConfirm"
>确认</van-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const canvasRef = ref(null)
const ctx = ref(null)
const isDrawing = ref(false)
const lastX = ref(0)
const lastY = ref(0)
const LINE_WIDTH = 2 //
//
const initCanvas = () => {
const canvas = canvasRef.value
const dpr = window.devicePixelRatio || 1
const rect = canvas.getBoundingClientRect()
//
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
ctx.value = canvas.getContext('2d')
//
ctx.value.scale(dpr, dpr)
ctx.value.lineCap = 'round'
ctx.value.lineJoin = 'round'
ctx.value.strokeStyle = '#000'
ctx.value.lineWidth = LINE_WIDTH
}
//
const handleStart = (e) => {
e.preventDefault() //
isDrawing.value = true
const point = getPoint(e)
lastX.value = point.x
lastY.value = point.y
}
//
const handleMove = (e) => {
if (!isDrawing.value) return
e.preventDefault() //
const point = getPoint(e)
ctx.value.beginPath()
ctx.value.moveTo(lastX.value, lastY.value)
ctx.value.lineTo(point.x, point.y)
ctx.value.stroke()
lastX.value = point.x
lastY.value = point.y
}
//
const handleEnd = () => {
isDrawing.value = false
}
//
const getPoint = (e) => {
const canvas = canvasRef.value
const rect = canvas.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
const event = e.touches ? e.touches[0] : e
//
const x = (event.clientX - rect.left)
const y = (event.clientY - rect.top)
return {
x: x,
y: y
}
}
//
const clearCanvas = () => {
const canvas = canvasRef.value
ctx.value.clearRect(0, 0, canvas.width, canvas.height)
emit('update:modelValue', '')
emit('change', '')
}
//
const handleConfirm = () => {
const canvas = canvasRef.value
const imageData = canvas.toDataURL('image/png')
emit('update:modelValue', imageData)
emit('change', imageData)
}
//
const handleResize = () => {
const canvas = canvasRef.value
const imageData = canvas.toDataURL('image/png')
initCanvas()
//
const img = new Image()
img.onload = () => {
ctx.value.drawImage(img, 0, 0, canvas.width, canvas.height)
}
img.src = imageData
}
onMounted(() => {
initCanvas()
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.signature-pad-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
.signature-pad {
flex: 1;
touch-action: none;
}
.signature-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
padding: 8px;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div>
<img v-if="active" src="./images/home1.png" alt="" class="w-[18px] h-[20px]">
<img v-else src="./images/home2.png" alt="" class="w-[18px] h-[20px]">
</div>
</template>
<script setup>
defineProps({
active: {
type: Boolean,
default: false
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<div>
<img v-if="active" src="./images/my1.png" alt="" class="w-[18px] h-[20px]">
<img v-else src="./images/my2.png" alt="" class="w-[18px] h-[20px]">
</div>
</template>
<script setup>
defineProps({
active: {
type: Boolean,
default: false
}
})
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,55 @@
<script setup>
import { showImagePreview } from 'vant';
import xImage from '@/components/x-image/index.vue'
const props = defineProps({
detailInfo: {
type: Object,
default: null
}
})
</script>
<template>
<div>
<div class="flex justify-center">
<xImage class="h-188px" :src="detailInfo?.artwork?.hdPic"></xImage>
</div>
<div class="px-[16px] bg-[#fff] pt-[11px] mb-6px">
<div class="text-[#000] text-[16px] mb-[12px]">{{detailInfo?.artworkTitle}}</div>
<div class="text-#575757 text-[14px] pb-8px">
<div class="flex mb-[4px]">
<div class="w-[70px]">作者</div>
<div>{{detailInfo?.artwork?.artistName??'-'}}</div>
</div>
<div class="flex mb-[4px]">
<div class="w-[70px] flex-shrink-0">总平尺数</div>
<div>{{detailInfo?.artwork?.ruler??'-'}}</div>
</div>
<div class="flex mb-[4px]">
<div class="w-[70px] flex-shrink-0">*</div>
<div>{{detailInfo?.artwork?.length}}*{{detailInfo?.artwork?.width}}cm</div>
</div>
<div class="flex mb-[4px]">
<div class="w-[70px] flex-shrink-0">画作简介</div>
<div>{{detailInfo?.artwork?.abstract??'-'}}</div>
</div>
</div>
</div>
<div class="flex px-[16px] bg-#fff h-[36px] items-center mb-6px">
<div class="text-[#575757] text-[14px]">起拍价</div>
<div class="text-#575757 text-14px font-bold">RMB 1,000</div>
</div>
<div class="px-[16px] bg-#fff pt-12px pb-18px">
<div class="text-[#575757] text-[14px] mb-4px">竞价表</div>
<div v-if="detailInfo?.priceRuleType!=='diy'">
<xImage :src="detailInfo?.priceRuleImage" alt=""/>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,183 @@
<template>
<div ref="dragRef"
:style="style"
class="fixed rounded-5px overflow-hidden shadow-lg cursor-move z-50"
@mousedown.stop="handleDragStart"
@touchstart.stop="handleDragStart">
<div class="relative" @click.stop="handleClick">
<img :src="props.snapshot"
class="w-80px object-cover"
alt="直播画面">
<div class="absolute inset-0 bg-black/40 flex items-center justify-center"
>
<span class="text-white text-12px">点击回到直播</span>
</div>
<button @click.stop="handleClose"
class="absolute top-10px right-10px text-white bg-black/40 rounded-full w-5 h-5 flex items-center justify-center hover:bg-black/60 cursor-pointer">
<van-icon name="cross" />
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onBeforeUnmount, shallowRef, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useThrottleFn } from '@vueuse/core'
const props = defineProps({
snapshot: {
type: String,
required: true
},
onClose: {
type: Function,
default: () => {}
},
onClick: {
type: Function,
default: () => {}
},
initialPosition: {
type: Object,
default: () => ({
top: '80px',
right: '16px'
})
}
})
const router = useRouter()
const dragRef = shallowRef(null)
const route = useRoute()
const isDragging = ref(false)
const startX = ref(0)
const startY = ref(0)
const left = ref(0)
const top = ref(0)
onMounted(() => {
const rect = dragRef.value.getBoundingClientRect()
left.value = window.innerWidth - rect.width - parseInt(props.initialPosition.right)
top.value = parseInt(props.initialPosition.top)
})
const style = computed(() => ({
left: left.value ? `${left.value}px` : 'auto',
top: top.value ? `${top.value}px` : 'auto',
right: !left.value ? props.initialPosition.right : 'auto',
transition: isDragging.value ? 'none' : 'all 0.3s ease',
}))
const handleDragStart = (event) => {
event.stopPropagation()
isDragging.value = true
const point = event.touches ? event.touches[0] : event
const rect = dragRef.value.getBoundingClientRect()
left.value = rect.left
top.value = rect.top
startX.value = point.clientX - left.value
startY.value = point.clientY - top.value
if (event.type === 'mousedown') {
document.addEventListener('mousemove', handleDragMove)
document.addEventListener('mouseup', handleDragEnd)
} else {
document.addEventListener('touchmove', handleDragMove, { passive: false })
document.addEventListener('touchend', handleDragEnd)
}
}
const handleDragMove = useThrottleFn((event) => {
if (!isDragging.value) return
event.preventDefault()
event.stopPropagation()
const point = event.touches ? event.touches[0] : event
const rect = dragRef.value.getBoundingClientRect()
const maxX = window.innerWidth - rect.width
const maxY = window.innerHeight - rect.height
left.value = Math.min(Math.max(0, point.clientX - startX.value), maxX)
top.value = Math.min(Math.max(0, point.clientY - startY.value), maxY)
}, 16)
const handleEdgeSnap = useThrottleFn(() => {
const rect = dragRef.value.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
left.value = centerX > window.innerWidth / 2
? window.innerWidth - rect.width - 16
: 16
}, 100)
const handleDragEnd = () => {
if (!isDragging.value) return
isDragging.value = false
handleEdgeSnap()
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
document.removeEventListener('touchmove', handleDragMove)
document.removeEventListener('touchend', handleDragEnd)
}
const handleClick = (event) => {
event.stopPropagation()
if (!isDragging.value) {
handleReturnLive()
}
}
const handleReturnLive = () => {
props.onClick()
}
const handleClose = (event) => {
event?.stopPropagation()
props.onClose()
}
watch(() => route.path, (newVal) => {
if (['/','/home'].includes(newVal)){
handleClose()
}
})
onBeforeUnmount(() => {
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
document.removeEventListener('touchmove', handleDragMove)
document.removeEventListener('touchend', handleDragEnd)
})
</script>
<style scoped>
.fixed {
will-change: transform, opacity;
touch-action: none;
-webkit-user-select: none;
user-select: none;
transform: translateZ(0);
}
.min-window-enter-active,
.min-window-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.min-window-enter-from,
.min-window-leave-to {
transform: translateY(100%);
opacity: 0;
}
img {
pointer-events: none;
-webkit-user-drag: none;
user-drag: none;
}
</style>

View File

@ -0,0 +1,28 @@
<script setup>
import { ref } from "vue";
const isButtonActive = ref(false);
const handleButtonPress = (event) => {
event.stopPropagation();
isButtonActive.value = true;
};
const handleButtonRelease = (event) => {
event.stopPropagation();
isButtonActive.value = false;
};
</script>
<template>
<div
:class="[
'transition-all duration-200',
isButtonActive ? 'scale-95' : ''
]"
@touchstart.stop="handleButtonPress"
@touchend.stop="handleButtonRelease"
>
<slot></slot>
</div>
</template>

View File

@ -0,0 +1,52 @@
<script setup>
import { showImagePreview } from 'vant';
const props = defineProps({
src: {
type: String,
default: ''
},
preview: {
type: Boolean,
default: true
},
//
sizes: {
type: Array,
default: () => [320, 640, 768, 1024]
},
//
format: {
type: String,
default: 'webp'
},
//
quality: {
type: Number,
default: 80
}
})
const showImage = () => {
if (props.preview) {
showImagePreview([props.src]);
}
}
</script>
<template>
<nuxt-img
loading="lazy"
v-bind="{ ...props, ...$attrs }"
style="object-fit: cover"
@click="showImage"
:src="src"
/>
</template>
<style scoped>
:deep(img) {
width: 100%;
height: 100%;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,93 @@
<script setup>
import { ref, computed } from 'vue'
import MessageContent from './message/index.vue'
const visible = ref(false)
const messageType = ref('success')
const messageText = ref('')
const showIcon = ref(true)
const customStyle = ref({})
const title = ref({})
const subTitle = ref({})
const containerStyle = computed(() => {
const { top, bottom, left, right, transform, ...otherStyles } = customStyle.value || {}
const baseStyle = {
position: 'fixed',
zIndex: 9999
}
const horizontalPosition = left || right
? { left, right }
: { left: '50%', transform: 'translateX(-50%)' }
const verticalPosition = {}
if (bottom !== undefined) {
verticalPosition.bottom = bottom
} else {
verticalPosition.top = top || '50px'
}
return {
...baseStyle,
...horizontalPosition,
...verticalPosition,
...otherStyles
}
})
const emit = defineEmits(['after-leave'])
const showMessage = (options) => {
if (typeof options === 'string') {
messageText.value = options
title.value = {}
subTitle.value = {}
} else {
messageText.value = options.message || ''
title.value = options.title || {}
subTitle.value = options.subTitle || {}
}
messageType.value = options.type || 'success'
showIcon.value = options.icon !== false
customStyle.value = options.style || {}
visible.value = true
setTimeout(() => {
visible.value = false
}, options.duration || 2000)
}
defineExpose({ showMessage })
</script>
<template>
<transition
name="fade"
@after-leave="$emit('after-leave')"
>
<MessageContent
v-if="visible"
:message="messageText"
:type="messageType"
:title="title"
:sub-title="subTitle"
:show-icon="showIcon"
:style="containerStyle"
/>
</transition>
</template>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,121 @@
<script setup>
import error from '../images/error.png'
import success from '../images/success.png'
import warning from '../images/warning.png'
import info from '../images/info.png'
const props = defineProps({
type: {
type: String,
default: 'success'
},
message: {
type: String,
default: ''
},
title: {
type: Object,
default: () => ({
text: '',
color: '',
align: 'left'
})
},
subTitle: {
type: Object,
default: () => ({
text: '',
color: '',
align: 'left'
})
},
showIcon: {
type: Boolean,
default: true
},
style: {
type: Object,
default: () => ({})
}
})
const typeConfig = {
info: {
imgSrc: info,
borderColor: '#C6DFFB',
bgColor: '#ECF5FE',
},
success: {
imgSrc: success,
borderColor: '#C5E7D5',
bgColor: '#EDF7F2',
},
error: {
imgSrc: error,
borderColor: '#FFD4D4',
bgColor: '#FFF0F0',
},
warning: {
imgSrc: warning,
borderColor: '#FFE2BA',
bgColor: '#FFF7EC',
}
}
// 使 props.style
const finalStyle = computed(() => {
return {
borderColor: props.style?.borderColor || typeConfig[props.type].borderColor,
backgroundColor: props.style?.backgroundColor || typeConfig[props.type].bgColor,
width: props.style?.width || '343px',
height: props.style?.height || 'auto',
minHeight: '46px',
...props.style
}
})
</script>
<template>
<div
:class="`box-border flex items-center border rounded-[4px] px-[15px] shadow-sm py-8px ${!message?'justify-center':''}`"
:style="finalStyle"
>
<div v-if="showIcon" class="mr-[12px]">
<img
:src="typeConfig[type].imgSrc"
class="w-20px h-20px"
style="object-fit: contain"
alt=""
>
</div>
<div class="flex flex-col justify-center">
<!-- 如果是简单文本模式 -->
<div v-if="message" class="text-[14px] line-height-none">
{{ message }}
</div>
<!-- 如果是标题+副标题模式 -->
<template v-else>
<div
v-if="title.text"
class="text-[14px] line-height-20px"
:style="{
color: title.color || 'inherit',
textAlign: title.align || 'left'
}"
>
{{ title.text }}
</div>
<div
v-if="subTitle.text"
class="text-[12px] leading-normal mt-1 line-height-17px"
:style="{
color: subTitle.color || '#939393',
textAlign: subTitle.align || 'left'
}"
>
{{ subTitle.text }}
</div>
</template>
</div>
</div>
</template>

View File

@ -0,0 +1,78 @@
import { createApp, nextTick } from 'vue'
import MessagePopup from './index.vue'
const message = {
success(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'success', message: options, duration })
} else {
this.show({
type: 'success',
...options,
duration
})
}
}
},
error(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'error', message: options, duration })
} else {
this.show({
type: 'error',
...options,
duration
})
}
}
},
info(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'info', message: options, duration })
} else {
this.show({
type: 'error',
...options,
duration
})
}
}
},
warning(options, duration = 2000) {
if (process.client) {
if (typeof options === 'string') {
this.show({ type: 'warning', message: options, duration })
} else {
this.show({
type: 'warning',
...options,
duration
})
}
}
},
show(options) {
if (!process.client) return
const container = document.createElement('div')
document.body.appendChild(container)
const app = createApp(MessagePopup, {
onAfterLeave: () => {
app.unmount()
document.body.removeChild(container)
}
})
const instance = app.mount(container)
nextTick(() => {
instance.showMessage?.(options)
})
}
}
export { message }

View File

@ -0,0 +1,57 @@
<script setup>
/*
* 封装一个带标题栏的弹窗
* */
const props = defineProps({
show: {
type: Boolean,
default: false
},
title:''
})
const emit = defineEmits(['update:show'])
const close=()=>{
emit('update:show',false)
}
</script>
<template>
<van-popup
:show="show"
:transition-appear="true"
teleport="#__nuxt"
position="bottom"
@click-overlay="close"
:style="{ height: '74%' }"
v-bind="{...$attrs,...$props}"
:safe-area-inset-bottom="true"
>
<div class="flex flex-col h-full">
<!-- 标题栏 -->
<div class="flex items-center pl-16px pr-19px h-40px border-b-1px border-gray-300 shrink-0 relative w-full">
<slot v-if="$slots.title" name="title">
</slot>
<div v-else class="text-black text-16px text-center flex-grow-1">{{ title }}</div>
<van-icon
style="position: absolute"
class="right-19px"
size="20"
name="cross"
color="#939393"
@click="close"
/>
</div>
<!-- 内容区域 -->
<div class="flex-1 px-16px py-18px overflow-hidden relative">
<div class="h-full overflow-y-auto relative list-container">
<slot/>
</div>
</div>
</div>
</van-popup>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,109 @@
<script setup>
import { ref, computed } from 'vue'
import dayjs from 'dayjs'
const props = defineProps({
modelValue: {
type: [Date, String, Number],
default: () => new Date() //
},
label: {
type: String,
default: '日期'
},
required: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择日期'
},
disabled: {
type: Boolean,
default: false
},
minDate: {
type: Date,
default: () => new Date(1900, 0, 1)
},
maxDate: {
type: Date,
default: () => new Date(2100, 11, 31)
},
format: {
type: String,
default: 'YYYY-MM-DD'
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const show = ref(false)
//
const displayText = computed(() => {
return dayjs(props.modelValue).format(props.format)
})
//
const defaultValue = computed(() => {
const date = props.modelValue || new Date()
return [
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
]
})
//
const onConfirm = ({ selectedValues }) => {
show.value = false
const date = new Date(selectedValues[0], selectedValues[1] - 1, selectedValues[2])
emit('update:modelValue', date)
emit('change', date)
}
//
const onCancel = () => {
show.value = false
}
//
const reset = () => {
emit('update:modelValue', new Date())
}
defineExpose({
reset
})
</script>
<template>
<div>
<van-field
:model-value="displayText"
@click="show = true"
readonly
:disabled="disabled"
:required="required"
:placeholder="placeholder"
:label="label"
class="mb-10px"
is-link
/>
<van-popup
v-model:show="show"
position="bottom"
>
<van-date-picker
:min-date="minDate"
:max-date="maxDate"
:model-value="defaultValue"
@confirm="onConfirm"
@cancel="onCancel"
title="选择日期"
/>
</van-popup>
</div>
</template>

View File

@ -0,0 +1,88 @@
<script setup>
import { ref } from 'vue'
const props = defineProps({
value: {
type: [Number, String]
},
columns: {
type: Array,
default: () => []
},
label: {
type: String,
default: ''
},
required: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择'
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:value', 'change'])
const show = ref(false)
const onConfirm = (value) => {
show.value = false
emit('update:value', value.value)
emit('change', value)
}
const displayText = computed(() => {
const selected = props.columns.find(x => x.value === props.value)
return selected?.text || ''
})
const reset = () => {
emit('update:value', undefined)
}
defineExpose({
reset
})
</script>
<template>
<div>
<van-field
:model-value="displayText"
@click="show = true"
readonly
:disabled="disabled"
:required="required"
:placeholder="placeholder"
:label="label"
class="mb-10px"
is-link
/>
<van-popup
v-model:show="show"
destroy-on-close
position="bottom"
safe-area-inset-bottom
>
<van-picker
:columns="columns"
@confirm="onConfirm"
@cancel="show = false"
:default-index="columns.findIndex(x => x.value === value)"
title="请选择"
confirm-button-text="确定"
cancel-button-text="取消"
/>
</van-popup>
</div>
</template>

View File

@ -1,18 +0,0 @@
import { defineStore } from 'pinia'
const useCounter = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment,
}
}, {
persist: true,
})
export default useCounter

View File

@ -1,24 +0,0 @@
import type { RouteLocationNormalized, RouteRecordName } from 'vue-router'
import { defineStore } from 'pinia'
const useKeepalive = defineStore('keepalive', () => {
const routeCaches = ref<RouteRecordName[]>([])
const addRoute = (route: RouteLocationNormalized) => {
if (!route.name)
return
if (routeCaches.value.includes(route.name))
return
if (route?.meta?.keepalive)
routeCaches.value.push(route.name)
}
return {
routeCaches,
addRoute,
}
})
export default useKeepalive

View File

@ -0,0 +1,21 @@
export function useWebSocket() {
const { $ws } = useNuxtApp()
const messages = ref<any[]>([])
// 监听消息
const onMessage = (callback: (data: any) => void) => {
const handler = (event: CustomEvent) => callback(event.detail)
window.addEventListener('ws-message', handler as EventListener)
// 返回清理函数
return () => {
window.removeEventListener('ws-message', handler as EventListener)
}
}
return {
ws: $ws,
messages,
onMessage
}
}

3
app/config/index.js Normal file
View File

@ -0,0 +1,3 @@
export const useAppFooterRouteNames= ['index', 'profile']
export const useAppHeaderRouteNames= ['index', 'profile','login','collectCode-login','collectCode-mine']

View File

@ -1,6 +0,0 @@
import type { RouteRecordName } from 'vue-router'
/**
* Use the AppFooter routing whitelist
*/
export const useAppFooterRouteNames: RouteRecordName[] = ['index', 'profile']

0
app/config/live/index.js Normal file
View File

2
app/constants/index.js Normal file
View File

@ -0,0 +1,2 @@
export const appName = '豐和'
export const appDescription = '泰丰国际京都拍卖会'

View File

@ -1,2 +0,0 @@
export const appName = 'nuxt-vant-mobile'
export const appDescription = 'Nuxt H5 Starter Template'

View File

@ -7,7 +7,7 @@ By default, `default.vue` will be used unless an alternative is specified in the
```vue
<script setup lang="ts">
definePageMeta({
layout: 'home',
layout: 'goods',
})
</script>
```

View File

@ -1,11 +1,11 @@
<template>
<main class="flex flex-col min-h-svh">
<AppHeader class="h-[var(--van-nav-bar-height)]" />
<div class="flex-1 p-16 pb-[var(--van-nav-bar-height)]">
<div class="flex-1 flex flex-col">
<slot />
</div>
<AppFooter />
<AppFooter />
</main>
</template>
<script setup >
</script>

View File

@ -1,7 +0,0 @@
import type { RouteLocationNormalized } from 'vue-router'
import useKeepalive from '~/composables/keepalive'
export default defineNuxtRouteMiddleware((to: RouteLocationNormalized) => {
if (to.meta && to.meta.keepalive)
useKeepalive().addRoute(to)
})

View File

@ -0,0 +1,101 @@
<script setup>
import itemDetail from '@/components/itemDetail/index.vue'
import {userArtwork} from "~/api/goods/index.js";
const route = useRoute()
const detail = ref({})
const uuid = route.query.uuid
const initData = async () => {
const res = await userArtwork({uuid})
if (res.status === 0) {
detail.value = res.data
}
}
const position = ref({x: window?.innerWidth - 120 || 0, y: 240}) //
const startPosition = ref({x: 0, y: 0})
const isDragging = ref(false)
const startDrag = (e) => {
isDragging.value = true
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const clientY = e.touches ? e.touches[0].clientY : e.clientY
startPosition.value = {
x: clientX - position.value.x,
y: clientY - position.value.y
}
}
const onDrag = (e) => {
if (isDragging.value) {
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const clientY = e.touches ? e.touches[0].clientY : e.clientY
//
const maxX = window.innerWidth - 108 //
const maxY = window.innerHeight - 137 //
//
const x = Math.min(Math.max(0, clientX - startPosition.value.x), maxX)
const y = Math.min(Math.max(0, clientY - startPosition.value.y), maxY)
position.value = {x, y}
}
}
const stopDrag = () => {
isDragging.value = false
}
onMounted(() => {
//
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
//
document.addEventListener('touchmove', onDrag)
document.addEventListener('touchend', stopDrag)
})
onUnmounted(() => {
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', stopDrag)
})
initData()
</script>
<template>
<div class="relative h-screen-nav flex flex-col">
<itemDetail class="grow-1" :detail-info="detail.auctionArtworkInfo"/>
<div v-if="[1,3,4].includes(detail.status)" class="h-81px bg-#fff flex justify-center pt-7px">
<van-button class="w-213px !h-38px" type="primary">
<span class="text-#fff text-14px">去支付 RMB10,000</span>
</van-button>
</div>
<div
class="w-108px h-137px absolute cursor-move"
:style="{
left: position.x + 'px',
top: position.y + 'px'
}"
@mousedown="startDrag"
@touchstart.prevent="startDrag"
>
<img src="@/static/images/zd5530@2x.png" class="w-full h-full" alt="">
<div
class="flex flex-col items-center absolute bottom-25px text-14px text-#B58047 left-1/2 transform translate-x--1/2 whitespace-nowrap">
<div>恭喜您</div>
<div>竞拍成功</div>
</div>
</div>
</div>
</template>
<style scoped>
.cursor-move {
touch-action: none;
user-select: none;
}
</style>

View File

@ -0,0 +1,208 @@
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'
import { senCode, userLogin } from "@/api/auth/index.js";
import { codeAuthStore } from "@/stores-collect-code/auth/index.js";
import { message } from '@/components/x-message/useMessage.js'
// ... ...
import FingerprintJS from '@fingerprintjs/fingerprintjs'
import {checkPhone, mobileLogin, userSend} from "@/api-collect-code/auth/index.js";
const { userInfo, token,fingerprint } = codeAuthStore()
const router = useRouter();
const route = useRoute();
const { locale } = useI18n()
const loadingRef = ref({
loading1: false,
loading2: false,
})
const password = ref('')
const loginType = ref(0)
const interval = ref(null)
const startCountdown = () => {
if (interval.value) {
clearInterval(interval.value);
}
countdown.value = 60;
interval.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(interval.value);
}
}, 1000);
}
const countdown = ref(0);
const phoneNum = ref('17630920520')
const code = ref('123789')
const pane = ref(0)
const showKeyboard = ref(false);
const getFingerprint = async () => {
const fp = await FingerprintJS.load()
const result = await fp.get()
return result.visitorId //
}
//
const checkFingerprint = async () => {
const tempFingerprint = await getFingerprint()
if (fingerprint && fingerprint === tempFingerprint) {
await router.push('/collectCode/mine')
}
}
checkFingerprint()
const vanSwipeRef = ref(null)
const getCode = async () => {
loadingRef.value.loading1 = true
const res = await checkPhone({
tel: phoneNum.value,
})
loadingRef.value.loading1 = false
if (res.status === 0){
const res=await userSend({telNum:phoneNum.value,zone:'+86'})
if (res.status === 0){
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value = true
}
}
/* loadingRef.value.loading1 = false
if (res.status === 0) {
}
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value = true
startCountdown();*/
/* pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value=true
startCountdown();*/
}
const changeToPwd = async () => {
loginType.value = loginType.value === 0 ? 1 : 0
}
const goBack = () => {
code.value = ''
pane.value = 0
vanSwipeRef.value?.swipeTo(pane.value)
}
const goLogin = async () => {
loadingRef.value.loading2 = true
const res = await mobileLogin({
TelNum: phoneNum.value,
Password:loginType.value===1?password.value:'',
Code: loginType.value===0?code.value:''
})
if (res.status === 0) {
userInfo.value = res.data.accountInfo
token.value = res.data.token
fingerprint.value = await getFingerprint()
await router.push('/collectCode/mine');
}
loadingRef.value.loading2 = false
}
</script>
<template>
<div class="h-[100vh] w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-cover px-[31px] pt-[86px]">
<div class="w-full flex justify-center mb-[100px] flex-col items-center">
<img class="h-[105px] w-[189px]" src="@/static/images/ghfggff.png" alt="">
<img class="h-[29px] w-[108px]" src="@/static/images/qrcodetext.png" alt="">
</div>
<van-swipe ref="vanSwipeRef" :show-indicators="false" :touchable="false" :lazy-render="true" :loop="false">
<van-swipe-item>
<div v-show="pane === 0">
<div class="">
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="phoneNum" clearable placeholder="请输入手机号">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
手机号
</div>
</template>
</van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]" v-show="loginType === 1">
<van-field v-model="password" clearable placeholder="请输入密码">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
密码
</div>
</template>
</van-field>
</div>
<div class="flex justify-end mt-[10px]" @click="changeToPwd">
<div class="text-[14px] text-[#2B53AC]">
{{ loginType === 0 ? '密码登录' : '验证码登录' }}
</div>
</div>
<div />
</div>
<div class="mt-[55px]">
<div v-if="loginType === 0">
<van-button :loading="loadingRef.loading1" v-if="phoneNum" loading-text="获取验证码"
type="primary" block style="height: 48px" @click="getCode">获取验证码</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">获取验证码</van-button>
</div>
<div v-else>
<van-button type="primary" v-if="password" block :loading="loadingRef.loading2" loading-text="登录"
style="height: 48px;margin-top:10px" @click="goLogin">登录</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">登录</van-button>
</div>
</div>
</div>
</van-swipe-item>
<van-swipe-item>
<div v-show="pane === 1">
<div class="flex mb-[16px]">
<div class="text-[16px] text-[#BDBDBD] mr-[10px]">{{ $t('login.hasSendTo') }}</div>
<div class="text-[16px] text-[#000]">+86 {{ phoneNum }}</div>
</div>
<van-password-input :value="code" :gutter="10" :mask="false" focused @focus="showKeyboard = true" />
<div :class="`${countdown > 0 ? 'text-#BDBDBD' : 'text-#2B53AC'} text-14px`">
{{ $t('login.reSend') }}<span v-if="countdown > 0">({{ countdown }})</span>
</div>
<div class="mt-[17px]">
<van-button v-if="code.length === 6" type="primary" block :loading="loadingRef.loading2"
:loading-text="$t('login.login')" style="height: 48px" @click="goLogin">{{
$t('login.login')
}}</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{
$t('login.login')
}}</van-button>
</div>
<div class="mt-[17px]">
<van-button type="primary" @click="goBack" block style="height: 48px">{{ $t('login.back')
}}</van-button>
</div>
</div>
</van-swipe-item>
</van-swipe>
<van-number-keyboard v-model="code" :show="showKeyboard" @blur="showKeyboard = false" />
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field) {
padding-left: 0;
}
:deep(.van-password-input) {
margin: 0;
}
:deep(.van-password-input__item) {
border: 1px solid #E5E5E5;
width: 41px;
height: 41px;
}
</style>

View File

@ -0,0 +1,72 @@
<script setup>
import XImage from "@/components/x-image/index.vue";
import {useRuntimeConfig} from "#app";
import QRCode from 'qrcode'
import { showImagePreview } from 'vant';
import {offlineQrcodeDelete} from "~/api-collect-code/goods/index.js";
const statusLabel=[
{label:'已付款',value:2,color:'#18A058'}, {label:'未付款',value:1,color:'#CF3050'}, {label:'已部分付款',value:4,color:'#F09F1F'}
]
const props = defineProps({
data: {
type: Object,
default: () => {
return {};
},
},
});
const itemLabel=(data)=>{
return statusLabel.find(x=>x.value===data.payStatus)
}
const config = useRuntimeConfig()
const getQRBase64 = async () => {
try {
return await QRCode.toDataURL(`${config.public.NUXT_PUBLIC_API_BASE}/collectCode/payment`, {
width: 200,
margin: 4,
errorCorrectionLevel: 'H'
})
} catch (err) {
console.error('生成二维码失败:', err)
return null
}
}
const openQrCode=async ()=>{
const base64=await getQRBase64()
showImagePreview([base64])
}
</script>
<template>
<div class="flex flex-col h-120px bg-#F7F7F7 rounded-4px px-13px">
<div class="flex h-40px border-b border-b-#F0F0F0 items-center justify-between px-8px">
<div class="text-14px text-#000">¥ {{data.paidPrice}}/{{data.price}}</div>
<div :class="`text-12px text-${itemLabel(data).color}`">{{itemLabel(data).label}}</div>
</div>
<div class="flex flex-grow-1 px-8px py-11px">
<div class="mr-8px">
<XImage class="w-57px h-56px rounded-4px" :src="data.hdPic"></XImage>
</div>
<div class="text-12px text-#1E1E1E">
<div>Lot{{ data.lotNo }}</div>
<div>创建人{{ data.userName }}</div>
<div>创建时间{{data.createdAt}}</div>
</div>
<div class="flex flex-col justify-end ml-auto ">
<div class="flex w-55px h-26px bg-#2B53AC rounded-4px justify-center items-center">
<div @click="openQrCode" class="text-12px text-#fff line-height-none mt-0.5px mr-5px">查看</div>
<div >
<img class="w-12px h-12px" src="@/static/images/icon-design-42@3x.png" alt="">
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,210 @@
<script setup>
import { userArtworks } from "@/api/goods/index.js";
import { codeAuthStore } from "@/stores-collect-code/auth/index.js";
import { showImagePreview } from 'vant';
import XImage from '@/components/x-image/index.vue'
import {useRouter} from "#vue-router";
import {goodStore} from "~/stores-collect-code/goods/index.js";
import {ref} from "vue";
import {offlineQrcodeCreate, offlineQrcodeDelete, offlineQrcodeList} from "~/api-collect-code/goods/index.js";
import codeCard from './components/codeCard/index.vue'
import {message} from "~/components/x-message/useMessage.js";
definePageMeta({
layout: 'default',
i18n: 'menu.profile',
})
const router = useRouter();
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
const { userInfo, } = codeAuthStore()
const {getOfflineQrcodeList,itemList, loading: storeLoading,pageRef}= goodStore()
const initData = async () => {
onRefresh()
}
const show=ref(false)
const close=()=>{
console.log('show',show.value)
show.value=false
}
const logOut=()=>{
localStorage.clear()
router.push('/collectCode/login')
}
const createForm=ref({
lotNo:'',
price:'',
})
const confirm=async ()=>{
if (!createForm.value.price){
message.warning('请输入金额')
return false
}else if (!createForm.value.lotNo){
message.warning('请输入Lot号')
return false
}
const res=await offlineQrcodeCreate({...createForm.value,price:String(createForm.value.price)})
if (res.status===0){
show.value=false
}
}
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getOfflineQrcodeList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
const loadMore = async () => {
pageRef.value.page++
const { finished } = await getOfflineQrcodeList()
localState.value.finished = finished
}
const abnormal=ref(false)
const abnormalRow=ref({})
const inputLotNo=async (data)=>{
const res=await offlineQrcodeList({
lotNo:createForm.value.lotNo
})
if (res.status===0){
if (res.data.Data?.length>0){
abnormal.value=true
abnormalRow.value=res.data.Data?.[0]
}
}
}
const deleteData=async (qrUid)=>{
const res=await offlineQrcodeDelete({
qrUid:qrUid
})
if (res.status===0){
getOfflineQrcodeList()
message.success('删除成功')
}
}
initData()
</script>
<template>
<div class="w-[100vw] bg-[url('@/static/images/3532@2x.png')] h-screen-nav bg-cover pt-43px flex-grow-1 flex flex-col">
<div class="flex items-center px-16px mb-43px">
<div class="mr-23px">
<img class="w-57px h-57px" src="@/static/images/5514@2x.png" alt="">
</div>
<div class="flex flex-col">
<div class="text-18px text-#181818">{{ userInfo.realName }}</div>
<div class="text-#575757 text-14px">{{ userInfo.telNum }}</div>
</div>
<div class="grow-1 flex justify-end" @click="logOut">
<img class="w-40px h-40px" src="@/static/images/logout.png" alt="">
</div>
</div>
<div class="border-b-1px border-b-#D3D3D3 px-16px flex">
<div class="text-#000 text-16px border-b-3 border-b-#2B53AC h-36px">线下付款二维码 </div>
</div>
<div class="grow-1 flex flex-col overflow-hidden py-15px">
<div class="overflow-auto">
<van-pull-refresh v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh">
<van-list v-model:loading="storeLoading"
:finished="localState.finished"
finished-text="没有更多了"
@load="loadMore" class="px-14px">
<template v-for="(item,index) of itemList" :key="item.qrUid">
<template v-if="item.payStatus===1">
<van-swipe-cell class="mb-14px" >
<codeCard :data="item"></codeCard>
<template #right>
<div class="w-65px h-full bg-#CF3050 flex items-center justify-center" @click="deleteData(item.qrUid)">
<img class="w-22px h-24px" src="@/static/images/delete3@.png" alt="">
</div>
</template>
</van-swipe-cell>
</template>
<template v-else>
<div class="mb-14px">
<codeCard :data="item"></codeCard>
</div>
</template>
</template>
</van-list>
</van-pull-refresh>
</div>
</div>
<div class="h-81px w-full flex justify-center shrink-0 pt-10px">
<div class="w-213px h-38px bg-#2B53AC text-#fff flex justify-center items-center text-14px rounded-4px" @click="show=true">
新增
</div>
</div>
<van-dialog v-model:show="show">
<div class="pt-18px pb-24px px-24px">
<div class="text-16px text-#000 font-bold text-center mb-26px">新增收款二维码</div>
<div class="">
<div class="flex mb-6px items-center">
<div class="w-58px">
<div class="text-#1A1A1A text-16px">金额</div>
<div class="text-#939393 text-12px">RMB</div>
</div>
<div>
<input v-model="createForm.price" type="number"
class="w-214px h-48px bg-#F3F3F3 rounded-4px px-11px text-16px" placeholder="请输入金额">
</div>
</div>
<div class="flex items-center">
<div class="w-58px">
<div class="text-#1A1A1A text-16px">Lot号</div>
</div>
<div>
<input type="number" v-model="createForm.lotNo" @input="inputLotNo" class="w-214px h-48px bg-#F3F3F3 rounded-4px px-11px text-16px" placeholder="请输入拍品序号">
</div>
</div>
</div>
<div class="flex flex-col items-center" v-if="abnormal">
<div class="text-#CF3050 text-12px mb-8px mt-4px">*该拍品号当前已存在收款二维码确定要创建吗</div>
<div>
<XImage class="w-116px h-116px rounded-4px mb-9px" :src="abnormalRow.hdPic"></XImage>
<div class="text-12px text-#575757 flex flex-col items-center">
<div>日出而作日落而息</div>
<div>张天赐</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="border-t flex">
<van-button class="w-50% h-56px" style="border: none;border-radius: 0;border-right: 1.5px solid #E7E7E7" @click="show=false">
<span class="text-#000 text-16px text-center">取消</span>
</van-button>
<van-button class="w-50% h-56px !rounded-0" style="border: none;border-radius: 0" @click="confirm">
<span class="text-#000 text-16px text-center text-#2B53AC">确定</span>
</van-button>
</div>
</template>
</van-dialog>
</div>
</template>
<style scoped lang="scss">
:deep(.van-hairline--top.van-dialog__footer){
&>.van-button{
border-top: 1px solid #E7E7E7;
&.van-dialog__cancel{
border-right: 1px solid #E7E7E7;
}
}
}
</style>

View File

@ -0,0 +1,43 @@
<script setup>
const payStatus=ref(0)
const changePayStatus=()=>{
payStatus.value=payStatus.value===0?1:0
}
const validateInput = (e) => {
const value = e.target.value
const char = String.fromCharCode(e.charCode)
if (!/[\d.]/.test(char)) {
e.preventDefault()
return
}
if (char === '.' && (value.includes('.') || !value)) {
e.preventDefault()
return
}
if (value.includes('.') && value.split('.')[1]?.length >= 2) {
e.preventDefault()
return
}
}
</script>
<template>
<div class="w-[100vw] h-screen-nav bg-[url('@/static/images/3532@2x.png')] bg-cover flex-grow-1 flex flex-col items-center pt-183px">
<div class="mb-30px">
<img class="w-126px h-126px" src="@/static/images/dddf34@2x.png" alt="">
</div>
<div class="text-#1A1A1A text-16px mb-25px font-bold">{{payStatus===0?'支付全部':'支付部分'}}</div>
<div class="text-#999999 text-16px mb-24px font-bold" v-if="payStatus===0">RMB 5000</div>
<div class="mb-12px">
<input class="w-272px h-48px bg-#F3F3F3 px-11px text-16px" type="text" placeholder="最多RMB5,000" @keydown="validateInput">
</div>
<div class="text-#2B53AC text-14px" @click="changePayStatus">{{payStatus===1?'支付全部':'支付部分'}}</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,18 @@
<script setup>
const image = ref('');
import { showToast } from 'vant';
const onSubmit = (data) => {
image.value = data.image;
};
const onClear = () => showToast('clear');
</script>
<template>
<van-signature @submit="onSubmit" @clear="onClear" />
<van-image v-if="image" :src="image" />
</template>
<style scoped>
</style>

View File

@ -0,0 +1,58 @@
<script setup>
import {useI18n} from "vue-i18n";
import XVanSelect from '@/components/x-van-select/index.vue'
import XVanDate from '@/components/x-van-date/index.vue'
definePageMeta({
layout: 'default',
i18n: 'menu.profile',
})
const {t} = useI18n()
const showPicker = ref(false)
const showPicker1 = ref(false)
const onConfirm = () => {
}
const columns = ref([
{text: t('realAuth.male'), value: 1},
{text: t('realAuth.female'), value: 2},
])
</script>
<template>
<div
class="w-[100vw] bg-[url('@/static/images/asdfsdd.png')] h-screen-nav bg-cover pt-77px flex-grow-1 flex flex-col ">
<div class="text-16px text-#191919 font-bold mb-40px px-34px">
请填写个人相关信息
</div>
<div class="grow-1 px-34px">
<van-field type="tel" :label-width="161" label="文本" class="mb-10px" placeholder="请输入手机号">
<template #label>
<div class="flex">
<div class="mr-41px whitespace-nowrap">手机号</div>
<div>
<span class="mr-13px">+ 86</span>
<van-icon name="arrow-down" class="text-#777777"/>
</div>
</div>
</template>
</van-field>
<van-field label="姓名" class="mb-10px" placeholder="请输入姓名"/>
<x-van-select label="性别" :columns="columns"/>
<x-van-date label="出生日期"/>
<van-field label="家庭住址" class="mb-10px" placeholder="请输入家庭住址"/>
<van-field label="所属银行" class="mb-10px" placeholder="请输入所属银行"/>
<van-field label="银行卡号码" class="mb-10px" placeholder="请输入银行卡号码"/>
</div>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">下一步</van-button>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field){
padding-left: 0;
}
</style>

View File

@ -0,0 +1,40 @@
<script setup>
const activeNames = ref(['1']);
</script>
<template>
<div class="bg-#EBEBEB h-screen-nav flex flex-col">
<div class="h-50px text-14px text-#191919 bg-#fff flex items-center px-21px mb-6px">支付前需同意以下内容并签字</div>
<van-collapse v-model="activeNames" class="grow-1">
<van-collapse-item name="1" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="2" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="3" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
</van-collapse>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">同意并签字</van-button>
</div>
</div>
</template>
<style scoped>
:deep(.van-cell__right-icon){
color: #ACACAC;
font-size: 12px;
}
</style>

View File

@ -1,35 +0,0 @@
<script setup lang="ts">
import useCounter from '~/composables/counter'
definePageMeta({
title: '🍍 持久化 Pinia 状态',
i18n: 'menu.persistPiniaState',
})
const counter = useCounter()
function add() {
counter.increment()
}
</script>
<template>
<div>
<h1 class="text-6xl color-pink font-semibold">
Hello, Pinia!
</h1>
<p class="mt-10 text-gray-700 dark:text-white">
{{ $t('counter_page.label') }}
</p>
<p class="mt-10">
{{ $t('counter_page.label_num') }}:
<strong class="text-green-500"> {{ counter.count }} </strong>
</p>
<button class="mt-10 btn" @click="add">
{{ $t('counter_page.btn_add') }}
</button>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,186 @@
<script setup>
import {ref, computed, watch} from 'vue';
import pinyin from 'pinyin';
import countryCode from './data/index.js';
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
definePageMeta({
title: '国家地区',
i18n: 'countryRegion.title',
})
const router = useRouter()
console.log('router',router)
const { t, locale } = useI18n()
const value = ref('');
const alphabet = [
'#',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
];
//
const frequentCountryCodes = ['CN', 'TW', 'JP', 'US'];
function groupByPinyinInitial(data) {
const grouped = {};
//
grouped['#'] = [];
data.forEach(country => {
if (frequentCountryCodes.includes(country.code)) {
const countryName = locale.value === 'zh-CN' ? country.cn :
locale.value === 'zh-TW' ? country.tw :
locale.value === 'ja-JP' ? country.ja :
country.en;
grouped['#'].push({
...country,
displayName: countryName
});
}
});
//
data.forEach(country => {
if (!frequentCountryCodes.includes(country.code)) {
const countryName = locale.value === 'zh-CN' ? country.cn :
locale.value === 'zh-TW' ? country.tw :
locale.value === 'ja-JP' ? country.ja :
country.en;
const initial = locale.value === 'ja-JP' ? '' :
locale.value === 'zh-CN' || locale.value === 'zh-TW' ?
pinyin(countryName, {style: pinyin.STYLE_FIRST_LETTER})[0][0].toUpperCase() :
countryName.charAt(0).toUpperCase();
if (!grouped[initial]) {
grouped[initial] = [];
}
grouped[initial].push({
...country,
displayName: countryName
});
}
});
if (locale.value === 'ja-JP') {
//
grouped[''] = grouped[''].sort((a, b) => a.displayName.localeCompare(b.displayName, 'ja-JP'));
}
return grouped;
}
const groupedCountries = ref([])
const initData = () => {
groupedCountries.value = groupByPinyinInitial(countryCode);
}
const searchCountry = computed(() => {
if (!value.value) {
return groupedCountries.value;
}
return Object.keys(groupedCountries.value).reduce((filtered, initial) => {
const countries = groupedCountries.value[initial].filter(country =>
country.displayName.toLowerCase().includes(value.value.toLowerCase())
);
if (countries.length > 0) {
filtered[initial] = countries;
}
return filtered;
}, {});
});
const showIndexBar = computed(() => locale.value !== 'ja-JP')
const route = useRoute()
const handleCountrySelect = (country) => {
router.replace({
path: window.history.state.back,
query: {
zone: country.zone,
countryName: country.displayName
}
})
}
initData()
//
watch(locale, () => {
initData()
})
</script>
<template>
<div>
<van-sticky>
<van-search v-model="value" :placeholder="t('countryRegion.searchPlaceholder')"/>
</van-sticky>
<van-index-bar
v-if="showIndexBar"
sticky
:sticky-offset-top="55"
:index-list="alphabet"
>
<!-- 常用国家分类 -->
<van-index-anchor index="#">{{ t('countryRegion.frequentCountry') }}</van-index-anchor>
<van-cell
v-for="country in searchCountry['#']"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
<!-- 其他国家按字母分类 -->
<template v-for="(countries, index) in searchCountry" :key="index">
<template v-if="index !== '#'">
<van-index-anchor
:index="index"
></van-index-anchor>
<van-cell
v-for="country in countries"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
</template>
</template>
</van-index-bar>
<div v-else>
<div class="mb-4">
<div class="px-4 py-2 text-gray-600">{{ t('countryRegion.frequentCountry') }}</div>
<van-cell
v-for="country in searchCountry['#']"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
</div>
<van-cell
v-for="country in Object.values(searchCountry).flat().filter(c => !frequentCountryCodes.includes(c.code))"
:key="country.code"
:title="country.displayName"
@click="handleCountrySelect(country)"
clickable
>
<div class="pr-[25px]"> +{{ country.zone }}</div>
</van-cell>
</div>
<van-back-top v-if="showIndexBar" right="15vw" bottom="10vh"/>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,19 @@
<script setup>
import {goodStore} from "@/stores/goods/index.js";
import xImage from '@/components/x-image/index.vue'
const {
auctionDetail
} = goodStore()
</script>
<template>
<div class="px-16px pt-14px">
<div class="text-#575757 text-14px" v-html="auctionDetail.info">
</div>
<xImage :src="auctionDetail.image" class="w-343px"></xImage>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,28 @@
<script setup>
import xPopup from '@/components/x-popup/index.vue'
import ItemDetail from "@/components/itemDetail/index.vue";
import {goodStore} from "@/stores/goods/index.js";
const {
artWorkDetail
} = goodStore()
const props = defineProps({
show: {
type: Boolean,
default: false
},
detailInfo: {
type: Object,
default: null
}
})
const emit = defineEmits(['update:show'])
const handleClose = () => {
emit('update:show', false)
}
</script>
<template>
<xPopup :show="show" title="拍品详情" @update:show="handleClose">
<ItemDetail :detailInfo="detailInfo" />
</xPopup>
</template>

View File

@ -0,0 +1,116 @@
<script setup>
import { ref, computed } from 'vue'
import { useRect } from "@vant/use"
import { goodStore } from "@/stores/goods"
import DetailPopup from '../DetailPopup/index.vue'
import MasonryWall from '@yeger/vue-masonry-wall'
const {
itemList,
pageRef,
auctionDetail,
liveRef,
artWorkDetail,
currentItem,
loading: storeLoading,
getArtworkList,
getArtworkDetail
} = goodStore()
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
//
const loadMore = async () => {
pageRef.value.page++
const { finished } = await getArtworkList()
localState.value.finished = finished
}
//
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getArtworkList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
//
const openShow = async (item) => {
localState.value.showDetail = true
currentItem.value = item
getArtworkDetail(item.uuid)
}
</script>
<template>
<div class="px-[16px] pt-[16px]">
<van-pull-refresh
v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh"
>
<template #success>
<van-icon name="success" /> <span>刷新成功</span>
</template>
<van-list
v-model:loading="storeLoading"
:finished="localState.finished"
finished-text="没有更多了"
@load="loadMore"
>
<div class="w-full flex gap-[16px]">
<masonry-wall :items="itemList" :ssr-columns="2" :maxColumns="2" :minColumns="2" :gap="5">
<template #default="{ item, index }">
<div
@click="openShow(item)"
class="w-full"
>
<div class="relative w-full">
<img
:src="item.artwork?.hdPic"
class="w-full object-cover rounded-4px"
loading="lazy"
/>
<div
class="absolute rounded-2px overflow-hidden line-height-12px left-[8px] top-[8px] h-[17px] w-[45px] flex items-center justify-center bg-[#2b53ac] text-[12px] text-[#fff]"
>
LOT{{ item.index }}
</div>
</div>
<div class="pt-[8px]">
<div class="text-[14px] text-[#000000] leading-[20px]">
{{ item.name }}
</div>
<div class="mt-[4px] text-[12px] text-[#575757]">
起拍价{{ item?.startPrice??0 }}
</div>
<div
v-if="item.soldPrice"
class="mt-[4px] text-[12px] text-[#b58047]"
>
成交价{{ item?.startPrice??0 }}
</div>
</div>
</div>
</template>
</masonry-wall>
</div>
</van-list>
</van-pull-refresh>
<DetailPopup v-model:show="localState.showDetail" :detailInfo="artWorkDetail"></DetailPopup>
</div>
</template>
<style scoped>
.content {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
</style>

77
app/pages/home/index.vue Normal file
View File

@ -0,0 +1,77 @@
<script setup>
import liveRoom from '@/pages/liveRoom/index.client.vue';
import {goodStore} from "@/stores/goods/index.js";
import ItemList from './components/ItemList/index.vue'
import Cescribe from './components/Cescribe/index.vue'
import {message} from '@/components/x-message/useMessage.js'
import {liveStore} from "~/stores/live/index.js";
const {getAuctionDetail,auctionDetail} = goodStore();
const {fullLive}= liveStore()
const changeLive = () => {
fullLive.value = true;
};
if (!auctionDetail.value.uuid){
await getAuctionDetail()
}
</script>
<template>
<div class="flex-grow-1">
<client-only>
<liveRoom @click="changeLive" :class="['changeLive', fullLive ? 'expanded' : 'collapsed']"/>
</client-only>
<div v-if="!fullLive" class="bg-#fff">
<van-tabs sticky animated>
<van-tab title="拍品列表">
<ItemList></ItemList>
</van-tab>
<van-tab title="拍卖说明">
<Cescribe></Cescribe>
</van-tab>
</van-tabs>
<van-back-top right="15vw" bottom="10vh"/>
</div>
</div>
</template>
<style scoped lang="scss">
.ellipsis {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.van-swipe__indicator) {
width: 8px;
height: 8px;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 1s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
:deep(.van-swipe__indicator:not(.van-swipe__indicator--active)) {
background: rgba(0, 0, 0, 0.8);
}
.changeLive {
width: 100%;
overflow: hidden;
transition: height 0.4s ease, transform 0.4s ease;
}
.changeLive.collapsed {
height: 188px;
}
.changeLive.expanded {
position: absolute;
z-index: 10;
height: calc(100vh - var(--van-nav-bar-height));
}
</style>

View File

@ -1,95 +1,11 @@
<script setup lang="ts">
import type { LocaleObject } from '@nuxtjs/i18n'
import type { PickerColumn } from 'vant'
import type { ComputedRef } from 'vue'
import { Locale } from 'vant'
<script setup>
import Home from './home/index.vue'
definePageMeta({
layout: 'default',
title: '主页',
i18n: 'menu.home',
})
const color = useColorMode()
useHead({
meta: [{
id: 'theme-color',
name: 'theme-color',
content: () => color.value === 'dark' ? '#222222' : '#ffffff',
}],
})
const checked = computed({
get: () => color.value === 'dark',
set: (val: boolean) => {
color.preference = val ? 'dark' : 'light'
},
})
const { setLocale, t } = useI18n()
const i18n = useNuxtApp().$i18n
const showLanguagePicker = ref(false)
const languageValues = ref<string[]>([i18n.locale.value])
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
const menus = computed(() => [
{ title: t('menu.unocssExample'), route: 'unocss' },
{ title: t('menu.keepAlive'), route: 'keepalive' },
{ title: t('menu.persistPiniaState'), route: 'counter' },
{ title: t('menu.fetch'), route: 'prose' },
{ title: t('menu.404Demo'), route: 'unknown' },
])
function onLanguageConfirm(event: { selectedOptions: PickerColumn }) {
const lang = event.selectedOptions[0]?.code
setLocale(lang)
Locale.use(lang)
localStorage.setItem('lang', lang)
showLanguagePicker.value = false
}
</script>
<template>
<div>
<VanCellGroup inset>
<VanCell :title="$t('menu.darkMode')" center>
<template #right-icon>
<ClientOnly>
<VanSwitch
v-model="checked"
size="20px"
aria-label="on/off Dark Mode"
/>
</ClientOnly>
</template>
</VanCell>
<VanCell
:title="$t('menu.language')"
:value="locales.find(i => i.code === i18n.locale.value)?.name"
is-link
@click="showLanguagePicker = true"
/>
<template v-for="item in menus" :key="item.route">
<VanCell :title="item.title" :to="item.route" is-link />
</template>
</VanCellGroup>
<van-popup v-model:show="showLanguagePicker" position="bottom">
<van-picker
v-model="languageValues"
:columns="locales"
:columns-field-names="{ text: 'name', value: 'code' }"
@confirm="onLanguageConfirm"
@cancel="showLanguagePicker = false"
/>
</van-popup>
</div>
</template>
<Home/>
</template>

View File

@ -1,21 +1,17 @@
<script setup lang="ts">
defineOptions({
name: 'Keepalive',
})
definePageMeta({
name: 'Keepalive',
keepalive: true,
title: '🧡 KeepAlive',
i18n: 'menu.keepAlive',
})
const value = ref(1)
</script>
<template>
<div>
<p> {{ $t('keepalive_page.label') }} </p>
<van-stepper v-model="value" class="mt-10" />
<div class="h-[100vh] w-[100vw]">
<SignaturePad v-model="signature" @change="handleSignatureChange"/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import SignaturePad from '@/components/SignaturePad.vue'
const signature = ref('')
const handleSignatureChange = (imageData) => {
// imageData base64
console.log('签名已更新:', imageData)
}
</script>

View File

@ -0,0 +1,62 @@
<script setup>
import {liveStore} from "@/stores/live/index.js";
import {authStore} from "~/stores/auth/index.js";
const {auctionData} = liveStore()
const {userInfo}= authStore()
const headList=[
{
label:'领先',
color:'#D03050',
value:'head'
},
{
label:'出局',
color:'#939393',
value:'out'
},
{
label:'成交',
color:'#34B633',
value:'success'
}
]
const headItem=(statusCode)=>{
return headList.find(x=>x.value===statusCode)
}
</script>
<template>
<div
id="list-container"
class="w-344px h-86px overflow-y-auto bg-#fff rounded-4px text-14px text-#939393 pt-7px pb-7px px-11px flex flex-col justify-between"
>
<transition-group name="list" tag="div">
<template v-if="auctionData.wsType==='stopArtwork'">
<div class="text-#939393 text-14px">即将开始下一个拍品</div>
</template>
<template v-else-if="auctionData.auctionPriceList?.buys?.length>0">
<div v-for="(item, index) in auctionData.auctionPriceList?.buys" :key="index" class="flex flex-shrink-0 h-25px">
<div class="text-start shrink-0 w-60px" :style="`color: ${headItem(item.statusCode).color}`" >{{ headItem(item.statusCode).label }}</div>
<div class="text-start shrink-0 w-80px">{{ item.auctionType==='local'?'现场竞价':'网络竞价' }}</div>
<div class="text-start shrink-0 w-80px">{{ item.createdAt }}</div>
<div class="text-start shrink-0 w-80px">{{item.baseCurrency}}{{ item.baseMoney }}</div>
<div class="text-start text-#2B53AC shrink-0 w-20px">{{ item.userId===userInfo.ID?'我':'' }}</div>
</div>
</template>
<template v-if="auctionData.wsType==='newArtwork'">
<div class="text-#939393 text-14px">开始拍卖</div>
</template>
</transition-group>
</div>
</template>
<style scoped>
.list-enter-active, .list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>

View File

@ -0,0 +1,92 @@
<script setup>
import {liveStore} from "~/stores/live/index.js";
import { showMinWindow, hideMinWindow } from '@/components/liveMinWindow/createMinWindow.js'
const {lastSnapshot,fullLive} = liveStore()
const props = defineProps({
show: {
type: Boolean,
default: false
},
price: {
type: Number,
default: 0
}
})
const router = useRouter()
const emit = defineEmits(['update:show'])
const payStatus=ref(0)
const changePayStatus=()=>{
payStatus.value=payStatus.value===0?1:0
}
const close=()=>{
emit('update:show',false)
}
const confirm=()=>{
router.push('/signature/protocol')
handleCapture()
emit('update:show',false)
}
const captureVideoFrame = () => {
try {
const video = document.querySelector('#J_prismPlayer video')
if (!video) {
console.error('未找到视频元素')
return null
}
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
return canvas.toDataURL('image/jpeg', 0.9)
} catch (error) {
console.error('获取视频截图失败:', error)
return null
}
}
const handleCapture = () => {
const imageUrl = captureVideoFrame()
if (imageUrl) {
lastSnapshot.value=imageUrl
showMinWindow(lastSnapshot.value,{
onClick:()=>{
router.replace('/')
fullLive.value=true
console.log('执行')
}
})
}
}
</script>
<template>
<div>
<van-dialog :show="show" show-cancel-button @cancel="close" @confirm="confirm">
<div class="flex flex-col pt-18px pb-13px justify-between items-center h-144px">
<template v-if="payStatus===0">
<div class="text-#000 text-16px font-600 ">支付全部</div>
<div class="text-#000 text-16px ">RMB 5,000</div>
</template>
<template v-if="payStatus===1">
<div class="text-#000 text-16px font-600 ">支付部分</div>
<input class="w-272px h-48px bg-#F3F3F3 px-11px text-16px" type="text" placeholder="最多RMB5,000">
</template>
<div class="text-#2B53AC text-14px" @click="changePayStatus">{{payStatus===0 ? '支付部分' : '支付全部'}}</div>
</div>
</van-dialog>
</div>
</template>
<style scoped lang="scss">
:deep(.van-hairline--top.van-dialog__footer){
&>.van-button{
border-top: 1px solid #E7E7E7;
&.van-dialog__cancel{
border-right: 1px solid #E7E7E7;
}
}
}
</style>

View File

@ -0,0 +1,42 @@
<script setup>
import successImg from '@/static/images/zu5554@2x.png'
import errorImg from '@/static/images/zu5561@2x.png'
const props = defineProps({
show: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'success'
},
price: {
type: Number,
default: 1000
}
})
const emit = defineEmits(['cancel','update:show'])
const cancel= () => {
emit('update:show', false)
}
</script>
<template>
<div>
<van-dialog style="overflow: visible" :show="show" show-cancel-button :show-confirm-button="false" cancelButtonText="返回" cancelButtonColor="#2B53AC" @cancel="cancel">
<div class="h-145px relative flex justify-center">
<img :src="type==='success' ? successImg : errorImg" class="w-119px h-120px absolute top--74px z-9999 left-1/2 transform translate-x--1/2" alt="">
<div class="mt-94px text-#A9A9A9 text-16px">{{price}}</div>
</div>
</van-dialog>
</div>
</template>
<style scoped>
:deep(.van-hairline--top.van-dialog__footer){
border-top: 1px solid #E7E7E7;
border-bottom-left-radius:8px ;
border-bottom-right-radius:8px ;
}
</style>

View File

@ -0,0 +1,68 @@
<script setup>
import {ref} from "vue";
import lockClosed from "@/static/images/lockdfd@2x.png";
import lockOpen from "@/static/images/lock4@2x.png";
import {liveStore} from "@/stores/live/index.js";
import xButton from '@/components/x-button/index.vue'
import tangPopup from './tangPopup.vue'
import {goodStore} from "@/stores/goods/index.js";
import {authStore} from "~/stores/auth/index.js";
const {quoteStatus, changeStatus, show, auctionData, getSocketData} = liveStore();
const {pageRef} = goodStore();
const {userInfo} = authStore()
const showTang = ref(false)
const openOne = () => {
showTang.value = true
}
const paySide = computed(() => {
//
if (auctionData.value.artwork?.isSoled && auctionData.value.artwork?.buyInfo.userID === userInfo.value.ID) {
return true
} else {
return false
}
})
const goPay = () => {
show.value = true
}
</script>
<template>
<div class="bg-white w-60px rounded-l-4px overflow-hidden">
<!-- 拍品信息 -->
<van-button class="w-60px !h-60px" @click="openOne" style="border: none;border-radius: 0">
<div class="text-center flex flex-col justify-center items-center text-#7D7D7F text-12px">
<div>拍品</div>
<div>({{ auctionData?.artwork?.index }}/{{ pageRef.itemCount ?? 0 }})</div>
</div>
</van-button>
<tangPopup v-model:show="showTang"></tangPopup>
<!-- 出价开关 -->
<van-button class="w-60px !h-60px" @click="changeStatus"
style="border-right: none;border-left: none;border-radius: 0;padding: 0">
<div class="text-center flex flex-col justify-center items-center">
<div class="mb-4px">
<img
:src="quoteStatus ? lockClosed : lockOpen"
class="w-16px h-21px"
alt="锁图标"
/>
</div>
<div :class="quoteStatus ? 'text-gray-500' : 'text-blue-600'" class="text-10px transition-colors duration-200">
{{ quoteStatus ? '关闭出价' : '开启出价' }}
</div>
</div>
</van-button>
<!-- 支付 -->
<van-button v-if="paySide" class="w-60px !h-60px" style="border: none;border-radius: 0" @click="goPay">
<div class="text-center flex flex-col justify-center items-center text-yellow-600">
<div class="text-10px">RMB</div>
<div class="text-12px">5,000</div>
<div class="text-10px">去支付</div>
</div>
</van-button>
</div>
</template>

View File

@ -0,0 +1,150 @@
<script setup>
import xPopup from '@/components/x-popup/index.vue'
import {goodStore} from "@/stores/goods/index.js";
import xImage from '@/components/x-image/index.vue'
import DetailPopup from '@/pages/home/components/DetailPopup/index.vue'
import {liveStore} from "~/stores/live/index.js";
import {ref} from "vue";
const {pageRef,itemList,getArtworkList, loading: storeLoading,} = goodStore();
const {auctionData} = liveStore()
const showDetail=ref(false)
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getArtworkList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
const props = defineProps({
show: Boolean,
title: {
type: String,
default: ''
}
})
const scrollToCurrentItem = () => {
if (!itemList.value?.length) return
const currentIndex = itemList.value.findIndex(
item => auctionData.value.artwork.index === item?.index
)
if (currentIndex > -1) {
const container = document.querySelector('.list-container')
const targetElement = document.querySelectorAll('.item-wrapper')[currentIndex]
if (targetElement && container) {
const containerTop = container.getBoundingClientRect().top
const elementTop = targetElement.getBoundingClientRect().top
const scrollTop = elementTop - containerTop + container.scrollTop
container.scrollTo({
top: scrollTop,
behavior: 'smooth'
})
}
}
}
const emit = defineEmits(['update:show'])
const showDetailInfo=ref(null)
const close = () => emit('update:show', false);
const openShow=(item)=>{
showDetailInfo.value=item
showDetail.value=true
}
const loadMore = async () => {
pageRef.value.page++
const { finished } = await getArtworkList()
localState.value.finished = finished
}
watch(()=>props.show,(newValue)=>{
if (newValue){
nextTick(()=>{
scrollToCurrentItem()
})
}
})
</script>
<template>
<div>
<x-popup :show="show" @update:show="close">
<template #title>
<div class="text-#000 text-16px">拍品列表</div>
<div class="text-#939393 text-16px ml-14px">{{ pageRef.itemCount }}个拍品</div>
</template>
<div>
<van-pull-refresh
v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh"
>
<template #success>
<van-icon name="success" /> <span>刷新成功</span>
</template>
<van-list
v-model:loading="storeLoading"
:finished="localState.finished"
finished-text="没有更多了"
@load="loadMore"
>
<div
v-for="(item,index) of itemList"
:key="item.uuid"
class="flex mb-21px item-wrapper"
@click="openShow(item)"
>
<div
class="mr-10px flex-shrink-0 rounded-4px overflow-hidden cursor-pointer relative"
>
<xImage
:preview="false"
class="w-80px h-80px"
:src="item.artwork?.hdPic"
:alt="item?.artworkTitle"
loading="lazy"
/>
<div class="w-45px h-17px bg-#2B53AC text-12px line-height-none flex justify-center items-center absolute top-2px left-2px text-#fff">LOT{{item.index}}</div>
<div v-if="auctionData.artwork.index===item?.index" class="w-80px h-20px bg-#B58047 flex line-height-none justify-center items-center text-#fff text-12px bottom-0 absolute blink">投屏中</div>
</div>
<div>
<div class="ellipsis line-height-20px text-16px font-600 min-h-40px">
{{ item.artworkTitle }}
</div>
<div class="text-14px text-#575757">起拍价RMB 1,000</div>
<div class="text-14px text-#B58047">成交价等待更新</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</x-popup>
<DetailPopup v-model:show="showDetail" :detail-info="showDetailInfo"></DetailPopup>
</div>
</template>
<style scoped>
.ellipsis {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.blink {
animation: fade 1s linear infinite;
}
@keyframes fade {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>

View File

@ -0,0 +1,173 @@
<script setup>
import {ref, onMounted, onBeforeUnmount, watch} from 'vue'
import Aliplayer from 'aliyun-aliplayer'
import 'aliyun-aliplayer/build/skins/default/aliplayer-min.css'
import sideButton from '@/pages/liveRoom/components/SideButton/index.vue'
import broadcast from '@/pages/liveRoom/components/Broadcast/index.vue'
import {liveStore} from "@/stores/live/index.js"
import paymentResults from '@/pages/liveRoom/components/PaymentResults/index.vue'
import paymentInput from '@/pages/liveRoom/components/PaymentInput/index.vue'
import {goodStore} from "@/stores/goods/index.js"
import {message} from "~/components/x-message/useMessage.js"
import {artworkBuy} from "@/api/goods/index.js"
const player = ref(null)
const {quoteStatus, show, playerId, show1, auctionData, getSocketData, getLiveLink,fullLive} = liveStore()
const isPlayerReady = ref(false)
const pullLink = ref('')
definePageMeta({
title: '主页',
i18n: 'login.title',
})
const handlePlayerError = (error) => {
console.error('播放器错误:', error)
player.value?.play()
}
const initializePlayer = async () => {
try {
if (player.value) {
player.value.dispose()
}
const playerConfig = {
id: playerId.value,
source: pullLink.value,
isLive: true,
preload: true,
autoplayPolicy: {fallbackToMute: true},
controlBarVisibility: 'never',
}
player.value = new Aliplayer(playerConfig, (playerInstance) => {
isPlayerReady.value = true
playerInstance?.play()
})
player.value.on('error', handlePlayerError)
} catch (error) {
console.error('播放器初始化失败:', error)
}
}
onMounted(async () => {
pullLink.value = await getLiveLink()
initializePlayer()
})
onBeforeUnmount(() => {
player.value?.dispose()
player.value = null
})
watch(()=>fullLive.value, (newVal) => {
if (newVal) {
getSocketData()
}
})
const goBuy = async () => {
const res = await artworkBuy({
auctionArtworkUuid: auctionData.value?.artwork?.uuid,
buyMoney: String(auctionData.value?.nowAuctionPrice?.nextPrice ?? 0)
})
if (res.status === 0) {
message.success('出价成功')
}
}
const tipOpen = () => {
message.warning('出价状态未开启')
}
const updateShow=()=>{
}
</script>
<template>
<div class="relative h-full">
<div :id="playerId" class="w-full h-full"></div>
<transition>
<div v-if="fullLive">
<sideButton class="absolute top-196px right-0 z-999"></sideButton>
<div class="absolute left-1/2 transform -translate-x-1/2 flex flex-col items-center"
style="bottom:calc(var(--safe-area-inset-bottom) + 26px)">
<div class="text-16px text-#FFB25F font-600">
当前价{{ auctionData?.nowAuctionPrice?.currency }}
<van-rolling-text class="my-rolling-text" :start-num="0" :duration="0.5"
:target-num="auctionData?.nowAuctionPrice?.nowPrice??0" direction="up"/>
</div>
<div class="text-16px text-#fff font-600">
下口价{{ auctionData?.nowAuctionPrice?.currency }}
<van-rolling-text class="my-rolling-text1" :start-num="0" :duration="0.5"
:target-num="auctionData?.nowAuctionPrice?.nextPrice??0" direction="up"/>
</div>
<div v-if="quoteStatus" class="mt-10px mb-10px">
<van-button @click="goBuy" color="#FFB25F" class="w-344px !h-[40px]">
<div>{{
`确认出价 ${auctionData?.nowAuctionPrice?.currency} ${auctionData?.nowAuctionPrice?.nextPrice ?? 0}`
}}</div>
</van-button>
</div>
<div v-else class="mt-10px mb-10px">
<van-button @click="tipOpen" color="#D6D6D8" class="w-344px !h-[40px]" v-if="!quoteStatus">
<div class="text-#7D7D7F text-14px">点击"开启出价"即刻参与竞拍</div>
</van-button>
</div>
<broadcast></broadcast>
</div>
<paymentInput v-model:show="show"/>
<div>
</div>
<paymentResults v-model:show="show1" type="error"/>
<div v-if="auctionData?.wsType==='newArtwork'"
class="w-344px h-31px rounded-4px absolute top-9px bg-[#151824]/45 backdrop-blur-[10px] backdrop-saturate-[180%] left-1/2 transform translate-x--1/2 flex text-#fff text-14px items-center px-12px line-height-none">
<div class="mr-11px whitespace-nowrap">LOT{{ auctionData.artwork.index }}</div>
<div class="mr-10px truncate">{{ auctionData.artwork.name }}</div>
<div class="whitespace-nowrap">开始拍卖</div>
</div>
</div>
</transition>
</div>
</template>
<style lang="scss">
#J_prismPlayer {
width: 100%;
height: 100% !important;
& > video {
width: 100%;
height: 100%;
}
}
</style>
<style scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.my-rolling-text {
--van-rolling-text-item-width: 10px;
--van-rolling-text-font-size: 16px;
--van-rolling-text-color: #FFB25F;
}
.my-rolling-text1 {
--van-rolling-text-item-width: 10px;
--van-rolling-text-font-size: 16px;
--van-rolling-text-color: #FFF;
}
:deep(.prism-license-watermark) {
display: none !important;
}
</style>

209
app/pages/login/index.vue Normal file
View File

@ -0,0 +1,209 @@
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'
import countryCode from '../countryRegion/data/index.js'
import {senCode, userLogin} from "@/api/auth/index.js";
import {authStore} from "@/stores/auth/index.js";
import {message} from '@/components/x-message/useMessage.js'
const {userInfo,token}= authStore()
const router = useRouter();
const route = useRoute();
const { locale } = useI18n()
definePageMeta({
title: '登录',
i18n: 'login.title',
})
const loadingRef=ref({
loading1:false,
loading2:false,
})
const isExist=ref(false)// true
const isReal=ref(false) //isReal
function goToPage() {
router.push('/countryRegion');
}
const interval=ref(null)
const startCountdown=()=> {
if (interval.value){
clearInterval(interval.value);
}
countdown.value = 60;
interval.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(interval.value);
}
}, 1000);
}
const countdown = ref(0);
const phoneNum = ref('17630920520')
const code = ref('123789')
const pane = ref(0)
const showKeyboard = ref(false);
//
const getDefaultCountry = () => {
let defaultCode = 'CN' //
switch (locale.value) {
case 'zh-CN':
defaultCode = 'CN'
break
case 'zh-TW':
defaultCode = 'TW'
break
case 'ja-JP':
defaultCode = 'JP'
break
case 'en-US':
defaultCode = 'US'
break
}
const country = countryCode.find(c => c.code === defaultCode)
return {
zone: country.zone,
name: locale.value === 'zh-CN' ? country.cn :
locale.value === 'zh-TW' ? country.tw :
locale.value === 'ja-JP' ? country.ja :
country.en
}
}
const defaultCountry = getDefaultCountry()
//
const selectedZone = ref(route.query.zone || defaultCountry.zone)
const selectedCountry = ref(route.query.countryName || defaultCountry.name)
//
watch(locale, () => {
if (!route.query.zone) {
const newDefault = getDefaultCountry()
selectedZone.value = newDefault.zone
selectedCountry.value = newDefault.name
}
})
const vanSwipeRef=ref(null)
const getCode =async () => {
loadingRef.value.loading1=true
const res=await senCode({
telNum:phoneNum.value,
zone:selectedZone.value
})
loadingRef.value.loading1=false
if (res.status===0){
}
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value=true
startCountdown();
/* pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
showKeyboard.value=true
startCountdown();*/
}
const goBack = () => {
code.value = ''
pane.value = 0
vanSwipeRef.value?.swipeTo(pane.value)
}
const goLogin =async () => {
loadingRef.value.loading2=true
const res=await userLogin({
telNum:phoneNum.value,
zone:selectedZone.value,
code:code.value
})
if (res.status===0){
userInfo.value=res.data.accountInfo
token.value=res.data.token
if (!res.data.isReal){
await router.push('/realAuth');
}else {
await router.push('/');
}
}
loadingRef.value.loading2=false
}
</script>
<template>
<div class="h-screen-nav w-[100vw] bg-[url('@/static/images/asdfsdd.png')] bg-cover px-[31px] pt-[86px]">
<div class="w-full flex justify-center mb-[100px]">
<img class="h-[105px] w-[189px]" src="@/static/images/ghfggff.png" alt="">
</div>
<van-swipe ref="vanSwipeRef" :show-indicators="false" :touchable="false" :lazy-render="true" :loop="false">
<van-swipe-item >
<div v-show="pane===0">
<div class="">
<div class="w-full flex justify-between" @click="goToPage">
<div class="text-[16px] text-[#000]">
{{ selectedCountry }}
</div>
<div><van-icon color="#777" name="arrow" size="14" /></div>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="phoneNum" clearable :placeholder="$t('login.phonePlaceholder')">
<template #label>
<div class="text-[16px] text-[#1A1A1A] flex align-center justify-start">
+{{ selectedZone }}
</div>
</template>
</van-field>
</div>
<div />
</div>
<div class="mt-[55px]">
<van-button :loading="loadingRef.loading1" v-if="phoneNum" :loading-text="$t('login.getCode')" color="#2B53AC" block style="height: 48px" @click="getCode">{{ $t('login.getCode')
}}</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{ $t('login.getCode')
}}</van-button>
</div>
</div>
</van-swipe-item>
<van-swipe-item>
<div v-show="pane===1">
<div class="flex mb-[16px]">
<div class="text-[16px] text-[#BDBDBD] mr-[10px]">{{ $t('login.hasSendTo') }}</div>
<div class="text-[16px] text-[#000]">+{{ selectedZone }} {{ phoneNum }}</div>
</div>
<van-password-input :value="code" :gutter="10" :mask="false" focused @focus="showKeyboard = true" />
<div :class="`${countdown>0?'text-#BDBDBD':'text-#2B53AC'} text-14px`">
{{ $t('login.reSend') }}<span v-if="countdown>0">({{countdown}})</span>
</div>
<div class="mt-[17px]">
<van-button v-if="code.length === 6" type="primary" block :loading="loadingRef.loading2" :loading-text="$t('login.login')" style="height: 48px" @click="goLogin">{{
$t('login.login')
}}</van-button>
<van-button v-else type="primary" color="#D3D3D3" block style="height: 48px">{{ $t('login.login') }}</van-button>
</div>
<div class="mt-[17px]">
<van-button type="primary" @click="goBack" block style="height: 48px">{{ $t('login.back') }}</van-button>
</div>
</div>
</van-swipe-item>
</van-swipe>
<van-number-keyboard v-model="code" :show="showKeyboard" @blur="showKeyboard = false" />
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field) {
padding-left: 0;
}
:deep(.van-password-input) {
margin: 0;
}
:deep(.van-password-input__item) {
border: 1px solid #E5E5E5;
width: 41px;
height: 41px;
}
</style>

View File

@ -1,13 +1,126 @@
<script setup lang="ts">
<script setup>
import {userArtworks} from "@/api/goods/index.js";
import {authStore} from "@/stores/auth/index.js";
import xImage from '@/components/x-image/index.vue'
import {goodStore} from "~/stores/goods/index.js";
import {ref} from "vue";
definePageMeta({
layout: 'default',
title: '我的',
i18n: 'menu.profile',
})
const {artWorkDetail} = goodStore()
const myList=ref([])
const showMyList=ref([])
const {userInfo}= authStore()
const groupAndSortByDate=(data)=> {
if (!Array.isArray(data)) {
return
}
return Object.values(data.reduce((acc, curr) => {
if (!acc[curr.userCreatedAt]) {
acc[curr.userCreatedAt] = {
userCreatedAt: curr.userCreatedAt,
list: []
}
}
acc[curr.userCreatedAt].list.push(curr)
return acc;
}, {})).sort((a, b) => new Date(b.userCreatedAt) - new Date(a.userCreatedAt));
}
const initData=async ()=>{
const res=await userArtworks({})
if (res.status===0){
myList.value=res.data.data
showMyList.value=groupAndSortByDate(myList.value)
}
}
const router = useRouter()
const localState = ref({
finished: false,
refreshing: false,
showDetail: false,
showHeight: ''
})
initData()
const goPay=()=>{
router.push({
path:'/signature/personal-Info'
})
}
const goDetail=(item)=>{
router.push({
path:'/artDetail',
query:{
uuid:item.uuid
}
})
}
const onRefresh = async () => {
try {
localState.value.refreshing = true
localState.value.finished = false
const { finished } = await getArtworkList(true)
localState.value.finished = finished
} finally {
localState.value.refreshing = false
}
}
</script>
<template>
<div mx-auto mb-60 pt-15 text-center text-16 text-dark dark:text-white>
{{ $t('profile_page.txt') }}
<div class="w-[100vw] bg-[url('@/static/images/3532@2x.png')] bg-cover pt-43px flex-grow-1 flex flex-col">
<div class="flex items-center px-16px mb-43px">
<div class="mr-23px">
<img class="w-57px h-57px" src="@/static/images/5514@2x.png" alt="">
</div>
<div class="flex flex-col">
<div class="text-18px text-#181818">{{userInfo.realName}}</div>
<div class="text-#575757 text-14px">{{userInfo.telNum}}</div>
</div>
</div>
<div class="flex-grow-1 ">
<div class="border-b-1px border-b-#D3D3D3 px-16px flex">
<div class="text-#000 text-16px border-b-3 border-b-#2B53AC h-36px">我的拍品</div>
</div>
<van-pull-refresh v-model="localState.refreshing"
success-text="刷新成功"
:success-duration="700"
@refresh="onRefresh">
<van-list
finished-text="没有更多了"
>
<div class="px-16px pt-14px" v-for="(item,index) of showMyList" >
<div class="text-#575757 text-14px mb-3px">{{item.userCreatedAt}}</div>
<div class="flex mb-22px" v-for="(item1,index1) of item.list" @click="goDetail(item1)">
<div class="flex-shrink-0 mr-10px rounded-4px overflow-hidden">
<x-image class="w-80px h-80px" :src="item1?.auctionArtworkInfo?.artwork?.hdPic" :preview="false" alt=""/>
</div>
<div class="flex flex-col justify-between grow-1">
<div class="text-#000 text-16px ellipsis line-height-21px">{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}{{item1?.auctionArtworkInfo?.artworkTitle}}</div>
<div class="flex justify-between">
<div>
<div class="text-#575757 text-14px line-height-none mb-5px">起拍价RMB 1,000</div>
<div class="text-#B58047 text-14px line-height-none">成交价RMB 10,000</div>
</div>
<div v-if="[1,3,4].includes(item1.status)" @click.stop="goPay">
<van-button class="w-73px !h-30px" type="primary"><span class="text-12px">去支付</span></van-button>
</div>
</div>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</div>
</template>
<style scoped>
.ellipsis {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -1,42 +0,0 @@
<script lang="ts" setup>
import { useProseStore } from '~/stores/prose'
definePageMeta({
layout: 'default',
title: '随笔',
i18n: 'menu.fetch',
})
const proseStore = useProseStore()
function fetch() {
proseStore.fetchProse()
}
function clear() {
proseStore.clearProse()
}
</script>
<template>
<div>
<div class="h-300 flex items-center justify-center rounded-15 bg-white p-16 dark:bg-[--van-background-2]">
<div v-if="proseStore.prose" class="text-16 leading-26">
{{ proseStore.prose }}
</div>
<ClientOnly v-else>
<van-empty :description="$t('prose_page.btn_empty_desc')" />
</ClientOnly>
</div>
<van-space class="m-10" direction="vertical" fill>
<van-button type="primary" round block @click="fetch">
{{ $t('prose_page.btn_fetch') }}
</van-button>
<van-button type="default" round block @click="clear">
{{ $t('prose_page.btn_clear') }}
</van-button>
</van-space>
</div>
</template>

View File

@ -0,0 +1,65 @@
<script setup>
import {authStore} from "@/stores/auth/index.js";
const props = defineProps({
type: {
type: Number,
default: 0
}
})
const {userInfo}= authStore()
</script>
<template>
<div class="text-#1A1A1A text-16px">
<template v-if="type===0">
<div class="flex mb-20px">
<div class="mr-10px">姓名</div>
<div>{{userInfo.realName}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">性别</div>
<div>{{userInfo.sex}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">出生日期</div>
<div>{{userInfo.birthDate}}</div>
</div>
<div class="flex">
<div class="mr-10px">身份证号</div>
<div>{{userInfo.idNum}}</div>
</div>
</template>
<template v-if="type===1">
<div class="flex mb-20px" >
<div class="mr-10px">姓名</div>
<div>{{userInfo.realName}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">性别</div>
<div>{{userInfo.sex}}</div>
</div>
<div class="flex mb-20px">
<div class="mr-10px">出生日期</div>
<div>{{userInfo.birthDate}}</div>
</div>
<div class="flex">
<div class="mr-10px">家庭住址</div>
<div>{{userInfo.idNum}}</div>
</div>
<div class="flex">
<div class="mr-10px">所属银行</div>
<div>{{userInfo.idNum}}</div>
</div>
<div class="flex">
<div class="mr-10px">银行卡号码</div>
<div>{{userInfo.idNum}}</div>
</div>
</template>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,166 @@
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'
import {userUpdate} from "@/api/auth/index.js";
import {message} from '@/components/x-message/useMessage.js'
import detail from './components/detail.vue'
import {authStore} from "@/stores/auth/index.js";
const router = useRouter();
const route = useRoute();
const showPicker = ref(false);
const {userInfo}= authStore()
const birthdayDate = ref([])
const showBirthdayPicker = ref(false)
const minDate = new Date(1950, 0, 1)
const maxDate = new Date(2025, 12, 31)
const active=ref(0)
const { t } = useI18n()
const form=ref({
idNum: "",
realName: "",
sex:'',
birthDate:'',
userExtend: {
address: "",
bankName: "",
bankNo: ""
}
})
const form1=ref({
idNum:'',
realName:''
})
const columns=ref([
{ text: t('realAuth.male'), value: 1 },
{ text: t('realAuth.female'), value: 2 },
])
const onConfirm = ({ selectedValues, selectedOptions }) => {
form.value.sex=selectedValues?.[0]
showPicker.value = false
}
const onBirthdayConfirm = (value) => {
form.value.birthDate=value.selectedValues.join('-')
showBirthdayPicker.value = false
}
function isFormComplete(obj) {
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
if (!isFormComplete(obj[key])) {
return false;
}
} else if (obj[key] === "") {
return false;
}
}
return true;
}
const statusCode=ref(0)
const confirm=async ()=>{
const thatForm=active.value===0?form1.value:form.value
if (isFormComplete(thatForm)){
const res=await userUpdate(thatForm)
if (res.status===0){
userInfo.value=res.data
message.success('提交成功')
statusCode.value=1
}
}else {
message.error('请填写身份证相关信息')
}
}
const goHome=()=>{
router.push('/')
}
definePageMeta({
title: '实名认证',
i18n: 'realAuth.title',
})
</script>
<template>
<div class="px-[31px] bg-[url('@/static/images/asdfsdd.png')] bg-cover w-100vw flex-grow-1 pt-[46px] relative flex flex-col">
<van-tabs v-if="statusCode===0" v-model:active="active" animated swipeable>
<van-tab :title="$t('realAuth.cnTab')" class="pt-[80px]">
<template v-if="statusCode===0">
<div class="text-[#BDBDBD] text-[16px] mb-[34px]">{{ $t('realAuth.cnTabDesc') }}</div>
<div class="mb-[100px]">
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form1.idNum" :label="$t('realAuth.idCard')" clearable
:placeholder="$t('realAuth.idCardPlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form1.realName" :label="$t('realAuth.name')" clearable :placeholder="$t('realAuth.namePlaceholder')"></van-field>
</div>
</div>
</template>
</van-tab>
<van-tab :title="$t('realAuth.otherTab')" class="pt-[80px]">
<div class="text-[#BDBDBD] text-[16px] mb-[34px]">{{ $t('realAuth.otherTabDesc') }}</div>
<div class="mb-[100px]">
<div class="border-b-[1.7px] mt-[8px]">
<van-field :label="$t('realAuth.name')" clearable :placeholder="$t('realAuth.namePlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field :modelValue="columns.find(x=>x.value===form.sex)?.text" is-link readonly name="picker" :label="$t('realAuth.gender')"
@click="showPicker = true" />
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.birthDate" is-link readonly name="birthdayPicker" :label="$t('realAuth.birthday')"
:placeholder="$t('realAuth.birthdayPlaceholder')" @click="showBirthdayPicker = true" />
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.userExtend.address" :label="$t('realAuth.adress')" clearable
:placeholder="$t('realAuth.adressPlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.userExtend.bankName" :label="$t('realAuth.bank')" clearable :placeholder="$t('realAuth.bankPlaceholder')"></van-field>
</div>
<div class="border-b-[1.7px] mt-[8px]">
<van-field v-model="form.userExtend.bankNo" :label="$t('realAuth.bankCard')" clearable
:placeholder="$t('realAuth.bankCardPlaceholder')"></van-field>
</div>
</div>
</van-tab>
</van-tabs>
<van-tabs v-else-if="statusCode===1" v-model:active="active" animated swipeable>
<van-tab :title="$t('realAuth.cnTab')" class="pt-[80px]">
<detail :type="active"></detail>
</van-tab>
<van-tab :title="$t('realAuth.otherTab')" class="pt-[80px]">
<detail :type="active"></detail>
</van-tab>
</van-tabs>
<div class="flex justify-between" v-if="statusCode===0">
<van-button style="width: 151px;height: 48px" color="#E9F1F8">
<div class="text-#2B53AC text-16px">{{ $t('realAuth.cancel') }}</div>
</van-button>
<van-button @click="confirm" style="width: 151px;height: 48px" color="#2B53AC">
<div class="text-#FFFFFF text-16px">{{ $t('realAuth.confirm') }}</div>
</van-button>
</div>
<div v-else class="mt-auto pb-94px">
<van-button color="#E9F1F8" @click="goHome" style="color: #2B53AC;font-weight: 600" block>去首页</van-button>
</div>
<van-popup v-model:show="showPicker" destroy-on-close position="bottom">
<van-picker :columns="columns" @confirm="onConfirm" @cancel="showPicker = false" />
</van-popup>
<van-popup v-model:show="showBirthdayPicker" destroy-on-close position="bottom">
<van-date-picker v-model="birthdayDate" :min-date="minDate" :max-date="maxDate"
@cancel="showBirthdayPicker = false" @confirm="onBirthdayConfirm" />
</van-popup>
</div>
</template>
<style scoped>
:deep(.van-tabs__line) {
height: 2px;
width: 107px;
}
:deep(.van-cell) {
padding-left: 0;
}
</style>

View File

@ -0,0 +1,18 @@
<script setup>
const image = ref('');
import { showToast } from 'vant';
const onSubmit = (data) => {
image.value = data.image;
};
const onClear = () => showToast('clear');
</script>
<template>
<van-signature @submit="onSubmit" @clear="onClear" />
<van-image v-if="image" :src="image" />
</template>
<style scoped>
</style>

View File

@ -0,0 +1,63 @@
<script setup>
import {useI18n} from "vue-i18n";
import XVanSelect from '@/components/x-van-select/index.vue'
import XVanDate from '@/components/x-van-date/index.vue'
definePageMeta({
name: 'personal-info',
})
const {t} = useI18n()
const showPicker = ref(false)
const showPicker1 = ref(false)
const onConfirm = () => {
}
const router = useRouter()
const columns = ref([
{text: t('realAuth.male'), value: 1},
{text: t('realAuth.female'), value: 2},
])
const goCountryRegion=()=>{
router.push({
path:'/countryRegion'
})
}
const adress=ref('')
</script>
<template>
<div
class="w-[100vw] bg-[url('@/static/images/asdfsdd.png')] h-screen-nav bg-cover pt-77px flex-grow-1 flex flex-col ">
<div class="text-16px text-#191919 font-bold mb-40px px-34px">
请填写个人相关信息
</div>
<div class="grow-1 px-34px">
<van-field type="tel" :label-width="161" label="文本" class="mb-10px" placeholder="请输入手机号">
<template #label>
<div class="flex">
<div class="mr-41px whitespace-nowrap">手机号</div>
<div @click="goCountryRegion">
<span class="mr-13px">+ 86</span>
<van-icon name="arrow-down" class="text-#777777"/>
</div>
</div>
</template>
</van-field>
<van-field label="姓名" class="mb-10px" placeholder="请输入姓名"/>
<x-van-select label="性别" :columns="columns"/>
<x-van-date label="出生日期"/>
<van-field v-model="adress" label="家庭住址" class="mb-10px" placeholder="请输入家庭住址"/>
<van-field label="所属银行" class="mb-10px" placeholder="请输入所属银行"/>
<van-field label="银行卡号码" class="mb-10px" placeholder="请输入银行卡号码"/>
</div>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">下一步</van-button>
</div>
</div>
</template>
<style scoped lang="scss">
:deep(.van-cell.van-field){
padding-left: 0;
}
</style>

View File

@ -0,0 +1,43 @@
<script setup>
const activeNames = ref(['1']);
definePageMeta({
layout: 'default',
title: '签署内容'
})
</script>
<template>
<div class="bg-#EBEBEB h-screen-nav flex flex-col">
<div class="h-50px text-14px text-#191919 bg-#fff flex items-center px-21px mb-6px">支付前需同意以下内容并签字</div>
<van-collapse v-model="activeNames" class="grow-1">
<van-collapse-item name="1" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="2" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
<van-collapse-item name="3" class="mb-6px">
<template #title>
<div class="text-#2B53AC text-14px">拍卖规则</div>
</template>
代码是写出来给人看的附带能在机器上运行
</van-collapse-item>
</van-collapse>
<div class="h-81px bg-#fff flex justify-center pt-7px border-t">
<van-button color="#2B53AC" class="w-213px van-btn-h-38px">同意并签字</van-button>
</div>
</div>
</template>
<style scoped>
:deep(.van-cell__right-icon){
color: #ACACAC;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import MasonryWall from '@yeger/vue-masonry-wall'
const items = [
{
title: 'First',
description: 'The first item.',
},
{
title: 'Second',
description: 'The second item.',
},
]
</script>
<template>
<masonry-wall :items="items" :ssr-columns="2" :minColumns="2" :gap="16">
<template #default="{ item, index }">
<div :style="{ height: `${(index + 1) * 100}px` }">
<h1>{{ item.title }}</h1>
<span>{{ item.description }}</span>
</div>
</template>
</masonry-wall>
</template>

View File

@ -1,22 +0,0 @@
<script setup lang="ts">
definePageMeta({
title: '🎨 Unocss 示例',
i18n: 'menu.unocssExample',
})
</script>
<template>
<div>
<h1 class="text-6xl color-pink font-semibold">
{{ $t('unocss_page.hello', ['Unocss!']) }}
</h1>
<p class="mt-10 text-gray-700 dark:text-white">
{{ $t('unocss_page.desc') }}
</p>
<button class="mt-10 btn">
{{ $t('unocss_page.btn_txt') }}
</button>
</div>
</template>

View File

@ -1,4 +1,4 @@
import { setupHttp } from '~/api/http'
import { setupHttp } from '@/api/http'
export default defineNuxtPlugin(() => {
setupHttp()

View File

@ -2,11 +2,15 @@ import type { Locale as TypeLocale } from '#i18n'
import { Locale } from 'vant'
import enUS from 'vant/es/locale/lang/en-US'
import zhCN from 'vant/es/locale/lang/zh-CN'
import jaJP from 'vant/es/locale/lang/ja-JP'
import zhTW from 'vant/es/locale/lang/zh-TW'
export default defineNuxtPlugin(() => {
// 载入 vant 语言包
Locale.use('zh-CN', zhCN)
Locale.use('en-US', enUS)
Locale.use('ja-JP', jaJP)
Locale.use('zh-TW', zhTW)
if (import.meta.client) {
const i18n = useNuxtApp().$i18n

View File

@ -0,0 +1,8 @@
import VConsole from 'vconsole'
export default defineNuxtPlugin(() => {
if (process.env.NODE_ENV !== 'production') {
const vConsole = new VConsole()
console.log('VConsole is enabled')
}
})

89
app/plugins/websocket.ts Normal file
View File

@ -0,0 +1,89 @@
import {authStore} from "@/stores/auth";
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const { token } = authStore()
const ws = reactive({
instance: null as WebSocket | null,
isConnected: false,
// 修改 connect 方法接收路径和数据对象
connect(path: string, data?: Record<string, any>) {
if (this.instance?.readyState === WebSocket.OPEN) {
this.instance.close()
}
// 构建查询字符串
const queryString =data
? '?' + Object.entries({ token: token.value,...data})
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
: ''
// 构建完整的 WebSocket URL
const wsUrl = `${config.public.NUXT_PUBLIC_SOCKET_URL}${path}${queryString}`
this.instance = new WebSocket(wsUrl)
this.instance.onopen = () => {
this.isConnected = true
console.log('WebSocket 已连接')
}
this.instance.onclose = () => {
this.isConnected = false
console.log('WebSocket 已断开')
/* this.reconnect(path, data)*/
}
this.instance.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
this.instance.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.handleMessage(data)
} catch (error) {
console.error('消息解析错误:', error)
}
}
},
// 更新重连方法以支持数据对象
reconnect(path: string, data?: Record<string, any>) {
setTimeout(() => {
console.log('尝试重新连接...')
this.connect(path, data)
}, 3000)
},
// 发送消息
send(data: any) {
if (this.instance?.readyState === WebSocket.OPEN) {
this.instance.send(JSON.stringify(data))
} else {
console.warn('WebSocket 未连接,无法发送消息')
}
},
// 关闭连接
disconnect() {
if (this.instance) {
this.instance.close()
this.instance = null
}
},
// 消息处理
handleMessage(data: any) {
// 触发自定义事件,让组件可以监听
window.dispatchEvent(new CustomEvent('ws-message', { detail: data }))
}
})
return {
provide: {
ws
}
}
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Some files were not shown because too many files have changed in this diff Show More