From 8694fde44a516dd9bf245904f275aaa4993b6d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Wed, 30 Sep 2026 17:03:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E3=80=81UI=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.d.ts | 3 +- package.json | 5 + src/App.vue | 77 +- src/api/app.ts | 41 + src/api/member.ts | 20 +- src/api/message.ts | 38 + src/api/project.ts | 155 ++++ src/api/quota.ts | 8 +- src/api/request.ts | 25 +- src/api/service.ts | 168 ++++ src/api/user.ts | 21 + src/components/BackFab.vue | 78 ++ src/components/TabBar.vue | 15 +- src/components/card/CardTile.vue | 51 +- src/components/home/PaneMine.vue | 340 ++++++-- src/components/home/PaneWorkbench.vue | 494 +++++++++++- src/components/mine/MineDrawers.vue | 125 ++- src/components/tool/ToolIdle.vue | 4 +- src/components/tool/ToolPage.vue | 7 + src/components/tool/ToolRunning.vue | 7 +- src/components/vip/QuotaBar.vue | 9 +- src/components/vip/VipCard.vue | 17 +- src/components/vip/VipFloatAd.vue | 9 +- src/config/card-styles.ts | 19 +- src/config/features.ts | 39 +- src/pages.json | 78 +- src/pages/account/login.vue | 8 +- src/pages/mine/profile.vue | 15 +- src/pages/tools/image/compress.vue | 13 +- src/pages/tools/image/index.vue | 7 + src/pages/tools/image/matting.vue | 7 +- src/pages/tools/image/qrcode.vue | 7 +- src/pages/tools/image/watermark.vue | 7 +- src/pages/workbench/edit.vue | 7 + src/pkg-event/list.vue | 7 + src/pkg-event/query.vue | 7 + src/pkg-member/coupon/index.vue | 7 + src/pkg-member/member/detail.vue | 281 +++++-- src/pkg-member/member/index.vue | 22 +- src/pkg-member/member/orders.vue | 389 ++++++--- src/pkg-member/quota/index.vue | 20 +- src/pkg-member/quota/my.vue | 15 +- src/pkg-msg/index.vue | 258 ++++++ src/pkg-note/edit.vue | 181 ++++- src/pkg-note/list.vue | 7 + src/pkg-service/detail.vue | 321 ++++++++ src/pkg-service/home.vue | 1061 +++++++++++++++++++++++++ src/pkg-service/list.vue | 348 ++++++++ src/pkg-service/project-bind.vue | 273 +++++++ src/pkg-service/project-detail.vue | 765 ++++++++++++++++++ src/pkg-service/projects.vue | 286 +++++++ src/pkg-service/quote.vue | 370 +++++++++ src/pkg-service/quotes.vue | 289 +++++++ src/pkg-service/work-detail.vue | 381 +++++++++ src/pkg-service/works.vue | 338 ++++++++ src/pkg-style/showcase.vue | 7 + src/stores/feedback.ts | 42 + src/stores/message.ts | 74 ++ src/uni.scss | 5 + src/utils/md-parser.ts | 295 +++++++ src/utils/page-scrolled.ts | 19 + vite.config.ts | 6 + 62 files changed, 7662 insertions(+), 336 deletions(-) create mode 100644 src/api/app.ts create mode 100644 src/api/message.ts create mode 100644 src/api/project.ts create mode 100644 src/api/service.ts create mode 100644 src/components/BackFab.vue create mode 100644 src/pkg-msg/index.vue create mode 100644 src/pkg-service/detail.vue create mode 100644 src/pkg-service/home.vue create mode 100644 src/pkg-service/list.vue create mode 100644 src/pkg-service/project-bind.vue create mode 100644 src/pkg-service/project-detail.vue create mode 100644 src/pkg-service/projects.vue create mode 100644 src/pkg-service/quote.vue create mode 100644 src/pkg-service/quotes.vue create mode 100644 src/pkg-service/work-detail.vue create mode 100644 src/pkg-service/works.vue create mode 100644 src/stores/feedback.ts create mode 100644 src/stores/message.ts create mode 100644 src/utils/md-parser.ts create mode 100644 src/utils/page-scrolled.ts diff --git a/components.d.ts b/components.d.ts index 4f85df3..b709739 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,6 +7,7 @@ export {} declare module 'vue' { export interface GlobalComponents { + BackFab: typeof import('./src/components/BackFab.vue')['default'] CardTile: typeof import('./src/components/card/CardTile.vue')['default'] Confetti: typeof import('./src/components/Confetti.vue')['default'] CountUp: typeof import('./src/components/CountUp.vue')['default'] @@ -32,7 +33,7 @@ declare module 'vue' { VipDock: typeof import('./src/components/vip/VipDock.vue')['default'] VipFloatAd: typeof import('./src/components/vip/VipFloatAd.vue')['default'] WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default'] - WdDatetimePicker: typeof import('wot-design-uni/components/wd-datetime-picker/wd-datetime-picker.vue')['default'] + WdDatetimePickerView: typeof import('wot-design-uni/components/wd-datetime-picker-view/wd-datetime-picker-view.vue')['default'] WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default'] WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default'] WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default'] diff --git a/package.json b/package.json index 3564f36..9a7327e 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,11 @@ "type": "module", "version": "0.1.0", "private": true, + "browserslist": [ + "iOS >= 15", + "Android >= 8", + "Chrome >= 79" + ], "scripts": { "dev": "uni", "dev:app": "uni -p app", diff --git a/src/App.vue b/src/App.vue index e8c1cd9..76d316d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -26,6 +26,15 @@ page { --tb-shadow-1: 0 2rpx 6rpx rgba(107, 47, 15, 0.05), 0 10rpx 24rpx rgba(107, 47, 15, 0.08); --tb-shadow-2: 0 3rpx 10rpx rgba(107, 47, 15, 0.05), 0 16rpx 38rpx rgba(107, 47, 15, 0.1); --tb-shadow-3: 0 4rpx 12rpx rgba(107, 47, 15, 0.06), 0 22rpx 52rpx rgba(107, 47, 15, 0.13); + /* 液态玻璃 token:色相全部沿用现有配色(白 + 暖棕影),只加透明度/模糊/高光层。 + 透明度压低 + 模糊加强,让卡片真正「透出」背后的氛围色(Apple Liquid Glass 的核心)。 */ + --tb-glass-bg: rgba(255, 255, 255, 0.42); + --tb-glass-bg-strong: rgba(255, 255, 255, 0.6); + --tb-glass-bg-soft: rgba(255, 255, 255, 0.3); + --tb-glass-line: rgba(255, 255, 255, 0.55); + --tb-glass-hi: rgba(255, 255, 255, 0.85); + --tb-glass-blur: 36rpx; + --tb-glass-saturate: 1.9; /* 撞色点缀 */ --tb-mint: #3ecfb2; --tb-lemon: #ffc53d; @@ -46,6 +55,15 @@ page { 'Helvetica Neue', sans-serif; font-size: 28rpx; -webkit-font-smoothing: antialiased; + /* 氛围光斑底:同色系(橙/柠檬/mint/pink)径向渐变色场, + 给玻璃卡片的 backdrop-filter 提供「可透出的颜色」——液态玻璃感的前提。 + 透明度够高才能透过玻璃看到颜色(之前太素,玻璃退化成白卡)。 */ + background-image: + radial-gradient(1000rpx 700rpx at 110% -8%, rgba(255, 159, 77, 0.34), transparent 66%), + radial-gradient(820rpx 600rpx at -16% 18%, rgba(255, 197, 61, 0.26), transparent 64%), + radial-gradient(920rpx 680rpx at 114% 48%, rgba(62, 207, 178, 0.2), transparent 66%), + radial-gradient(860rpx 640rpx at -14% 78%, rgba(255, 123, 169, 0.16), transparent 64%), + radial-gradient(700rpx 520rpx at 52% 104%, rgba(255, 159, 77, 0.18), transparent 66%); } /* 图片工具隐藏工作画布(MP) */ .hidden-canvas { @@ -128,13 +146,41 @@ page { margin-top: -120rpx; } -/* ============ 卡片 ============ */ +/* ============ 卡片(液态玻璃)============ + 半透明白 + backdrop blur/saturate + 白高光描边 + 底部折射亮边;圆角不变。 + 外阴影只用中性暖棕(Apple 玻璃不用彩色光晕),不支持 blur 的环境回落不透明白卡。 */ .tb-card { - background: var(--tb-card, #ffffff); + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.42)); border-radius: 36rpx; - border: 1rpx solid var(--tb-card-line, rgba(255, 195, 155, 0.55)); - /* 升级(非推翻):环境影 + 近距离高光影 + 顶部内高光(R2-1) */ - box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), var(--tb-shadow-2); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.55)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); + backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), + inset 0 -1rpx 0 rgba(255, 255, 255, 0.4), var(--tb-shadow-2); +} +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .tb-card { + background: var(--tb-card, #ffffff); + border-color: var(--tb-card-line, rgba(255, 195, 155, 0.55)); + } +} +/* 玻璃材质公共类:给页面本地卡片叠加用(与 .tb-card 二选一或组合皆可) */ +.tb-glass { + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.42)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.55)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); + backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); +} +/* 玻璃加厚:悬浮元素(搜索胶囊 / dock / 弹层)用,透明度更低 */ +.tb-glass-strong { + background: var(--tb-glass-bg-strong, rgba(255, 255, 255, 0.6)); +} +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .tb-glass, + .tb-glass-strong { + background: var(--tb-card, #ffffff); + border-color: var(--tb-card-line, rgba(255, 195, 155, 0.55)); + } } /* ============ 卡片质感分层(R2-1 / R2-2)============ @@ -142,13 +188,16 @@ page { 另有两档专用:tb-elev-primary(主卡·橙)、tb-elev-gold(会员卡·金)。 改一处 token,全站卡片同步生效;不新增色系。 */ .tb-elev-1 { - box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), var(--tb-shadow-1); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), + inset 0 -1rpx 0 rgba(255, 255, 255, 0.4), var(--tb-shadow-1); } .tb-elev-2 { - box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), var(--tb-shadow-2); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), + inset 0 -1rpx 0 rgba(255, 255, 255, 0.4), var(--tb-shadow-2); } .tb-elev-3 { - box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), var(--tb-shadow-3); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), + inset 0 -1rpx 0 rgba(255, 255, 255, 0.4), var(--tb-shadow-3); } /* 主卡(最常用工具):橙渐变上叠内高光 + 主色投影(对齐既有 .wb-lead 阴影升级) */ .tb-elev-primary { @@ -198,10 +247,14 @@ page { display: flex; align-items: center; gap: 16rpx; - background: rgba(255, 255, 255, 0.97); + /* 悬浮在橙色 hero 上:玻璃化收益大 */ + background: var(--tb-glass-bg-strong, rgba(255, 255, 255, 0.68)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); border-radius: 999rpx; padding: 22rpx 30rpx; - box-shadow: 0 14rpx 30rpx rgba(151, 60, 8, 0.22); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 14rpx 30rpx rgba(151, 60, 8, 0.18); position: relative; z-index: 1; } @@ -282,7 +335,9 @@ page { border: 2rpx dashed #ffd4b8; border-radius: 30rpx; padding: 56rpx 40rpx; - background: rgba(255, 255, 255, 0.7); + background: var(--tb-glass-bg-soft, rgba(255, 255, 255, 0.42)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); text-align: center; box-sizing: border-box; } diff --git a/src/api/app.ts b/src/api/app.ts new file mode 100644 index 0000000..74afea0 --- /dev/null +++ b/src/api/app.ts @@ -0,0 +1,41 @@ +import { get } from '@/api/request' + +/** + * 应用配置接口(对应后端 `api/user/v1/app_config.go`,走 UserAuth 组,需登录态)。 + * 契约要点(与后端 `g.Meta` 的 path/method **逐字对齐**,改后端务必同步此处): + * - `feature_covers` 的 key 对齐本端 `config/features.ts` 的 FEATURE_ENTRIES key; + * - 值已是后端按 site.base_url 拼好的绝对 URL,直接用于背景图 / ; + * - 未配置的 key 不出现在 map 中(调用方做渐变兜底)。 + */ +export interface AppConfigData { + feature_covers: Record +} + +let cache: AppConfigData | null = null +let pending: Promise | null = null + +/** + * 拉取应用配置(进程内缓存:配置变更频率极低,首页多次激活不重复请求; + * 失败返回空配置,功能卡走渐变兜底,不打扰用户)。 + */ +export function apiAppConfig(): Promise { + if (cache) + return Promise.resolve(cache) + if (!pending) { + pending = get('/app/config') + .then((d) => { + cache = d || { feature_covers: {} } + return cache + }) + .catch(() => ({ feature_covers: {} } as AppConfigData)) + .finally(() => { + pending = null + }) + } + return pending +} + +/** 获取某个功能卡的背景图 URL(未配置返回空串) */ +export function featureCoverOf(covers: Record | undefined, key: string): string { + return covers?.[key] || '' +} diff --git a/src/api/member.ts b/src/api/member.ts index 2ad8892..8b9b6a5 100644 --- a/src/api/member.ts +++ b/src/api/member.ts @@ -158,9 +158,18 @@ export function apiMemberCenter() { return get('/member/center') } +/** 客户端平台标识:iOS 走 App Store IAP(pay_channel=apple),服务端据此禁用 API 退款 */ +export function clientPlatform(): string { + try { + return uni.getSystemInfoSync().platform || '' + } catch { + return '' + } +} + /** 建单(第一步):只落一条待支付订单,**不返回支付参数** */ export function apiMemberOrderCreate(planKey: string) { - return post('/member/order', { plan_key: planKey }) + return post('/member/order', { plan_key: planKey, platform: clientPlatform() }) } /** @@ -195,7 +204,10 @@ export function apiMemberOrderCheck(outTradeNo: string) { return post('/member/order/check', { out_trade_no: outTradeNo }) } -/** 我的会员订单(含次数包订单,用 order_type 区分) */ -export function apiMemberOrders(limit = 20) { - return get<{ list: MemberOrder[] }>('/member/orders', { limit }) +/** + * 我的会员订单(含次数包订单,用 order_type 区分)。 + * tab:0全部 1待付款 2已生效 3退款(后端按 status / refund_status 过滤)。 + */ +export function apiMemberOrders(limit = 20, tab = 0) { + return get<{ list: MemberOrder[] }>('/member/orders', { limit, tab: tab || undefined }) } diff --git a/src/api/message.ts b/src/api/message.ts new file mode 100644 index 0000000..ef0d8f5 --- /dev/null +++ b/src/api/message.ts @@ -0,0 +1,38 @@ +import { get, post } from '@/api/request' + +/** + * 站内信接口封装(对应后端 `api/user/v1/message.go`,UserAuth 组,需登录态)。 + * + * 写入点全在后端业务事件(会员开通/次数包到账、报价回执、报价处理、反馈回复), + * C 端只读:列表 / 未读数 / 标记已读。徽标数据源走 `stores/message.ts`。 + */ +export const MSG_TYPE_ORDER_DELIVERED = 'order_delivered' +export const MSG_TYPE_QUOTE_SUBMITTED = 'quote_submitted' +export const MSG_TYPE_QUOTE_HANDLED = 'quote_handled' +export const MSG_TYPE_FEEDBACK_REPLIED = 'feedback_replied' +export const MSG_TYPE_SYSTEM = 'system' + +export interface MessageItem { + id: number + type: string + title: string + content: string + link: string + is_read: number + created_at: string +} + +/** 我的消息(最近 N 条 + 未读数;不自动置已读) */ +export function apiMessageList(limit = 20) { + return get<{ list: MessageItem[], unread: number }>('/message/list', { limit }) +} + +/** 未读消息数(首页 / 我的页徽标) */ +export function apiMessageUnread() { + return get<{ count: number }>('/message/unread') +} + +/** 标记已读:id>0 单条;id 缺省 = 全部 */ +export function apiMessageRead(id = 0) { + return post>('/message/read', id > 0 ? { id } : {}) +} diff --git a/src/api/project.ts b/src/api/project.ts new file mode 100644 index 0000000..760a51b --- /dev/null +++ b/src/api/project.ts @@ -0,0 +1,155 @@ +import { get, post } from '@/api/request' + +/** + * 开发项目接口封装(对应后端 `api/user/v1/project.go`,UserAuth 组)。 + * + * 流程:后台建单生成绑定码 → 用户扫码(scene=p<项目id><令牌>)进入绑定页绑定 → + * 此后「我的项目」可随时查看阶段进度 / 小节点 / 交付物(原型、UI 设计稿)。 + */ +export const PROJECT_STATUS_RUNNING = 1 +export const PROJECT_STATUS_DONE = 2 +export const PROJECT_STATUS_CLOSED = 3 + +/** 项目状态 → 主题色 */ +export function projectStatusTone(status: number): string { + switch (status) { + case PROJECT_STATUS_DONE: return '#0fa385' + case PROJECT_STATUS_CLOSED: return '#9a7b65' + default: return '#e8531a' + } +} + +/** 阶段状态 → 主题色 */ +export function stageStatusTone(status: number): string { + switch (status) { + case 1: return '#b57f00' + case 2: return '#0fa385' + default: return '#9a7b65' + } +} + +export interface ProjectNode { + id: number + title: string + is_done: number + done_at: string +} + +export interface ProjectFile { + id: number + file_name: string + file_url: string +} + +export interface ProjectStage { + id: number + title: string + status: number + status_text: string + nodes: ProjectNode[] + files: ProjectFile[] +} + +export interface ProjectItem { + id: number + title: string + description: string + price_cents: number + price_note: string + due_note: string + status: number + status_text: string + stage_total: number + stage_done: number + cover_url: string + updated_at: string +} + +/** 我的项目列表 */ +export function apiProjectMine() { + return get<{ list: ProjectItem[] }>('/service/project/mine') +} + +/** 项目详情(仅限已绑定用户;四期扩展:分期付款 / 维护期 / 验收) */ +export function apiProjectDetail(id: number) { + return get('/service/project/detail', { id }) +} + +/** 扫码 / 手动输码绑定(绑定成功后服务端会发站内信回执) */ +export function apiProjectBind(token: string) { + return post<{ project_id: number }>('/service/project/bind', { token }) +} + +/** + * 解析小程序码 scene(URL 编码过的 "p<项目id><令牌>")→ 绑定令牌。 + * 兼容直接传 token 的入口。 + */ +export function parseProjectScene(scene?: string): string { + if (!scene) + return '' + let s = scene + try { + s = decodeURIComponent(scene) + } + catch { + /* 已是明文则原样使用 */ + } + s = s.trim() + if (!s.startsWith('p')) + return '' + s = s.slice(1) + let i = 0 + while (i < s.length && s[i] >= '0' && s[i] <= '9') + i++ + const token = s.slice(i).trim() + return token.length >= 6 && token.length <= 16 ? token : '' +} + +// ============================================================================ +// 四期:分期付款 / 维护期 / 确认验收 +// ============================================================================ + +/** 分期触发节点 → 展示名(与后端 consts.PayTriggerNameOf 对齐) */ +export function payTriggerText(trigger: string): string { + switch (trigger) { + case 'start': return '开工前(定金)' + case 'mid_demo': return '中期演示确认后' + case 'online_before': return '上线前' + case 'online_after': return '上线后' + } + return trigger +} + +export interface ProjectPay { + id: number + seq: number + percent: number + amount_cents: number + trigger: string + trigger_text: string + status: number // 0待支付 1已支付 + paid_at: string +} + +export interface ProjectMaintain { + plan_name: string + free_years: number + yearly_fee_cents: number + source_only: number + accepted: number + accepted_at: string + start_at: string + paid_until: string +} + +export interface ProjectDetail extends ProjectItem { + stages: ProjectStage[] + pays: ProjectPay[] + maintain: ProjectMaintain | null + can_accept: number +} + +/** 确认验收:进入维护期(维护期起点=确认当天,后台可调整) */ +export function apiProjectAccept(id: number) { + return post<{ maintain_start_at: string, paid_until: string }>('/service/project/accept', { id }) +} diff --git a/src/api/quota.ts b/src/api/quota.ts index 61eeef5..d69c06b 100644 --- a/src/api/quota.ts +++ b/src/api/quota.ts @@ -108,7 +108,13 @@ export function apiQuotaPacks(toolKey?: string) { /** 建单(第一步):只落一条待支付次数包订单,**不返回支付参数** */ export function apiQuotaOrderCreate(packKey: string, toolKey: string) { - return post('/quota/order', { pack_key: packKey, tool_key: toolKey }) + let platform = '' + try { + platform = uni.getSystemInfoSync().platform || '' + } catch { + platform = '' + } + return post('/quota/order', { pack_key: packKey, tool_key: toolKey, platform }) } /** diff --git a/src/api/request.ts b/src/api/request.ts index 568586e..bc91e0a 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -79,17 +79,40 @@ export function gotoH5Login() { // #endif } +/** + * GET 查询参数净化:剥离 undefined / null。 + * + * ⚠️ 跨端差异(真踩过):微信 `wx.request` 对 GET 的 data 对象做**字符串化**序列化, + * `undefined` 会变成字面量 `"undefined"` 随请求发出(如 `?keyword=undefined`); + * H5 端的 shim 则会跳过。于是「可选参数留空」在 H5 正常、在 MP 变成按 + * 「undefined」关键词过滤(笔记列表因此恒为空)。统一在此剥离,杜绝该类差异。 + */ +function sanitizeParams(params?: Record): Record | undefined { + if (!params) + return params + const out: Record = {} + for (const k of Object.keys(params)) { + const v = params[k] + if (v !== undefined && v !== null) + out[k] = v + } + return out +} + function request(url: string, method: 'GET' | 'POST' | 'PUT', data?: unknown, opts?: { silent?: boolean }): Promise { const header: Record = {} const token = getToken() if (token) header.Authorization = `Bearer ${token}` + // GET 的 data 会被端侧序列化成 query,先剥掉 undefined/null(见 sanitizeParams 注释) + const payload = method === 'GET' ? sanitizeParams(data as Record | undefined) : data + return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, - data: data as Record | undefined, + data: payload, header, timeout: 15000, success: (res) => { diff --git a/src/api/service.ts b/src/api/service.ts new file mode 100644 index 0000000..865096e --- /dev/null +++ b/src/api/service.ts @@ -0,0 +1,168 @@ +import { get, post } from '@/api/request' + +/** + * 软件开发服务接口封装(对应后端 `api/user/v1/goods.go`,走 UserAuth 组,需登录态)。 + * 契约要点(与后端 `g.Meta` 的 path/method **逐字对齐**,改后端务必同步此处): + * - 列表/详情均只返回上架(is_enabled=1)商品; + * - `cover_url` 已由后端按 settings 的 site.base_url 拼成绝对地址,前端拿到直接用于 ; + * - `content_md` 为 Markdown 原文,本端用 `utils/md-parser.ts` 解析成 rich-text 节点渲染。 + */ +export const SERVICE_CATEGORY_WEBSITE = 1 +export const SERVICE_CATEGORY_MINI_PROGRAM = 2 +export const SERVICE_CATEGORY_APP = 3 +export const SERVICE_CATEGORY_DESKTOP = 4 + +/** 分类筛选项(列表页 tabs;0 = 全部) */ +export const SERVICE_CATEGORY_TABS = [ + { value: 0, label: '全部' }, + { value: 1, label: '网站开发' }, + { value: 2, label: '小程序开发' }, + { value: 3, label: 'App 开发' }, + { value: 4, label: '桌面程序开发' }, +] as const + +export interface ServiceItem { + id: number + category: number + category_name: string + title: string + summary: string + cover_url: string + price_cents: number + price_note: string +} + +export interface ServiceListData { + list: ServiceItem[] + total: number +} + +export interface ServiceDetail extends ServiceItem { + content_md: string + updated_at: string +} + +export interface ServiceListQuery { + category?: number + page?: number + pageSize?: number +} + +/** 定价展示:分 → 「¥2999」;0 = 面议(返回空串由调用方兜底) */ +export function servicePriceText(priceCents: number, priceNote = ''): string { + if (!priceCents) + return '面议' + const yuan = priceCents % 100 === 0 ? String(priceCents / 100) : (priceCents / 100).toFixed(2) + return `¥${yuan}${priceNote || ''}` +} + +/** 服务列表(仅上架;sort 升序) */ +export function apiServiceList(query: ServiceListQuery = {}) { + return get('/goods/list', { + category: query.category || undefined, + page: query.page, + pageSize: query.pageSize, + }) +} + +/** 服务详情(仅上架;下架/不存在时抛错 message「服务不存在或已下架」) */ +export function apiServiceDetail(id: number) { + return get('/goods/detail', { id }) +} + +// ============================================================================ +// 服务介绍页 / 作品案例 / 报价申请(迭代-2026-09-30,对应后端 api/user/v1/service.go) +// ============================================================================ + +/** 介绍页模块(kind 决定渲染版式:hero/rich/image_text/image_list/developer/works/goods) */ +export interface ServiceBlock { + id: number + kind: string + title: string + subtitle: string + image_url: string + images: string[] + content: string +} + +/** 服务介绍页模块列表(仅启用,sort 升序;图片为后端拼好的绝对 URL) */ +export function apiServicePage() { + return get<{ list: ServiceBlock[] }>('/service/page') +} + +export interface WorkItem { + id: number + category: number + category_name: string + title: string + summary: string + cover_url: string + price_cents: number + price_note: string +} + +export interface WorkDetail extends WorkItem { + images: string[] + content_md: string + updated_at: string +} + +/** 作品列表(仅上架) */ +export function apiServiceWorks(query: ServiceListQuery = {}) { + return get<{ list: WorkItem[], total: number }>('/service/works', { + category: query.category || undefined, + page: query.page, + pageSize: query.pageSize, + }) +} + +/** 作品详情(仅上架;下架/不存在时抛错) */ +export function apiServiceWorkDetail(id: number) { + return get('/service/works/detail', { id }) +} + +/** 报价单状态 → 主题色(与订单状态色板一致) */ +export function quoteStatusTone(status: number): string { + switch (status) { + case 1: return '#b57f00' // 已报价 + case 2: return '#0fa385' // 已成交 + case 3: return '#9a7b65' // 已关闭 + default: return '#e8531a' // 待处理 + } +} + +export interface QuoteSubmitData { + work_id?: number + contact: string + requirement: string + budget?: string +} + +/** 提交报价申请(成功后服务端会发一条站内信回执) */ +export function apiQuoteSubmit(data: QuoteSubmitData) { + return post<{ id: number }>('/service/quote/submit', { + work_id: data.work_id || 0, + contact: data.contact, + requirement: data.requirement, + budget: data.budget || '', + }) +} + +export interface QuoteItem { + id: number + work_id: number + work_title: string + requirement: string + budget: string + status: number + status_text: string + quote_cents: number + quote_note: string + handled_at: string + created_at: string +} + +/** 我的报价申请(最近 N 条,含处理结果) */ +export function apiQuoteMine(limit = 20) { + return get<{ list: QuoteItem[] }>('/service/quote/mine', { limit }) +} diff --git a/src/api/user.ts b/src/api/user.ts index 7f88de5..2f6e1ea 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -100,3 +100,24 @@ export function apiWorkbenchSave(toolKeys: string[]) { export function apiFeedbackSubmit(content: string, contact = '') { return post('/feedback/submit', { content, contact }) } + +/** 我的反馈条目(含官方回复) */ +export interface FeedbackMineItem { + id: number + content: string + status: number + reply_content: string + reply_at: string + reply_read: number + created_at: string +} + +/** 我的反馈(最近 N 条 + 未读官方回复数;不会自动置已读) */ +export function apiFeedbackMine(limit = 20) { + return get<{ list: FeedbackMineItem[], unread: number }>('/feedback/mine', { limit }) +} + +/** 反馈回复标记已读(清红点) */ +export function apiFeedbackRead() { + return post('/feedback/read', {}) +} diff --git a/src/components/BackFab.vue b/src/components/BackFab.vue new file mode 100644 index 0000000..de2a368 --- /dev/null +++ b/src/components/BackFab.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/components/TabBar.vue b/src/components/TabBar.vue index 1742512..27c9221 100644 --- a/src/components/TabBar.vue +++ b/src/components/TabBar.vue @@ -99,14 +99,25 @@ function onTap(key: string) { display: flex; align-items: center; padding: 14rpx 28rpx; - background: rgba(255, 255, 255, 0.96); + /* 液态玻璃 dock:半透明白 + backdrop 模糊(悬浮在页面内容与氛围光斑之上) */ + background: var(--tb-glass-bg-strong, rgba(255, 255, 255, 0.68)); + border: 1rpx solid rgba(255, 255, 255, 0.7); + -webkit-backdrop-filter: blur(28rpx) saturate(1.8); + backdrop-filter: blur(28rpx) saturate(1.8); border-radius: 999rpx; - box-shadow: 0 18rpx 50rpx rgba(107, 47, 15, 0.18), 0 2rpx 8rpx rgba(107, 47, 15, 0.06); + box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92), 0 18rpx 50rpx rgba(107, 47, 15, 0.18), + 0 2rpx 8rpx rgba(107, 47, 15, 0.06); /* 凹槽:在 dock 顶部中央「咬」出一个圆弧缺口,中心键从中凸起 (mask 同时裁掉阴影,缺口轮廓会带出阴影形状;不支持 mask 的环境退化为普通胶囊) */ -webkit-mask: radial-gradient(circle 80rpx at 50% 0, transparent 96%, #000 100%); mask: radial-gradient(circle 80rpx at 50% 0, transparent 96%, #000 100%); } +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .tb-tabbar__dock { + background: rgba(255, 255, 255, 0.96); + border-color: rgba(255, 255, 255, 0.9); + } +} .tb-tabbar__item { flex: 1; display: flex; diff --git a/src/components/card/CardTile.vue b/src/components/card/CardTile.vue index d72de13..a3d112f 100644 --- a/src/components/card/CardTile.vue +++ b/src/components/card/CardTile.vue @@ -498,6 +498,26 @@ const effArtPos = computed<'top' | 'rightBottom'>(() => color: rgba(255, 255, 255, 0.88); padding-right: 88rpx; } +/* duo 版式 × glass 皮肤:玻璃底替代强渐变(0,3,0 压过 .ct.ct--duo 的 0,2,0), + 白字随之换深字,保证半透明底上的对比度;外阴影同样中性(不用彩色光晕) */ +.ct.ct--duo.ct--skin-glass { + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.42)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.55)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); + backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); + box-shadow: + inset 0 0 0 1rpx rgba(255, 255, 255, 0.4), + inset 0 2rpx 10rpx rgba(255, 255, 255, 0.8), + inset 0 -1rpx 0 rgba(255, 255, 255, 0.4), + 0 2rpx 8rpx rgba(80, 45, 20, 0.06), + 0 16rpx 40rpx rgba(80, 45, 20, 0.12); +} +.ct.ct--duo.ct--skin-glass .ct__title { + color: #3d2a1e; +} +.ct.ct--duo.ct--skin-glass .ct__desc { + color: #6b4f3a; +} .ct__art--duo { margin-bottom: 8rpx; } @@ -506,12 +526,15 @@ const effArtPos = computed<'top' | 'rightBottom'>(() => 白底容器 + 区块标题 + 右上「更多」角标 + 子卡网格(子卡由默认插槽渲染)。 特异性 0,2,0(.ct.ct--group)> 皮肤 0,1,0 → 恒为白底,不被主题皮肤染色。 */ .ct.ct--group { - background: #ffffff; - border: 1rpx solid rgba(255, 195, 155, 0.55); + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.42)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.55)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); + backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); /* 容器尺寸基准也走变量:与尺寸档同特异度时靠**源序**(尺寸档在后)让窄列尺寸档胜出(R-2) */ --ct-radius: 34rpx; --ct-pad: 26rpx 24rpx 28rpx; - box-shadow: 0 3rpx 10rpx rgba(107, 47, 15, 0.05), 0 16rpx 38rpx rgba(107, 47, 15, 0.1); + box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92), inset 0 -1rpx 0 rgba(255, 255, 255, 0.4), + 0 16rpx 40rpx rgba(80, 45, 20, 0.12); gap: 20rpx; align-items: stretch; } @@ -534,9 +557,10 @@ const effArtPos = computed<'top' | 'rightBottom'>(() => align-items: center; gap: 12rpx; min-width: 0; - background: rgba(255, 255, 255, 0.86); + background: rgba(255, 255, 255, 0.8); border-radius: 999rpx; padding: 4rpx 16rpx; + -webkit-backdrop-filter: blur(6rpx); backdrop-filter: blur(6rpx); } .ct__headtitle { @@ -620,14 +644,21 @@ const effArtPos = computed<'top' | 'rightBottom'>(() => var(--ct-tint); box-shadow: 0 10rpx 24rpx var(--ct-shadow); } -/* glass:半透明 + 1rpx 白描边 + 内高光 */ +/* glass:液态玻璃——半透明白 + backdrop blur/saturate + 玻璃环高光。 + 外阴影只用中性暖棕(**不用 var(--ct-shadow) 彩色光晕**:那是「AI 渐变光」的来源, + Apple 玻璃的立体感来自中性阴影 + 顶部高光 + 底部折射亮边)。 + token 与 App.vue 的 --tb-glass-* 同源(page 上定义,跨组件继承;带兜底值)。 */ .ct--skin-glass { - background: rgba(255, 255, 255, 0.55); - border: 1rpx solid rgba(255, 255, 255, 0.9); + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.42)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.55)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); + backdrop-filter: blur(var(--tb-glass-blur, 36rpx)) saturate(var(--tb-glass-saturate, 1.9)); box-shadow: - inset 0 2rpx 8rpx rgba(255, 255, 255, 0.85), - 0 8rpx 22rpx rgba(107, 47, 15, 0.1); - backdrop-filter: blur(10rpx); + inset 0 0 0 1rpx rgba(255, 255, 255, 0.4), + inset 0 2rpx 10rpx rgba(255, 255, 255, 0.8), + inset 0 -1rpx 0 rgba(255, 255, 255, 0.4), + 0 2rpx 8rpx rgba(80, 45, 20, 0.06), + 0 16rpx 40rpx rgba(80, 45, 20, 0.12); } /* solid:饱和渐变 + 釉面高光 + 彩色投影(不是灰黑) */ .ct--skin-solid { diff --git a/src/components/home/PaneMine.vue b/src/components/home/PaneMine.vue index 9c6973f..1262a52 100644 --- a/src/components/home/PaneMine.vue +++ b/src/components/home/PaneMine.vue @@ -15,6 +15,9 @@ import CountUp from '@/components/CountUp.vue' import PaneRefresher from '@/components/home/PaneRefresher.vue' import Sticker from '@/components/Sticker.vue' import { FEATURE_ENTRIES } from '@/config/features' +import { useFeedbackStore } from '@/stores/feedback' +import { useMessageStore } from '@/stores/message' +import { useQuotaStore } from '@/stores/quota' import { useToolsStore } from '@/stores/tools' import { useUserStore } from '@/stores/user' import { presetOf } from '@/utils/avatar' @@ -26,6 +29,9 @@ const emit = defineEmits<{ feedback: [], bind: [], cardstyle: [] }>() const toolsStore = useToolsStore() const userStore = useUserStore() +const feedbackStore = useFeedbackStore() +const quotaStore = useQuotaStore() +const messageStore = useMessageStore() /** 「我的」页菜单行图标块配色(回滚:还原改造前的 CELL_BG/CELL_COLOR 语义) */ const CELL_ICON = { @@ -49,13 +55,19 @@ const heroPad = heroPadTop() let loadedOnce = false -/** 我的域刷新:登录态 + 等级 + 工具表(**不含额度**) */ +/** 我的域刷新:登录态 + 等级 + 工具表(**不含额度**)+ 反馈未读数(红点) */ async function load() { try { await userStore.ensureLogin() await Promise.all([ toolsStore.fetchAll().catch(() => {}), userStore.refreshLevel().catch(() => {}), + // 会员用户卡需要 is_member 标记(/quota/my 很轻,失败静默走非会员卡片); + // 站内信未读数(金刚区徽标)同样仅登录后拉 + ...(userStore.logged ? [quotaStore.fetchMine().catch(() => {})] : []), + ...(userStore.logged ? [messageStore.fetchUnread()] : []), + // 已登录才拉反馈未读(未登录时接口 401,会触发全局 toast) + ...(userStore.logged ? [feedbackStore.fetchMine().catch(() => {})] : []), ]) } catch { @@ -77,9 +89,16 @@ watch(() => props.active, (v) => { defineExpose({ refresh }) const levelName = computed(() => userStore.level?.name || '未授权') +/** 是否会员(quota 总览的 is_member 标记,工作台/我的任一 pane 加载后即有值) */ +const isMember = computed(() => quotaStore.isMember) +/** 已解锁的大类名(toolsStore.modules 即大类,锁定态过滤) */ const unlockedModules = computed(() => toolsStore.modules.filter(m => !m.locked).map(m => m.name), ) +/** 已解锁大类一行文案(R6:不再逐个 chip 铺开,合并成一行更清爽) */ +const unlockedText = computed(() => + unlockedModules.value.length ? unlockedModules.value.join(' · ') : '暂无已解锁大类', +) const stats = computed(() => [ { label: '工作台工具', value: toolsStore.workbenchKeys.length }, @@ -164,16 +183,54 @@ function goMember() { uni.navigateTo({ url: '/pkg-member/member/index' }) } -/** 我的订单 */ -function goOrders() { - uni.navigateTo({ url: '/pkg-member/member/orders' }) +/** 我的订单(金刚区 / 订单入库统一入口,tab: 0全部 1待付款 2已生效 3退款) */ +function goOrders(tab = 0) { + uni.navigateTo({ url: `/pkg-member/member/orders?tab=${tab}` }) } -/** 我的优惠券 */ +/** 我的优惠券(金刚区) */ function goCoupons() { uni.navigateTo({ url: '/pkg-member/coupon/index' }) } +/** 我的报价(软件开发服务域) */ +function goQuotes() { + uni.navigateTo({ url: '/pkg-service/quotes' }) +} + +/** 消息中心(站内信) */ +function goMessages() { + uni.navigateTo({ url: '/pkg-msg/index' }) +} + +/** 我的项目(开发项目进度跟踪) */ +function goProjects() { + uni.navigateTo({ url: '/pkg-service/projects' }) +} + +/** 服务与价格列表 */ +function goGoodsList() { + uni.navigateTo({ url: '/pkg-service/list' }) +} + +/** 订单入库四态(R4):与订单页 tab 一一对应 */ +const ORDER_TABS = [ + { tab: 0, label: '全部', icon: 'list', bg: '#FFE8DC', color: '#E8531A' }, + { tab: 1, label: '待付款', icon: 'clock', bg: '#FFF4D6', color: '#B57F00' }, + { tab: 2, label: '已生效', icon: 'check-circle', bg: '#DFF7F0', color: '#0FA385' }, + { tab: 3, label: '退款', icon: 'rollback', bg: '#E3F2FF', color: '#1677D9' }, +] as const + +/** 金刚区宫格(R2 + 三期):消息中心带未读徽标;第二行放项目与服务价格 */ +const QUICK_ENTRIES = computed(() => [ + { key: 'messages', label: '消息中心', icon: 'notification', bg: '#FFF1E8', color: '#E8531A', action: goMessages, badge: messageStore.unread }, + { key: 'quotes', label: '我的报价', icon: 'edit-outline', bg: '#DFF7F0', color: '#0FA385', action: goQuotes, badge: 0 }, + { key: 'projects', label: '我的项目', icon: 'app', bg: '#ECE2FF', color: '#5B2E9E', action: goProjects, badge: 0 }, + { key: 'orders', label: '我的订单', icon: 'list', bg: '#E3F2FF', color: '#1677D9', action: () => goOrders(0), badge: 0 }, + { key: 'coupons', label: '优惠券', icon: 'discount-filled', bg: '#FFF4D6', color: '#B57F00', action: goCoupons, badge: 0 }, + { key: 'goods', label: '服务价格', icon: 'computer', bg: '#FFE8DC', color: '#E8531A', action: goGoodsList, badge: 0 }, +]) + function logout() { uni.showModal({ title: '退出登录', @@ -206,27 +263,32 @@ function logout() { - - + + + + + VIP + - + - + {{ displayName }} - - {{ levelName }} - - - {{ m }} + + {{ isMember ? `${levelName}会员` : levelName }} + + + {{ unlockedText }} + - + @@ -242,8 +304,58 @@ function logout() { - - + + + + + + + {{ q.badge > 99 ? '99+' : q.badge }} + + + + {{ q.label }} + + + + + + + + + 我的订单 + + + 全部订单 + + + + + + + + + + {{ t.label }} + + + + + + + @@ -277,32 +389,6 @@ function logout() { - - - - - - 我的订单 - - - 查看购买记录 - - - - - - - - - - 我的优惠券 - - - 免单券可抵扣 - - - - @@ -339,6 +425,10 @@ function logout() { 意见反馈 + + {{ feedbackStore.unread }} 条新回复 + + @@ -398,12 +488,34 @@ function logout() { padding: 32rpx 28rpx; box-shadow: 0 16rpx 40rpx rgba(151, 60, 8, 0.28); } +/* 会员特殊用户卡:深炭金卡(与会员中心/金币卡同一套语言),半透明深色玻璃 */ +.user--vip { + background: linear-gradient(150deg, rgba(255, 233, 184, 0.2) 0%, rgba(255, 255, 255, 0.05) 55%), linear-gradient(160deg, #2b1a10 0%, #4a2c16 55%, #2b1a10 100%); + -webkit-backdrop-filter: blur(24rpx) saturate(1.4); + backdrop-filter: blur(24rpx) saturate(1.4); + border: 2rpx solid rgba(245, 197, 107, 0.55); + box-shadow: inset 0 1rpx 0 rgba(255, 246, 224, 0.28), 0 18rpx 44rpx rgba(20, 10, 4, 0.4); +} .user__sticker { position: absolute; right: 16rpx; top: 12rpx; opacity: 0.9; } +/* 会员右上角 VIP 徽标(替代贴纸位) */ +.user__vip-badge { + position: absolute; + right: 24rpx; + top: 20rpx; + padding: 6rpx 18rpx; + border-radius: 999rpx; + background: linear-gradient(140deg, #ffe9b8, #ffd77a); + color: #8a5a00; + font-size: 20rpx; + font-weight: 800; + letter-spacing: 0.08em; + box-shadow: 0 6rpx 14rpx rgba(180, 127, 0, 0.3); +} .user__avatar { flex-shrink: 0; width: 112rpx; @@ -416,21 +528,35 @@ function logout() { overflow: hidden; box-shadow: 0 10rpx 22rpx rgba(107, 47, 15, 0.14); } +.user--vip .user__avatar { + border-color: rgba(245, 197, 107, 0.75); +} .user__img { width: 100%; height: 100%; } +.user__body { + flex: 1; + min-width: 0; +} .user__name { display: block; font-size: 34rpx; font-weight: 800; color: #3d2a1e; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.user__name--vip { + color: #ffe9b8; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.25); } .user__tags { display: flex; flex-wrap: wrap; gap: 12rpx; - margin-top: 14rpx; + margin-top: 12rpx; } .user__level { background: linear-gradient(140deg, #ff9f4d, #ff6b35); @@ -441,13 +567,26 @@ function logout() { border-radius: 999rpx; box-shadow: 0 6rpx 14rpx rgba(251, 78, 20, 0.3); } -.user__module { - background: #fff1e8; - border: 2rpx solid #ffd4b8; - color: #e8531a; +.user__level--vip { + background: linear-gradient(140deg, #ffe9b8, #f5c56b); + color: #6b4400; + box-shadow: 0 6rpx 14rpx rgba(180, 127, 0, 0.28); +} +/* 已解锁大类:合并为一行(R6) */ +.user__unlocked { + display: block; + margin-top: 12rpx; font-size: 22rpx; - padding: 6rpx 20rpx; - border-radius: 999rpx; + line-height: 1.5; + color: #9a7b65; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} +.user__unlocked--vip { + color: rgba(255, 233, 184, 0.78); } /* 数据总览(回滚:白卡内的三段统计) */ @@ -476,11 +615,96 @@ function logout() { color: #9a7b65; } +/* 金刚区(宫格):四列图标入口 */ +.quick { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 18rpx; + padding: 28rpx 20rpx; + margin-bottom: 24rpx; +} +.quick__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 12rpx; +} +.quick__icon { + position: relative; + width: 88rpx; + height: 88rpx; + border-radius: 26rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.9), 0 6rpx 14rpx rgba(107, 47, 15, 0.1); +} +.quick__badge { + position: absolute; + top: -8rpx; + right: -10rpx; + min-width: 30rpx; + height: 30rpx; + padding: 0 8rpx; + border-radius: 999rpx; + background: #ff4d1f; + border: 2rpx solid #ffffff; + color: #ffffff; + font-size: 18rpx; + font-weight: 700; + line-height: 26rpx; + text-align: center; + box-sizing: border-box; +} +.quick__label { + font-size: 23rpx; + color: #3d2a1e; + font-weight: 500; +} + +/* 订单入库(四态直达) */ +.orderq { + padding: 28rpx; + margin-bottom: 28rpx; +} +.orderq__head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24rpx; +} +.orderq__row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 18rpx; +} +.orderq__item { + display: flex; + flex-direction: column; + align-items: center; + gap: 12rpx; +} +.orderq__icon { + width: 80rpx; + height: 80rpx; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 2rpx 4rpx rgba(255, 255, 255, 0.9), 0 6rpx 14rpx rgba(107, 47, 15, 0.1); +} +.orderq__label { + font-size: 23rpx; + color: #3d2a1e; + font-weight: 500; +} + /* 菜单(回滚:白卡 + 行列表,无卡片风格) */ .cells { padding: 8rpx 0; } .cell { + position: relative; display: flex; align-items: center; gap: 22rpx; @@ -517,6 +741,22 @@ function logout() { color: #9a7b65; margin-right: 8rpx; } +/* 意见反馈:新回复提醒(N 条新回复 + 红点) */ +.cell__value--dot { + color: #ff6b35; + font-weight: 600; +} +.cell__red-dot { + position: absolute; + top: 50%; + margin-top: -34rpx; + right: 44rpx; + width: 14rpx; + height: 14rpx; + border-radius: 50%; + background: #ff4d1f; + box-shadow: 0 0 0 4rpx rgba(255, 107, 53, 0.18); +} .logout { margin-top: 40rpx; diff --git a/src/components/home/PaneWorkbench.vue b/src/components/home/PaneWorkbench.vue index 6639cb8..8ea9fcc 100644 --- a/src/components/home/PaneWorkbench.vue +++ b/src/components/home/PaneWorkbench.vue @@ -7,14 +7,19 @@ * ——工作台需要展示额度,这是三 pane 里唯一会打 /quota/my 的域。 */ import { computed, ref, watch } from 'vue' +import { apiAppConfig } from '@/api/app' import { apiToolsUsed } from '@/api/user' import CardTile from '@/components/card/CardTile.vue' import EmptyState from '@/components/EmptyState.vue' import PaneRefresher from '@/components/home/PaneRefresher.vue' import VipCard from '@/components/vip/VipCard.vue' -import { FEATURE_ENTRIES } from '@/config/features' +import type { ProjectItem } from '@/api/project' +import { apiProjectMine } from '@/api/project' +import { CARD_TONES } from '@/config/card-styles' +import { FEATURE_ENTRIES, type FeatureEntry } from '@/config/features' import { TOOL_REGISTRY } from '@/config/tools' import { useCardStyleStore } from '@/stores/cardStyle' +import { useMessageStore } from '@/stores/message' import { useQuotaStore } from '@/stores/quota' import { useToolsStore } from '@/stores/tools' import { useUserStore } from '@/stores/user' @@ -30,6 +35,7 @@ const toolsStore = useToolsStore() const userStore = useUserStore() const quotaStore = useQuotaStore() const cardStyle = useCardStyleStore() +const messageStore = useMessageStore() const { refreshing, bubbleScale, onPulling, onRestore, run } = usePaneRefresh() @@ -40,12 +46,15 @@ const heroPad = heroPadTop() const ready = ref(false) let loadedOnce = false -/** 工作台域刷新:工具表 + 工作台 + 额度(额度为工作台独有) */ +/** 工作台域刷新:工具表 + 工作台 + 额度(额度为工作台独有)+ 功能卡背景图(fire-and-forget) */ async function load() { try { await userStore.ensureLogin() await toolsStore.fetchAll() await quotaStore.fetchMine().catch(() => {}) + // 站内信未读数(首页消息入口徽标):登录后才拉,失败静默 + if (userStore.logged) + messageStore.fetchUnread() } catch { /* 静默:展示空态 */ @@ -53,6 +62,30 @@ async function load() { finally { ready.value = true } + // 功能卡背景图:后台系统配置下发(/app/config,api/app.ts 内有进程缓存), + // 失败静默走色系渐变兜底,不打扰首页 + apiAppConfig().then((d) => { + covers.value = d.feature_covers || {} + }).catch(() => {}) + // 进行中项目快捷入口(fire-and-forget,失败静默) + pickActiveProject() +} + +/** 进行中的开发项目(首页快捷入口):取「我的项目」里第一个进行中的 */ +const activeProject = ref(null) + +function pickActiveProject() { + if (!userStore.logged) { + activeProject.value = null + return + } + apiProjectMine() + .then((res) => { + activeProject.value = (res.list || []).find(p => p.status === 1) || null + }) + .catch(() => { + /* 静默:入口下次刷新再出 */ + }) } /** 供首页 onShow 调用 */ @@ -107,6 +140,19 @@ function goEdit() { uni.navigateTo({ url: '/pages/workbench/edit' }) } +/** 消息中心(站内信):首页消息入口 */ +function goMessages() { + uni.navigateTo({ url: '/pkg-msg/index' }) +} + +/** 进行中项目详情 */ +function goProject(id: number) { + uni.navigateTo({ url: `/pkg-service/project-detail?id=${id}` }) +} + +/** 徽标数字:超过 99 显示 99+ */ +const messageBadge = computed(() => (messageStore.unread > 99 ? '99+' : messageStore.unread > 0 ? String(messageStore.unread) : '')) + /** * 功能入口(笔记 / 年会查座)跳转:入口来源 `config/features.ts`,**不走** TOOL_REGISTRY * (后者会被后端 tools 表判 locked 而静默锁死,见 features.ts 顶部注释)。 @@ -116,6 +162,27 @@ function goFeature(path: string) { uni.navigateTo({ url: path }) } +// ===== 便捷功能自适应布局(迭代-2026-09-30)===== +// ≤2 张:双列 CardTile(原排版);=3 张:「1 大 2 小」拼图(图2 参考样式,主题化); +// ≥4 张:横滑图片卡(图3 参考样式)。背景图来自后台「系统配置 → 首页功能卡」 +//(settings home.feature.covers,经 /app/config 下发绝对 URL),未配置用色系渐变兜底。 +const covers = ref>({}) + +const featsMode = computed(() => + FEATURE_ENTRIES.length >= 4 ? 'scroll' : FEATURE_ENTRIES.length === 3 ? 'mosaic' : 'grid', +) + +/** 功能卡背景图(绝对 URL;空 = 未配置) */ +function coverOf(fe: FeatureEntry): string { + return covers.value[fe.key] || '' +} + +/** 未配置背景图时的色系渐变兜底(配色铁律:只动质感不动色相) */ +function fallbackStyle(fe: FeatureEntry): string { + const tone = CARD_TONES.find(t => t.id === fe.tone) || CARD_TONES[0] + return `background: linear-gradient(140deg, ${tone.gradFrom}, ${tone.gradTo})` +} + const SUB_STICKERS = ['cloud', 'rainbow', 'bolt'] as const const HOT_STICKERS = ['sparkle', 'bolt', 'cloud', 'star', 'rocket', 'rainbow'] as const @@ -180,6 +247,13 @@ function leadCard(key: string) { {{ userStore.level.name }} + + + + + {{ messageBadge }} + + 工具百宝箱 @@ -199,15 +273,68 @@ function leadCard(key: string) { - - + + + + + + + + + {{ activeProject.stage_done }}/{{ activeProject.stage_total }} + + + + + + {{ activeProject.title }} + + + 进行中 + + + + + + + + + + 阶段进度 {{ activeProject.stage_done }}/{{ activeProject.stage_total }} + + + 交付周期 {{ activeProject.due_note }} + + + + + + + 便捷功能 - + + + + + + + + + + + + + {{ FEATURE_ENTRIES[0].name }} + + + + {{ FEATURE_ENTRIES[0].desc }} + + + + + + + + + + + + + {{ fe.name }} + + + + {{ fe.desc }} + + + + + + + + + + + + + + + + + + {{ fe.name }} + + + {{ fe.desc }} + + + + @@ -365,6 +569,36 @@ function leadCard(key: string) { padding: 4rpx 18rpx; border-radius: 999rpx; } +/* 消息入口:白色玻璃圆钮 + 未读徽标 */ +.hero__msg { + position: relative; + margin-left: auto; + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.22); + border: 1rpx solid rgba(255, 255, 255, 0.5); + display: flex; + align-items: center; + justify-content: center; +} +.hero__msg-badge { + position: absolute; + top: -8rpx; + right: -10rpx; + min-width: 30rpx; + height: 30rpx; + padding: 0 8rpx; + border-radius: 999rpx; + background: #ff4d1f; + border: 2rpx solid #ffffff; + color: #ffffff; + font-size: 18rpx; + font-weight: 700; + line-height: 26rpx; + text-align: center; + box-sizing: border-box; +} .hero__title { display: block; margin-top: 12rpx; @@ -378,12 +612,120 @@ function leadCard(key: string) { margin-top: 30rpx; } +/* 进行中项目快捷入口 */ +.proj { + display: flex; + gap: 22rpx; + padding: 22rpx; + margin-top: 28rpx; +} +.proj__cover { + position: relative; + flex-shrink: 0; + width: 190rpx; + height: 150rpx; + border-radius: 20rpx; + overflow: hidden; + background: #fff3e9; + border: 1rpx solid #ffe9dd; +} +.proj__img { + width: 100%; + height: 100%; + /* 同上:图片自带圆角,不依赖容器裁切 */ + border-radius: 20rpx; +} +.proj__img--ph { + display: flex; + align-items: center; + justify-content: center; +} +.proj__badge { + position: absolute; + top: 0; + right: 0; + padding: 4rpx 16rpx; + border-radius: 0 20rpx 0 18rpx; + background: linear-gradient(140deg, #ff7a3d, #fb4e14); + color: #ffffff; + font-size: 19rpx; + font-weight: 800; + box-shadow: 0 4rpx 10rpx rgba(251, 78, 20, 0.3); +} +.proj__body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + justify-content: center; + gap: 12rpx; +} +.proj__top { + display: flex; + align-items: center; + gap: 14rpx; +} +.proj__title { + flex: 1; + min-width: 0; + font-size: 29rpx; + font-weight: 800; + color: #3d2a1e; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.proj__status { + flex-shrink: 0; + padding: 4rpx 16rpx; + border-radius: 999rpx; + background: #fff1e8; + border: 1rpx solid #ffd4b8; + color: #e8531a; + font-size: 19rpx; + font-weight: 700; +} +.proj__progress { + display: flex; +} +.proj__track { + flex: 1; + height: 12rpx; + border-radius: 999rpx; + background: #fff1e8; + border: 1rpx solid #ffe3d3; + overflow: hidden; +} +.proj__fill { + height: 100%; + border-radius: 999rpx; + background: linear-gradient(90deg, #ff9f4d, #fb4e14); +} +.proj__meta { + display: flex; + align-items: center; + gap: 18rpx; +} +.proj__stage { + font-size: 21rpx; + font-weight: 700; + color: #fb4e14; +} +.proj__due { + font-size: 20rpx; + color: #9a7b65; +} + .sec { padding: 30rpx 28rpx 32rpx; } .sec--gap { margin-top: 28rpx; } +/* frameless 便捷功能(用户点名:玻璃壳在大图卡下显脏):去壳后贴齐页面栅格 */ +.sec--flat { + padding: 8rpx 0 0; +} .sec__head { display: flex; align-items: center; @@ -427,7 +769,7 @@ function leadCard(key: string) { min-width: 0; } -/* 便捷功能:两列 */ +/* 便捷功能:两列(≤2 张时的原排版) */ .feats { display: grid; grid-template-columns: repeat(2, 1fr); @@ -435,6 +777,144 @@ function leadCard(key: string) { margin-top: 20rpx; } +/* 便捷功能:=3 张的「1 大 2 小」拼图(参考图2,主题化) */ +.fm { + display: grid; + grid-template-columns: 1.15fr 1fr; + gap: 16rpx; + height: 380rpx; + margin-top: 20rpx; +} +.fm__main { + position: relative; + overflow: hidden; + border-radius: 26rpx; + background: #fff3e9; + box-shadow: 0 10rpx 24rpx rgba(151, 60, 8, 0.16); +} +.fm__side { + display: flex; + flex-direction: column; + gap: 16rpx; + min-width: 0; +} +.fm__sub { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + border-radius: 26rpx; + background: #fff3e9; + box-shadow: 0 8rpx 18rpx rgba(151, 60, 8, 0.12); +} +.fm__bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + /* 与容器圆角同值:MP 端 overflow:hidden 裁切 image 子元素不可靠, + 必须让图片/遮罩自己带圆角,否则「图片圆角、容器方角」穿帮(用户点名) */ + border-radius: 26rpx; +} +.fm__shade { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 26rpx; + background: linear-gradient(180deg, rgba(40, 20, 6, 0.02) 34%, rgba(40, 20, 6, 0.56) 100%); +} +.fm__body { + position: absolute; + left: 20rpx; + right: 20rpx; + bottom: 16rpx; + display: flex; + flex-direction: column; + gap: 10rpx; +} +.fm__title { + color: #ffffff; + font-size: 32rpx; + font-weight: 800; + letter-spacing: 0.01em; + text-shadow: 0 2rpx 10rpx rgba(60, 25, 5, 0.45); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.fm__title--sm { + font-size: 27rpx; +} +.fm__pill { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: 4rpx; + background: rgba(255, 255, 255, 0.94); + border-radius: 999rpx; + padding: 6rpx 16rpx; + box-shadow: 0 4rpx 12rpx rgba(60, 25, 5, 0.18); +} +.fm__pill-text { + color: #e8531a; + font-size: 20rpx; + font-weight: 700; + max-width: 240rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 便捷功能:≥4 张的横滑图片卡(参考图3,主题化) */ +.fs { + width: 100%; + margin-top: 20rpx; +} +.fs__row { + display: inline-flex; + gap: 16rpx; + padding: 4rpx 2rpx 10rpx; +} +.fs__card { + display: inline-flex; + flex-direction: column; + width: 300rpx; + background: rgba(255, 255, 255, 0.94); + border: 1rpx solid #ffe9dd; + border-radius: 24rpx; + overflow: hidden; + box-shadow: 0 8rpx 18rpx rgba(151, 60, 8, 0.10); +} +.fs__imgwrap { + height: 220rpx; + background: #fff3e9; +} +.fs__img { + width: 100%; + height: 100%; + display: block; +} +.fs__title { + margin: 14rpx 16rpx 0; + font-size: 26rpx; + font-weight: 800; + color: #3d2a1e; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.fs__desc { + margin: 4rpx 16rpx 16rpx; + font-size: 21rpx; + color: #9a7b65; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* 发现工具:两列 */ .hots { display: grid; diff --git a/src/components/mine/MineDrawers.vue b/src/components/mine/MineDrawers.vue index 0ba7ffa..68f03b0 100644 --- a/src/components/mine/MineDrawers.vue +++ b/src/components/mine/MineDrawers.vue @@ -24,6 +24,7 @@ import { CARD_TEMPLATES } from '@/config/card-templates' import { FEATURE_ENTRIES } from '@/config/features' import { TOOL_REGISTRY } from '@/config/tools' import { useCardStyleStore } from '@/stores/cardStyle' +import { useFeedbackStore } from '@/stores/feedback' import { useToolsStore } from '@/stores/tools' import { useUserStore } from '@/stores/user' import { Z } from '@/utils/z-index' @@ -31,6 +32,7 @@ import { Z } from '@/utils/z-index' const userStore = useUserStore() const toolsStore = useToolsStore() const cardStyle = useCardStyleStore() +const feedbackStore = useFeedbackStore() const feedbackVisible = ref(false) const feedbackContent = ref('') @@ -49,6 +51,11 @@ const burst = ref(0) function openFeedback() { feedbackVisible.value = true + // 打开即拉「我的反馈」(此刻服务端未读标记仍在,列表内可显示「新回复」标签), + // 随后立刻置已读清掉入口红点。 + if (userStore.logged) { + feedbackStore.fetchMine().then(() => feedbackStore.markRead()).catch(() => {}) + } } function openBind() { @@ -309,10 +316,12 @@ async function submitFeedback() { try { await apiFeedbackSubmit(feedbackContent.value.trim(), feedbackContact.value.trim()) uni.showToast({ title: '感谢反馈', icon: 'success' }) - feedbackVisible.value = false feedbackContent.value = '' feedbackContact.value = '' burst.value++ + // 刷新「我的反馈」列表(抽屉保持打开,用户可直接看到自己刚提交的记录) + if (userStore.logged) + feedbackStore.fetchMine().catch(() => {}) } finally { submitting.value = false @@ -393,8 +402,43 @@ defineExpose({ openFeedback, openBind, openCardStyle }) 意见反馈 - 说说你想用的工具,或遇到的问题 + 说说你想用的工具,或遇到的问题;官方回复会在这里展示 + + + + + + + {{ item.content }} + + + 新回复 + + + 已处理 + + + 待处理 + + + + + 官方回复 + + + {{ item.reply_content }} + + + {{ item.reply_at }} + + + + 我们会尽快查看你的反馈 + + + + () flex-direction: column; align-items: center; gap: 18rpx; - background: #ffffff; + background: var(--tb-glass-bg-soft, rgba(255, 255, 255, 0.42)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border: 2rpx dashed #ffd4b8; border-radius: 32rpx; padding: 90rpx 40rpx; diff --git a/src/components/tool/ToolPage.vue b/src/components/tool/ToolPage.vue index 60f3e3d..218c512 100644 --- a/src/components/tool/ToolPage.vue +++ b/src/components/tool/ToolPage.vue @@ -11,6 +11,11 @@ * 与「年会查座」等其它画布任务使用**不同 id**,互不冲突。 */ import { heroPadTop } from '@/utils/safearea' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' + +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() withDefaults(defineProps<{ /** 头区主标题 */ @@ -60,6 +65,8 @@ function goBack() { + + diff --git a/src/components/tool/ToolRunning.vue b/src/components/tool/ToolRunning.vue index eca6a66..7ccd40b 100644 --- a/src/components/tool/ToolRunning.vue +++ b/src/components/tool/ToolRunning.vue @@ -54,7 +54,12 @@ withDefaults(defineProps<{ width: 300rpx; padding: 44rpx 32rpx; border-radius: 30rpx; - background: #ffffff; + /* 悬浮在遮罩上的运行框:液态玻璃 */ + background: var(--tb-glass-bg-strong, rgba(255, 255, 255, 0.68)); + -webkit-backdrop-filter: blur(24rpx) saturate(1.7); + backdrop-filter: blur(24rpx) saturate(1.7); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 14rpx 36rpx rgba(43, 26, 16, 0.2); display: flex; flex-direction: column; align-items: center; diff --git a/src/components/vip/QuotaBar.vue b/src/components/vip/QuotaBar.vue index bf6a054..8e7f700 100644 --- a/src/components/vip/QuotaBar.vue +++ b/src/components/vip/QuotaBar.vue @@ -193,9 +193,12 @@ function buyMember() { overflow: hidden; padding: 24rpx 24rpx 22rpx; border-radius: 30rpx; - background: linear-gradient(160deg, #2b1a10 0%, #3d2413 55%, #241408 100%); - border: 2rpx solid rgba(245, 197, 107, 0.5); - box-shadow: 0 18rpx 44rpx rgba(43, 26, 16, 0.5); + /* 深色液态玻璃悬浮面板 */ + background: linear-gradient(160deg, rgba(43, 26, 16, 0.86) 0%, rgba(61, 36, 19, 0.8) 55%, rgba(36, 20, 8, 0.88) 100%); + -webkit-backdrop-filter: blur(24rpx) saturate(1.4); + backdrop-filter: blur(24rpx) saturate(1.4); + border: 1rpx solid rgba(245, 197, 107, 0.4); + box-shadow: inset 0 1rpx 0 rgba(255, 246, 224, 0.22), 0 18rpx 44rpx rgba(43, 26, 16, 0.5); animation: qPanelIn 0.24s cubic-bezier(0.22, 1, 0.36, 1) both; } .qpanel__glow { diff --git a/src/components/vip/VipCard.vue b/src/components/vip/VipCard.vue index cde1aaa..6c2252e 100644 --- a/src/components/vip/VipCard.vue +++ b/src/components/vip/VipCard.vue @@ -127,9 +127,20 @@ function goMyQuota() { overflow: hidden; padding: 28rpx 26rpx 26rpx; border-radius: 34rpx; - background: linear-gradient(160deg, #2b1a10 0%, #3d2413 52%, #22130a 100%); - border: 2rpx solid rgba(245, 197, 107, 0.55); - /* 阴影/内高光来自公共 token .tb-elev-gold(R2-1) */ + /* 深色液态玻璃:深炭半透明 + backdrop 模糊,金光 aura/扫光叠在其上(色相不变) */ + background: + radial-gradient(120% 92% at 8% -14%, rgba(255, 236, 200, 0.14), rgba(255, 236, 200, 0) 62%), + linear-gradient(160deg, rgba(43, 26, 16, 0.82) 0%, rgba(61, 36, 19, 0.76) 52%, rgba(34, 19, 10, 0.86) 100%); + -webkit-backdrop-filter: blur(24rpx) saturate(1.4); + backdrop-filter: blur(24rpx) saturate(1.4); + border: 1rpx solid rgba(245, 197, 107, 0.45); + box-shadow: inset 0 1rpx 0 rgba(255, 246, 224, 0.28), 0 18rpx 42rpx rgba(43, 26, 16, 0.34); +} +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .vcard { + background: linear-gradient(160deg, #2b1a10 0%, #3d2413 52%, #22130a 100%); + border: 2rpx solid rgba(245, 197, 107, 0.55); + } } .vcard__aura { position: absolute; diff --git a/src/components/vip/VipFloatAd.vue b/src/components/vip/VipFloatAd.vue index f92c5fa..19815ab 100644 --- a/src/components/vip/VipFloatAd.vue +++ b/src/components/vip/VipFloatAd.vue @@ -233,9 +233,12 @@ function goPack() { width: 500rpx; padding: 26rpx 26rpx 24rpx; border-radius: 32rpx; - background: linear-gradient(160deg, #2b1a10 0%, #3d2413 55%, #241408 100%); - border: 2rpx solid rgba(245, 197, 107, 0.55); - box-shadow: 0 18rpx 44rpx rgba(43, 26, 16, 0.5); + /* 深色液态玻璃悬浮卡 */ + background: linear-gradient(160deg, rgba(43, 26, 16, 0.86) 0%, rgba(61, 36, 19, 0.8) 55%, rgba(36, 20, 8, 0.88) 100%); + -webkit-backdrop-filter: blur(24rpx) saturate(1.4); + backdrop-filter: blur(24rpx) saturate(1.4); + border: 1rpx solid rgba(245, 197, 107, 0.45); + box-shadow: inset 0 1rpx 0 rgba(255, 246, 224, 0.22), 0 18rpx 44rpx rgba(43, 26, 16, 0.5); animation: vipPanelIn 0.26s cubic-bezier(0.22, 1, 0.36, 1) both; } .panel__glow { diff --git a/src/config/card-styles.ts b/src/config/card-styles.ts index 96b8154..263e0b1 100644 --- a/src/config/card-styles.ts +++ b/src/config/card-styles.ts @@ -206,7 +206,7 @@ export interface CardSection { /** 全局默认(解析顺序末级兜底;仅在「区块默认也拿不到」时命中) */ export const GLOBAL_DEFAULT_STYLE: CardStyle = { template: 'iconTop', - variant: 'tint', + variant: 'glass', tone: 'gold', } @@ -223,20 +223,21 @@ const TPL_FULL = ['iconTop', 'iconLeft', 'lead', 'duo', 'notch', 'arch', 'leaf', /** * 首页区块清单(按源码位置登记;key 见派单表)。 - * 默认样式偏「瓷片区」观感:主卡/发现用渐变插画版式,宫格用图标墙。 + * 默认样式(液态玻璃迭代):卡片一律 glass 玻璃皮肤,色系沿用原配色; + * group 楼层容器版式固定,内部小卡用 tint(容器内玻璃叠玻璃易浑浊)。 * * 注:「我的」页已**不再使用卡片风格体系**(相关页面正回滚), * 故 `mine.stats` / `mine.cells` 两个区块已移除,避免用户配了却看不见效果。 */ export const CARD_SECTIONS: CardSection[] = [ - { key: 'workbench.lead', label: '工作台 · 通栏主卡', default: { template: 'lead', variant: 'porcelain', tone: 'gold' }, members: [], memberSource: 'workbench', templates: TPL_FULL }, - { key: 'workbench.sub', label: '工作台 · 副卡', default: { template: 'duo', variant: 'porcelain', tone: 'mint' }, members: [], memberSource: 'workbench', templates: TPL_NARROW }, - { key: 'workbench.grid', label: '工作台 · 工具宫格', default: { template: 'iconTop', variant: 'tint', tone: 'blue' }, members: [], memberSource: 'workbench', templates: TPL_NARROW }, - { key: 'workbench.features', label: '工作台 · 便捷功能', default: { template: 'iconTop', variant: 'porcelain', tone: 'gold' }, members: [], memberSource: 'features', templates: TPL_NARROW }, - { key: 'workbench.hot', label: '工作台 · 发现工具', default: { template: 'duo', variant: 'porcelain', tone: 'violet' }, members: [], memberSource: 'hot', templates: TPL_NARROW }, + { key: 'workbench.lead', label: '工作台 · 通栏主卡', default: { template: 'lead', variant: 'glass', tone: 'gold' }, members: [], memberSource: 'workbench', templates: TPL_FULL }, + { key: 'workbench.sub', label: '工作台 · 副卡', default: { template: 'duo', variant: 'glass', tone: 'mint' }, members: [], memberSource: 'workbench', templates: TPL_NARROW }, + { key: 'workbench.grid', label: '工作台 · 工具宫格', default: { template: 'iconTop', variant: 'glass', tone: 'blue' }, members: [], memberSource: 'workbench', templates: TPL_NARROW }, + { key: 'workbench.features', label: '工作台 · 便捷功能', default: { template: 'iconTop', variant: 'glass', tone: 'gold' }, members: [], memberSource: 'features', templates: TPL_NARROW }, + { key: 'workbench.hot', label: '工作台 · 发现工具', default: { template: 'duo', variant: 'glass', tone: 'violet' }, members: [], memberSource: 'hot', templates: TPL_NARROW }, { key: 'library.search', label: '工具库 · 搜索卡', default: { template: 'iconLeft', variant: 'glass', tone: 'gold' }, members: [], templates: TPL_FULL }, - { key: 'library.grid', label: '工具库 · 搜索宫格', default: { template: 'iconTop', variant: 'tint', tone: 'blue' }, members: [], templates: TPL_NARROW, hint: '搜索结果为动态列表,按区块统一设置' }, - { key: 'library.floor', label: '工具库 · 模块楼层', default: { template: 'group', variant: 'tint', tone: 'mint' }, members: [], templates: TPL_CONTAINER, hint: '楼层是容器卡,版式固定为「分组容器卡」才能容纳里面的工具小卡;里面的工具小卡在「楼层工具小卡」区块单独设置' }, + { key: 'library.grid', label: '工具库 · 搜索宫格', default: { template: 'iconTop', variant: 'glass', tone: 'blue' }, members: [], templates: TPL_NARROW, hint: '搜索结果为动态列表,按区块统一设置' }, + { key: 'library.floor', label: '工具库 · 模块楼层', default: { template: 'group', variant: 'glass', tone: 'mint' }, members: [], templates: TPL_CONTAINER, hint: '楼层是容器卡,版式固定为「分组容器卡」才能容纳里面的工具小卡;里面的工具小卡在「楼层工具小卡」区块单独设置' }, { key: 'library.tile', label: '工具库 · 楼层工具小卡', default: { template: 'iconTop', variant: 'tint', tone: 'blue' }, members: [], memberSource: 'floors', templates: TPL_NARROW, hint: '楼层里的工具小卡:默认「图标在上、文字横排在下」(窄列,不含容器版式)' }, ] diff --git a/src/config/features.ts b/src/config/features.ts index 08bfa7b..a6a59bd 100644 --- a/src/config/features.ts +++ b/src/config/features.ts @@ -42,6 +42,16 @@ export interface FeatureEntry { */ export const NOTE_REMIND_TMPL_ID = '' +/** + * 开发项目维护费提醒的订阅消息模板 ID。 + * + * ⚠️ 必须与后端 `manifest/config/config.yaml` 的 `wx.subscribe.projectRemindTemplateId` 保持一致。 + * 为空时的降级(两端对称、绝不报错): + * - 前端:确认验收时不发起 requestSubscribeMessage; + * - 后端:维护费到期只发站内信(管理端横幅仍常驻可见)。 + */ +export const PROJECT_REMIND_TMPL_ID = '' + /** 功能入口列表(我的页渲染来源) */ export const FEATURE_ENTRIES: FeatureEntry[] = [ { @@ -67,14 +77,25 @@ export const FEATURE_ENTRIES: FeatureEntry[] = [ tone: 'blue', }, { - key: 'cardLibrary', - name: '卡片样式库', - icon: 'photo', - illustration: 'sparkle', - desc: '20 组卡片设计复刻', - path: '/pkg-style/showcase', - bg: '#ECE2FF', - color: '#5B2E9E', - tone: 'violet', + key: 'service', + name: '软件开发', + icon: 'computer', + illustration: 'rocket', + desc: '定制开发 · 在线报价', + path: '/pkg-service/home', + bg: '#DFF7F0', + color: '#0FA385', + tone: 'mint', }, + // { + // key: 'cardLibrary', + // name: '卡片样式库', + // icon: 'photo', + // illustration: 'sparkle', + // desc: '20 组卡片设计复刻', + // path: '/pkg-style/showcase', + // bg: '#ECE2FF', + // color: '#5B2E9E', + // tone: 'violet', + // }, ] diff --git a/src/pages.json b/src/pages.json index 593a703..dfb607e 100644 --- a/src/pages.json +++ b/src/pages.json @@ -156,12 +156,88 @@ } } ] + }, + { + "root": "pkg-service", + "pages": [ + { + "path": "home", + "style": { + "navigationBarTitleText": "软件开发服务" + } + }, + { + "path": "works", + "style": { + "navigationBarTitleText": "作品案例" + } + }, + { + "path": "work-detail", + "style": { + "navigationBarTitleText": "作品详情" + } + }, + { + "path": "quote", + "style": { + "navigationBarTitleText": "申请报价" + } + }, + { + "path": "quotes", + "style": { + "navigationBarTitleText": "我的报价" + } + }, + { + "path": "project-bind", + "style": { + "navigationBarTitleText": "绑定开发项目" + } + }, + { + "path": "projects", + "style": { + "navigationBarTitleText": "我的项目" + } + }, + { + "path": "project-detail", + "style": { + "navigationBarTitleText": "项目详情" + } + }, + { + "path": "list", + "style": { + "navigationBarTitleText": "服务与价格" + } + }, + { + "path": "detail", + "style": { + "navigationBarTitleText": "服务详情" + } + } + ] + }, + { + "root": "pkg-msg", + "pages": [ + { + "path": "index", + "style": { + "navigationBarTitleText": "消息中心" + } + } + ] } ], "preloadRule": { "pages/index/index": { "network": "wifi", - "packages": ["pkg-member", "pkg-note", "pkg-style"] + "packages": ["pkg-member", "pkg-note", "pkg-style", "pkg-service", "pkg-msg"] } } } diff --git a/src/pages/account/login.vue b/src/pages/account/login.vue index af6032b..907b140 100644 --- a/src/pages/account/login.vue +++ b/src/pages/account/login.vue @@ -212,10 +212,12 @@ async function devLogin() { z-index: 1; } .card { - background: #ffffff; + background: var(--tb-glass-bg-strong, rgba(255, 255, 255, 0.68)); border-radius: 36rpx; - border: 1rpx solid rgba(255, 195, 155, 0.5); - box-shadow: 0 18rpx 44rpx rgba(107, 47, 15, 0.1); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(24rpx) saturate(1.7); + backdrop-filter: blur(24rpx) saturate(1.7); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 18rpx 44rpx rgba(107, 47, 15, 0.1); padding: 32rpx 32rpx 40rpx; } diff --git a/src/pages/mine/profile.vue b/src/pages/mine/profile.vue index 1c3357e..c001469 100644 --- a/src/pages/mine/profile.vue +++ b/src/pages/mine/profile.vue @@ -6,11 +6,16 @@ import { useUserStore } from '@/stores/user' import { AVATAR_PRESETS, presetOf } from '@/utils/avatar' import { navPadTop } from '@/utils/safearea' import { Z } from '@/utils/z-index' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const userStore = useUserStore() /** 导航栏顶部留白:MP 用真实状态栏高度 */ const navPad = navPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + const nickname = ref('') const avatarUrl = ref('') const saving = ref(false) @@ -173,6 +178,8 @@ function goBack() { 保存 + + @@ -233,12 +240,14 @@ function goBack() { } .card { - background: #ffffff; - border: 1rpx solid rgba(255, 195, 155, 0.5); + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.55)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border-radius: 30rpx; padding: 26rpx 28rpx; margin-top: 24rpx; - box-shadow: 0 10rpx 26rpx rgba(107, 47, 15, 0.06); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 10rpx 26rpx rgba(107, 47, 15, 0.06); } .card__title { display: block; diff --git a/src/pages/tools/image/compress.vue b/src/pages/tools/image/compress.vue index 9f171bc..bcf0163 100644 --- a/src/pages/tools/image/compress.vue +++ b/src/pages/tools/image/compress.vue @@ -209,11 +209,13 @@ async function saveAll() { margin-right: 64rpx; } .card { - background: #ffffff; - border: 2rpx solid #ffe3d3; + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.55)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border-radius: 32rpx; padding: 28rpx; - box-shadow: 0 8rpx 24rpx rgba(107, 47, 15, 0.06); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 8rpx 24rpx rgba(107, 47, 15, 0.06); } .quality { display: flex; @@ -274,8 +276,9 @@ async function saveAll() { display: flex; align-items: center; gap: 20rpx; - background: #ffffff; - border: 2rpx solid #ffe3d3; + /* 卡内嵌套面:只半透明不加模糊(外层卡已 blur) */ + background: rgba(255, 255, 255, 0.6); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); border-radius: 28rpx; padding: 20rpx; } diff --git a/src/pages/tools/image/index.vue b/src/pages/tools/image/index.vue index 6ae032f..5a107a7 100644 --- a/src/pages/tools/image/index.vue +++ b/src/pages/tools/image/index.vue @@ -6,12 +6,17 @@ import { useUserStore } from '@/stores/user' import { toolPalette } from '@/utils/palette' import { heroPadTop } from '@/utils/safearea' import { showUnlockGuide } from '@/utils/unlock-guide' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const toolsStore = useToolsStore() const userStore = useUserStore() /** 头区顶部留白:MP 避开胶囊按钮 */ const heroPad = heroPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + function goBack() { uni.navigateBack() } @@ -76,6 +81,8 @@ toolsStore.fetchAll().catch(() => {}) + + diff --git a/src/pages/tools/image/matting.vue b/src/pages/tools/image/matting.vue index a723deb..a8f7680 100644 --- a/src/pages/tools/image/matting.vue +++ b/src/pages/tools/image/matting.vue @@ -400,13 +400,16 @@ async function save() { padding: 12rpx 32rpx; } .card { - background: #ffffff; - border: 2rpx solid #ffe3d3; + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.55)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border-radius: 32rpx; padding: 28rpx; display: flex; flex-direction: column; gap: 20rpx; + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 8rpx 24rpx rgba(107, 47, 15, 0.06); } .card__title { font-size: 26rpx; diff --git a/src/pages/tools/image/qrcode.vue b/src/pages/tools/image/qrcode.vue index 92831ac..2c01756 100644 --- a/src/pages/tools/image/qrcode.vue +++ b/src/pages/tools/image/qrcode.vue @@ -462,13 +462,16 @@ async function save() { box-shadow: 0 8rpx 18rpx rgba(251, 78, 20, 0.28); } .card { - background: #ffffff; - border: 2rpx solid #ffe3d3; + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.55)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border-radius: 32rpx; padding: 28rpx; display: flex; flex-direction: column; gap: 18rpx; + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 8rpx 24rpx rgba(107, 47, 15, 0.06); } .card__title { font-size: 26rpx; diff --git a/src/pages/tools/image/watermark.vue b/src/pages/tools/image/watermark.vue index 2972596..a23e112 100644 --- a/src/pages/tools/image/watermark.vue +++ b/src/pages/tools/image/watermark.vue @@ -200,13 +200,16 @@ async function save() { padding: 8rpx 20rpx; } .card { - background: #ffffff; - border: 2rpx solid #ffe3d3; + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.55)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border-radius: 32rpx; padding: 28rpx; display: flex; flex-direction: column; gap: 24rpx; + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 8rpx 24rpx rgba(107, 47, 15, 0.06); } .modes { display: flex; diff --git a/src/pages/workbench/edit.vue b/src/pages/workbench/edit.vue index 291f49f..1640db4 100644 --- a/src/pages/workbench/edit.vue +++ b/src/pages/workbench/edit.vue @@ -5,6 +5,8 @@ import { useToolsStore } from '@/stores/tools' import { useUserStore } from '@/stores/user' import { navPadTop } from '@/utils/safearea' import { Z } from '@/utils/z-index' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const toolsStore = useToolsStore() const userStore = useUserStore() @@ -13,6 +15,9 @@ const saving = ref(false) /** 导航栏顶部留白:MP 用真实状态栏高度 */ const navPad = navPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + function goBack() { uni.navigateBack() } @@ -159,6 +164,8 @@ async function save() { 保存工作台 + + diff --git a/src/pkg-event/list.vue b/src/pkg-event/list.vue index d11dafd..9ba66fb 100644 --- a/src/pkg-event/list.vue +++ b/src/pkg-event/list.vue @@ -17,9 +17,14 @@ import { onShow } from '@dcloudio/uni-app' import { ref } from 'vue' import { apiEventList } from '@/api/event' import { heroPadTop } from '@/utils/safearea' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const heroPad = heroPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + const loading = ref(true) const list = ref([]) const loadError = ref('') @@ -123,6 +128,8 @@ function goBack() { + + diff --git a/src/pkg-event/query.vue b/src/pkg-event/query.vue index cd2ef82..d8dd2ff 100644 --- a/src/pkg-event/query.vue +++ b/src/pkg-event/query.vue @@ -34,9 +34,14 @@ import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app' import { computed, nextTick, ref, watch } from 'vue' import SeatMap from '@/components/event/SeatMap.vue' import { heroPadTop } from '@/utils/safearea' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const heroPad = heroPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + /** 返回上一页 */ function goBack() { uni.navigateBack() @@ -369,6 +374,8 @@ onUnload(() => { + + diff --git a/src/pkg-member/coupon/index.vue b/src/pkg-member/coupon/index.vue index d4a1bc8..0894103 100644 --- a/src/pkg-member/coupon/index.vue +++ b/src/pkg-member/coupon/index.vue @@ -14,10 +14,15 @@ import { COUPON_STATUS_EXPIRED, COUPON_STATUS_UNUSED, COUPON_STATUS_USED, apiCou import CouponCard from '@/components/coupon/CouponCard.vue' import { useUserStore } from '@/stores/user' import { navPadTop } from '@/utils/safearea' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const userStore = useUserStore() const navPad = navPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + /** 分栏:status 为 undefined 表示「全部」 */ interface TabItem { key: string @@ -168,6 +173,8 @@ function onDetail(coupon: UserCoupon) { 正在加载… + + diff --git a/src/pkg-member/member/detail.vue b/src/pkg-member/member/detail.vue index 0b36024..d3ab67e 100644 --- a/src/pkg-member/member/detail.vue +++ b/src/pkg-member/member/detail.vue @@ -30,12 +30,17 @@ import { fenToYuan, payMemberOrder } from '@/utils/member-pay' import { payQuotaOrder } from '@/utils/quota-pay' import { navPadTop } from '@/utils/safearea' import { Z } from '@/utils/z-index' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const userStore = useUserStore() const toolsStore = useToolsStore() const quotaStore = useQuotaStore() const navPad = navPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + const outTradeNo = ref('') const orderType = ref(0) const targetKey = ref('') @@ -484,6 +489,7 @@ onShow(() => { { > - - 选择优惠券 - - - 与优惠码互斥,选择后立即生效 - + + + 选择优惠券 + + + 与优惠码互斥,选择后立即生效 + + - + - - 不使用优惠券 - 以原价支付本单 + + - - + + + 不使用优惠券 + + + 以原价支付本单 + + + + - + - - {{ c.title }} - 有效期至 {{ c.expire_at || '长期' }} + + + + {{ c.is_freebie ? '免单' : '优惠' }} + + + 整单抵扣 + - - + + + + + + {{ c.title }} + + + 有效期至 {{ c.expire_at || '长期' }} + + + + 免单券 + + + 使用中 + + + + + 暂无可用的免单券 + + + @@ -615,12 +657,14 @@ onShow(() => { } .card { - background: #ffffff; - border: 1rpx solid rgba(255, 195, 155, 0.5); + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.55)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border-radius: 30rpx; padding: 26rpx 28rpx; margin-bottom: 22rpx; - box-shadow: 0 10rpx 26rpx rgba(107, 47, 15, 0.06); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 10rpx 26rpx rgba(107, 47, 15, 0.06); } .card__title { display: block; @@ -747,11 +791,11 @@ onShow(() => { color: #b3907c; } -/* 优惠券选择抽屉(页面根级 + sheet 层级) */ +/* 优惠券选择抽屉(页面根级 + sheet 层级;R5 券面风格重做) */ .cd { - background: #ffffff; + background: #fff9f2; border-radius: 36rpx 36rpx 0 0; - padding: 20rpx 32rpx calc(36rpx + env(safe-area-inset-bottom, 0px)); + padding: 20rpx 28rpx calc(30rpx + env(safe-area-inset-bottom, 0px)); display: flex; flex-direction: column; } @@ -761,70 +805,199 @@ onShow(() => { height: 8rpx; border-radius: 999rpx; background: #ffe3d3; - margin-bottom: 16rpx; + margin-bottom: 18rpx; +} +.cd__head { + padding: 0 6rpx 4rpx; } .cd__title { + display: block; font-size: 34rpx; font-weight: 800; color: #3d2a1e; } .cd__sub { + display: block; margin-top: 6rpx; - font-size: 24rpx; + font-size: 23rpx; color: #9a7b65; } .cd__list { - max-height: 52vh; - margin-top: 20rpx; + max-height: 56vh; + margin-top: 22rpx; } .cd__empty { text-align: center; - padding: 40rpx 0; + padding: 50rpx 0; font-size: 24rpx; color: #b3907c; } -.cp { - position: relative; +.cd__safe { + height: 8rpx; +} + +/* 「不使用优惠券」:素面通栏选项 */ +.none { display: flex; align-items: center; - justify-content: space-between; - gap: 16rpx; - padding: 20rpx 24rpx; + gap: 18rpx; + padding: 22rpx 24rpx; border-radius: 22rpx; - background: #fff9f2; - border: 2rpx solid #ffe3d3; - /* 抽屉内逐条堆叠的间距(原内联列表靠 .coupons 的 gap,已移除) */ - margin-bottom: 14rpx; + background: #ffffff; + border: 2rpx solid #ffe9dd; + margin-bottom: 16rpx; } -.cp--on { - background: #fff4d6; - border-color: #f5c56b; - box-shadow: 0 8rpx 18rpx rgba(224, 166, 63, 0.2); +.none--on { + border-color: #ffb27d; + background: #fff4ec; } -.cp__body { +.none__icon { + flex-shrink: 0; + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: #f6efe8; + display: flex; + align-items: center; + justify-content: center; +} +.none__body { + flex: 1; min-width: 0; } -.cp__title { +.none__title { display: block; font-size: 28rpx; font-weight: 700; color: #3d2a1e; } -.cp__sub { +.none__sub { display: block; - margin-top: 6rpx; + margin-top: 4rpx; font-size: 22rpx; color: #9a7b65; } -.cp__on { + +/* 选中圆点(none / ticket 共用) */ +.radio { flex-shrink: 0; - width: 44rpx; - height: 44rpx; + width: 40rpx; + height: 40rpx; border-radius: 50%; - background: #0fa385; + border: 3rpx solid #e2d3c6; + background: #ffffff; display: flex; align-items: center; justify-content: center; + box-sizing: border-box; +} +.radio--on { + border-color: transparent; + background: linear-gradient(140deg, #ff9f4d, #fb4e14); + box-shadow: 0 6rpx 14rpx rgba(251, 78, 20, 0.32); +} + +/* 券面票卡:左面额区 + 虚线切口 + 右信息区 */ +.tk { + position: relative; + display: flex; + align-items: stretch; + border-radius: 24rpx; + overflow: hidden; + background: #ffffff; + border: 2rpx solid #ffe9dd; + margin-bottom: 18rpx; + padding-right: 20rpx; +} +.tk--on { + border-color: #f5c56b; + background: #fffaf0; + box-shadow: 0 10rpx 24rpx rgba(224, 166, 63, 0.22); +} +.tk__left { + flex-shrink: 0; + width: 168rpx; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6rpx; + padding: 26rpx 10rpx; + background: linear-gradient(135deg, #fff4d6 0%, #ffe3a8 100%); +} +.tk--on .tk__left { + background: linear-gradient(135deg, #ffe9b8 0%, #f5c56b 100%); +} +.tk__amount { + font-size: 38rpx; + font-weight: 800; + line-height: 1.05; + color: #3d2413; +} +.tk__amount-sub { + font-size: 19rpx; + color: rgba(61, 36, 19, 0.65); +} +/* 中缝:虚线 + 上下半圆切口(切口圆用页面底色) */ +.tk__notch { + flex-shrink: 0; + width: 2rpx; + margin: 16rpx 0; + border-left: 2rpx dashed rgba(61, 36, 19, 0.22); + position: relative; +} +.tk__notch::before, +.tk__notch::after { + content: ''; + position: absolute; + left: -13rpx; + width: 26rpx; + height: 26rpx; + border-radius: 50%; + background: #fff9f2; +} +.tk__notch::before { + top: -26rpx; +} +.tk__notch::after { + bottom: -26rpx; +} +.tk__body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6rpx; + padding: 20rpx 0 20rpx 20rpx; +} +.tk__title { + font-size: 28rpx; + font-weight: 800; + color: #3d2a1e; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.tk__expire { + font-size: 21rpx; + color: #b3907c; +} +.tk__tags { + display: flex; + gap: 10rpx; + margin-top: 2rpx; +} +.tk__tag { + font-size: 19rpx; + padding: 3rpx 14rpx; + border-radius: 999rpx; + color: #b57f00; + background: rgba(255, 233, 184, 0.55); +} +.tk__tag--using { + color: #0fa385; + background: #dff7f0; + font-weight: 700; } .foot { diff --git a/src/pkg-member/member/index.vue b/src/pkg-member/member/index.vue index bc13b8a..ff9679e 100644 --- a/src/pkg-member/member/index.vue +++ b/src/pkg-member/member/index.vue @@ -17,11 +17,16 @@ import { useUserStore } from '@/stores/user' import { createMemberOrder, fenToYuan } from '@/utils/member-pay' import { heroPadTop } from '@/utils/safearea' import { Z } from '@/utils/z-index' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const userStore = useUserStore() /** 头区顶部留白:MP 避开胶囊按钮 */ const heroPad = heroPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + const loading = ref(true) const center = ref(null) const selected = ref('') @@ -380,6 +385,8 @@ async function pay() { {{ paying ? '处理中…' : '立即开通' }} + + @@ -451,9 +458,12 @@ async function pay() { margin-top: 16rpx; padding: 30rpx 28rpx; border-radius: 34rpx; + /* 深色液态玻璃:半透明金调 + backdrop 模糊(悬浮在深炭 hero 上) */ background: linear-gradient(150deg, rgba(255, 233, 184, 0.16) 0%, rgba(255, 255, 255, 0.06) 60%); - border: 2rpx solid rgba(245, 197, 107, 0.55); - box-shadow: 0 16rpx 40rpx rgba(20, 10, 4, 0.35); + -webkit-backdrop-filter: blur(24rpx) saturate(1.4); + backdrop-filter: blur(24rpx) saturate(1.4); + border: 1rpx solid rgba(245, 197, 107, 0.45); + box-shadow: inset 0 1rpx 0 rgba(255, 246, 224, 0.25), 0 16rpx 40rpx rgba(20, 10, 4, 0.35); } .mb-card__sweep { position: absolute; @@ -596,12 +606,14 @@ async function pay() { padding: 0 32rpx; } .card { - background: #ffffff; - border: 2rpx solid rgba(245, 197, 107, 0.34); + background: var(--tb-glass-bg, rgba(255, 255, 255, 0.55)); + border: 1rpx solid var(--tb-glass-line, rgba(255, 255, 255, 0.65)); + -webkit-backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); + backdrop-filter: blur(var(--tb-glass-blur, 20rpx)) saturate(var(--tb-glass-saturate, 1.6)); border-radius: 32rpx; padding: 26rpx 26rpx 22rpx; margin-bottom: 22rpx; - box-shadow: 0 12rpx 30rpx rgba(107, 47, 15, 0.08); + box-shadow: inset 0 1rpx 0 var(--tb-card-hi, rgba(255, 255, 255, 0.92)), 0 12rpx 30rpx rgba(107, 47, 15, 0.08); } .sec__head { display: flex; diff --git a/src/pkg-member/member/orders.vue b/src/pkg-member/member/orders.vue index c394fca..15d6354 100644 --- a/src/pkg-member/member/orders.vue +++ b/src/pkg-member/member/orders.vue @@ -1,28 +1,55 @@ diff --git a/src/pkg-note/edit.vue b/src/pkg-note/edit.vue index 2b9650c..51900bb 100644 --- a/src/pkg-note/edit.vue +++ b/src/pkg-note/edit.vue @@ -16,9 +16,14 @@ import { NOTE_REMIND_TMPL_ID } from '@/config/features' import { onLoad } from '@dcloudio/uni-app' import { computed, ref } from 'vue' import { heroPadTop } from '@/utils/safearea' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' const heroPad = heroPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + const id = ref(0) const title = ref('') const content = ref('') @@ -29,8 +34,40 @@ const pinned = ref(false) const remindValue = ref('') /** 加载时的原始提醒时间串,用于判断「本次是否重设了提醒」(决定是否申请授权) */ const originalRemindAt = ref('') -/** 允许选择的最小提醒时间(过去时间不可选,R6-9) */ -const minRemindDate = Date.now() +/** 允许选择的最小提醒时间(过去时间不可选,R6-9);打开选择器时刷新为当下 */ +const minRemindDate = ref(Date.now()) +/** 选择器弹层 */ +const pickerOpen = ref(false) +/** 弹层内的草稿值(确认后才写回 remindValue) */ +const remindDraft = ref(Date.now()) + +/** 行内展示:YYYY-MM-DD HH:mm(不带秒) */ +const remindDisplay = computed(() => { + const v = remindValue.value + if (v === '' || v === null || v === undefined) + return '' + const ms = typeof v === 'number' ? v : parseDateTime(String(v)) + if (ms <= 0) + return '' + const d = new Date(ms) + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` +}) + +function openRemindPicker() { + remindDraft.value = typeof remindValue.value === 'number' && remindValue.value > 0 ? remindValue.value : Date.now() + minRemindDate.value = Date.now() + pickerOpen.value = true +} + +function confirmRemind() { + remindValue.value = remindDraft.value + pickerOpen.value = false +} + +function clearRemind() { + remindValue.value = '' + pickerOpen.value = false +} const loading = ref(false) const submitting = ref(false) @@ -156,11 +193,8 @@ function requestSubscribe(): Promise { async function onSave() { if (submitting.value || notFound.value) return + // 标题可留空:后端自动命名为「未命名笔记 YYYY-MM-DD hh:mm:ss」 const t = title.value.trim() - if (!t) { - uni.showToast({ title: '请填写标题', icon: 'none' }) - return - } const payloadRemind = currentRemindPayload() // 「重设了提醒」= 本次提醒值非空且与加载时不同 → 需要重新授权 @@ -298,7 +332,7 @@ function goBack() { @@ -362,14 +396,27 @@ function goBack() { - + + + + + 定时提醒 + + + 到点通过微信订阅消息提醒你 + + + + + {{ remindDisplay }} + + + 未设置 + + + + 设置后将申请一次微信订阅消息授权(一次授权对应一次通知);未授权时提醒仅应用内可见。 @@ -393,6 +440,44 @@ function goBack() { + + + + + + + + 取消 + + + 定时提醒 + + + 完成 + + + + + 清除提醒 + + + + + @@ -549,6 +634,72 @@ function goBack() { margin: 20rpx 0; } +/* 定时提醒行的值区 */ +.remind-value { + display: flex; + align-items: center; + gap: 8rpx; + max-width: 62%; + flex-shrink: 0; +} +.remind-value__text { + font-size: 27rpx; + font-weight: 700; + color: #e8531a; +} +.remind-value__ph { + font-size: 27rpx; + color: #c39a80; +} + +/* 定时提醒选择弹层(页面根级 bottom sheet,风格对齐「我的」页抽屉) */ +.rp { + background: #fffdfb; + border-radius: 36rpx 36rpx 0 0; + padding: 20rpx 32rpx calc(24rpx + env(safe-area-inset-bottom, 0px)); +} +.rp__grip { + align-self: center; + width: 72rpx; + height: 8rpx; + border-radius: 999rpx; + background: #ffe3d3; + margin-bottom: 8rpx; +} +.rp__bar { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + padding: 6rpx 0 10rpx; +} +.rp__title { + font-size: 32rpx; + font-weight: 800; + color: #3d2a1e; +} +.rp__btn { + font-size: 28rpx; + font-weight: 700; + padding: 10rpx 8rpx; +} +.rp__btn--ghost { + justify-self: start; + color: #9a7b65; +} +.rp__btn--ok { + justify-self: end; + color: #ff6b35; +} +.rp__clear { + margin-top: 12rpx; + padding: 20rpx 0 6rpx; + border-top: 1rpx solid #ffe9dd; + text-align: center; + font-size: 26rpx; + font-weight: 700; + color: #e5484d; +} + /* 操作按钮 */ .acts { display: flex; diff --git a/src/pkg-note/list.vue b/src/pkg-note/list.vue index 8ef555a..33ac5b5 100644 --- a/src/pkg-note/list.vue +++ b/src/pkg-note/list.vue @@ -23,10 +23,15 @@ import { import { onReachBottom, onShow } from '@dcloudio/uni-app' import { computed, ref } from 'vue' import { heroPadTop } from '@/utils/safearea' +import BackFab from '@/components/BackFab.vue' +import { usePageScrolled } from '@/utils/page-scrolled' /** 头区顶部留白:MP 避开胶囊按钮 */ const heroPad = heroPadTop() +/** 滚过阈值后显示吸顶返回按钮(R3) */ +const scrolled = usePageScrolled() + /** 列表本地缓存键(离线只读兜底;后端为权威源,清缓存后重新拉取即可) */ const CACHE_KEY = 'note_list_cache' const PAGE_SIZE = 20 @@ -382,6 +387,8 @@ function remindClass(status: number): string { + + diff --git a/src/pkg-service/detail.vue b/src/pkg-service/detail.vue new file mode 100644 index 0000000..b52c2e4 --- /dev/null +++ b/src/pkg-service/detail.vue @@ -0,0 +1,321 @@ + + + + + diff --git a/src/pkg-service/home.vue b/src/pkg-service/home.vue new file mode 100644 index 0000000..da2467f --- /dev/null +++ b/src/pkg-service/home.vue @@ -0,0 +1,1061 @@ + + + + + diff --git a/src/pkg-service/list.vue b/src/pkg-service/list.vue new file mode 100644 index 0000000..75290ce --- /dev/null +++ b/src/pkg-service/list.vue @@ -0,0 +1,348 @@ + + + + + diff --git a/src/pkg-service/project-bind.vue b/src/pkg-service/project-bind.vue new file mode 100644 index 0000000..87c1d5b --- /dev/null +++ b/src/pkg-service/project-bind.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/src/pkg-service/project-detail.vue b/src/pkg-service/project-detail.vue new file mode 100644 index 0000000..ec7a8e7 --- /dev/null +++ b/src/pkg-service/project-detail.vue @@ -0,0 +1,765 @@ + + + + + diff --git a/src/pkg-service/projects.vue b/src/pkg-service/projects.vue new file mode 100644 index 0000000..c884345 --- /dev/null +++ b/src/pkg-service/projects.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/src/pkg-service/quote.vue b/src/pkg-service/quote.vue new file mode 100644 index 0000000..14e6bdb --- /dev/null +++ b/src/pkg-service/quote.vue @@ -0,0 +1,370 @@ + + +