合并后台端

This commit is contained in:
comlibmb
2026-02-03 11:38:00 +08:00
57 changed files with 24996 additions and 5613 deletions

View File

@@ -1,13 +1,18 @@
// Supabase 配置
// 内网环境 - 本地部署的 Supabase
// IP: 192.168.1.62
// IP: 192.168.1.62
// Kong HTTP Port: 8000
//自己的配置自己解开即可
//export const SUPA_URL: string = 'http://192.168.1.61:18000'
//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
//export const SUPA_URL: string = 'http://192.168.1.62:18000'
//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
export const SUPA_URL: string = 'http://192.168.1.63:18000'
export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU'
// WebSocket 实时连接(内网使用 ws:// 而非 wss://
// export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
//export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket'
export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket'

38
ak/configbackup.uts Normal file
View File

@@ -0,0 +1,38 @@
// // export const SUPA_URL: string = 'http://192.168.0.150:8080'
// // export const SUPA_ANON_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
// // export const SUPA_SERVICE_ROLE_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q'
// // export const SUPA_KEY = SUPA_ANON_KEY
// // export const WS_URL: string = 'ws://'+'/192.168.0.150:8080'+'/realtime/v1/websocket';
// export const SUPA_URL: string = 'https://ak3.oulog.com';
// export const SUPA_KEY: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE";
// export const SUPA_SERVICE_KEY: string = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
// export const WS_URL: string = 'wss://'+'ak3.oulog.com'+'/realtime/v1/websocket';
// // Optional: Edge Function or API endpoint that returns S3 presigned POST
// // Expected response: { url: string, fields: object, publicUrl?: string }
// export const S3_PRESIGN_URL: string = ''
// // Optional: Public base URL for your S3/CND to build final URLs when presign response has no publicUrl
// export const S3_PUBLIC_BASE: string = ''
// export const RAG_API_KEY: string ='ragflow-lkZmNjMzI2YzRiNjExZWY4ZGIwMDI0Mm';
// export const RAG_BASE_URL: string ='https://rag.oulog.com';
// export const RAG_AGENT_ID: string ='15b01b26128111f08cd30242ac120006';
// export const TABORPAGE:boolean = false
// // export const HOME_REDIRECT :string = '/pages/ec/health/ecalert'
// //export const HOME_REDIRECT :string = '/pages/sport/index'
// // export const HOME_REDIRECT :string = '/pages/sport/teacher/dashboard'
// // export const HOME_REDIRECT :string = '/pages/test/multi_device_monitor'
// // export const HOME_REDIRECT :string = '/pages/ec/admin/dashboard'
// // export const HOME_REDIRECT :string = '/pages/sense/healthble'
// //export const HOME_REDIRECT :string = '/pages/ec/elder/dashboard'
// // export const HOME_REDIRECT :string = '/pages/ec/caregiver/dashboard'
// // export const HOME_REDIRECT :string = '/pages/ec/doctor/dashboard'
// // export const HOME_REDIRECT :string = '/pages/ec/family/dashboard'

29
ak/configme.uts Normal file
View File

@@ -0,0 +1,29 @@
// // Supabase 配置
// // 内网环境 - 本地部署的 Supabase
// // 家里通过端口映射访问公司内网Supabase
// // 本地映射端口HTTP 18000, WebSocket 13000
// export const SUPA_URL: string = 'http://192.168.1.61:18000'
// export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg1234567890'
// //export const SUPA_URL: string = 'https://ak3.oulog.com'
// //export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'
// // WebSocket 实时连接(内网使用 ws:// 而非 wss://
// export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket'
// // 备用配置(已注释,如需切换可取消注释)
// // 开发环境 - 其他内网地址
// // export const SUPA_URL: string = 'http://192.168.0.150:8080'
// // export const SUPA_KEY: string = 'your-anon-key'
// // export const WS_URL: string = 'ws://192.168.0.150:8080/realtime/v1/websocket'
// // 生产环境 - Supabase 云服务(已注释)
// // export const SUPA_URL: string = 'https://ak3.oulog.com'
// // export const SUPA_KEY: string = 'your-anon-key'
// // export const WS_URL: string = 'wss://ak3.oulog.com/realtime/v1/websocket'
// // 指向你的 Supabase 服务(开发/私有部署)
// // export const SUPA_URL: string = 'http://192.168.1.64:3000'
// // export const SUPA_KEY: string = 'your-anon-key'
// // export const WS_URL: string = 'ws://192.168.1.64:3000/realtime/v1'
// //export const HOME_REDIRECT :string = '/pages/mall/consumer/index'

View File

@@ -82,7 +82,26 @@ export class AkSupaQueryBuilder {
is(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); }
contains(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cs', value); }
containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); }
not(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'not', value); }
not(field : string, opOrValue : any, value?: any) : AkSupaQueryBuilder {
if (value !== undefined) {
// 三元形式field, operator, value
// 例如 not('badge', 'is', null) -> badge=not.is.null
const combinedOp = 'not.' + opOrValue;
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
let safeValue = value;
if (value === null) {
safeValue = 'null';
}
return this._addCond(field, combinedOp, safeValue);
} else {
// 二元形式field, value
let safeValue = opOrValue;
if (opOrValue === null) {
safeValue = 'null';
}
return this._addCond(field, 'not', safeValue);
}
}
and() : AkSupaQueryBuilder { this._nextLogic = 'and'; return this; }
or(str ?: string) : AkSupaQueryBuilder {
@@ -97,7 +116,12 @@ export class AkSupaQueryBuilder {
private _addCond(afield : string, op : string, value : any) : AkSupaQueryBuilder {
//console.log('add cond:', op, afield, value)
const field = encodeURIComponent(afield)!!
this._conditions.push({ field, op, value, logic: this._nextLogic });
// 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性
let safeValue = value;
if (value === null) {
safeValue = 'null';
}
this._conditions.push({ field, op, value: safeValue, logic: this._nextLogic });
//console.log(this._conditions)
this._nextLogic = 'and';
return this;
@@ -213,8 +237,8 @@ export class AkSupaQueryBuilder {
const val = cond.value;
if ((op == 'in' || op == 'not.in') && Array.isArray(val)) {
params.push(`${k}=${op}.(${val.map(x => typeof x == 'object' ? encodeURIComponent(JSON.stringify(x)) : encodeURIComponent(x.toString())).join(',')})`);
} else if (op == 'is' && (val == null || val == 'null')) {
params.push(`${k}=is.null`);
} else if ((op == 'is' || op == 'not.is') && (val == null || val == 'null')) {
params.push(`${k}=${op}.null`);
} else {
const opvalstr: string = (typeof val == 'object') ? JSON.stringify(val) : (val as string);
params.push(`${k}=${op}.${encodeURIComponent(opvalstr)}`);
@@ -826,6 +850,13 @@ async select(table : string, filter ?: string | null, options ?: AkSupaSelectOpt
headers['Prefer'] = 'return=representation,single-object';
}
}
// 确保有 select 参数
if (options.columns == null) {
params.push('select=*');
} else if (options.columns == "") {
params.push('select=*');
}
} else {
params.push('select=*');
}
@@ -1101,4 +1132,4 @@ export function createClient(url : string, key : string) : AkSupa {
return new AkSupa(url, key);
}
export default AkSupa;
export default AkSupa;

View File

@@ -1,63 +1,34 @@
// /components/supadb/aksupainstance.uts
import AkSupa from './aksupa.uts'
import { createClient } from './aksupa.uts'
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
import { AkReq } from '@/uni_modules/ak-req/index.uts'
// 创建 AkSupa 实例(在全局复用)
const supa = new AkSupa(SUPA_URL, SUPA_KEY)
// 创建单一真实的 Supabase 客户端实例 (使用 config.uts 配置)
// Create single source of truth client using config
const supaInstance = createClient(SUPA_URL, SUPA_KEY)
/**
* supaReady: 初始化时尝试从持久化 token 恢复会话。
* - 若内存中已有 session 则直接返回 true
* - 否则尝试读取 AkReq 中持久化的 refresh token 并调用 `refreshSession()` 恢复
*/
const supaReady: Promise<boolean> = (async () => {
try {
const cur = supa.getSession()
if (cur && cur.session != null && cur.session.access_token) {
return true
}
// 导出默认实例 (供 login.uvue 等使用)
export default supaInstance
// 从持久化 storage 读取 token
const access = AkReq.getToken()
const refresh = AkReq.getRefreshToken()
const expiresAt = AkReq.getExpiresAt() ?? 0
// 导出命名实例 'supabase' (供 store.uts 使用)
export const supabase = supaInstance
if (refresh && refresh !== '') {
// 临时注入 session以便 refreshSession 使用 refresh_token
try {
supa.session = {
access_token: access ?? '',
refresh_token: refresh,
expires_at: expiresAt,
user: null,
token_type: '',
expires_in: 0,
raw: new UTSJSONObject({})
}
} catch (e) {
// 在某些环境 UTSJSONObject 构造可能不可用,忽略并继续
try { (supa as any).session = { access_token: access ?? '', refresh_token: refresh, expires_at: expiresAt } } catch (_) {}
}
// 导出 isSupabaseReady 状态
export const isSupabaseReady = true
const ok = await supa.refreshSession()
if (ok) return true
// 刷新失败,清空内存 session
try { supa.session = null; supa.user = null } catch (_) {}
}
return true
} catch (err) {
console.error('Supabase instance init failed', err)
return false
}
})()
// 向后兼容:保留 ensureSupabaseReady 接口
// 兼容 ensureSupabaseReady
export async function ensureSupabaseReady() {
return await supaReady
return true
}
export { supaReady }
export default supa
// 检查连接状态的函数
export function checkConnection() {
return Promise.resolve(true)
}
// 兼容 supaReady Promise
export const supaReady = Promise.resolve(true)
// 如果有其他需要导出的函数,可以这样导出:
export function initializeSupabase(url: string, key: string) {
return createClient(url, key)
}

View File

@@ -1,97 +0,0 @@
// 在浏览器 DevTools Console 中运行此脚本,诊断 system-info 页面的实际状态
(function () {
console.clear();
console.log("=== System-Info 页面运行时诊断 ===\n");
// 1. 检查当前路由
console.log("1⃣ 当前路由信息:");
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
console.log(" route:", currentPage?.route);
console.log(" $page.fullPath:", currentPage?.$page?.fullPath);
console.log("");
// 2. 查找 AdminLayout 组件实例
console.log("2⃣ 查找 AdminLayout 组件:");
const layoutRoot = document.querySelector(".layout-root");
if (!layoutRoot) {
console.log(" ❌ 找不到 .layout-root");
console.log(" AdminLayout 可能未渲染");
return;
}
console.log(" ✅ 找到 .layout-root DOM");
// 3. 检查 Vue 实例数据
console.log("\n3⃣ 检查 Vue 实例数据:");
// 获取 Vue 实例(不同版本的获取方式可能不同)
let vueInstance = null;
// 方法1: 从 DOM 上直接获取
if (layoutRoot.__vue__) {
vueInstance = layoutRoot.__vue__;
} else if (layoutRoot.__vueParentComponent) {
vueInstance = layoutRoot.__vueParentComponent;
} else if (window.__NUXT__) {
vueInstance = window.__NUXT__.$el.__vue__;
}
if (!vueInstance) {
console.log(" ⚠️ 无法直接获取 Vue 实例");
console.log(" 💡 打开 Vue DevTools 查看组件树");
} else {
const ctx = vueInstance.setupState || vueInstance.ctx;
console.log(" activeMenuId:", ctx?.activeMenuId?.value || "未找到");
console.log(" activeSubId:", ctx?.activeSubId?.value || "未找到");
console.log(
" activeGroups length:",
ctx?.activeGroups?.value?.length || "未找到",
);
}
// 4. 检查 AdminSubSider 是否存在
console.log("\n4⃣ 检查二级侧边栏:");
const subSider = document.querySelector(".sub-sider");
if (subSider) {
const style = getComputedStyle(subSider);
console.log(" ✅ 找到 .sub-sider");
console.log(" display:", style.display);
console.log(" visibility:", style.visibility);
} else {
console.log(" ❌ 找不到 .sub-sider");
console.log(" 可能原因:");
console.log(" - activeGroups.length === 0 (v-if 条件不满足)");
console.log(' - nav 匹配失败activeMenuId === "home"');
}
// 5. 检查面包屑信息
console.log("\n5⃣ 检查面包屑:");
const breadcrumb = document.querySelector(".breadcrumb");
if (breadcrumb) {
console.log(" 内容:", breadcrumb.textContent);
}
// 6. 检查 currentPage prop 是否传递
console.log("\n6⃣ 检查 currentPage prop:");
const parentComponent = layoutRoot?.parentElement?.__vue__;
if (parentComponent?.props) {
console.log(" currentPage:", parentComponent.props.currentPage);
} else {
console.log(" ⚠️ 无法获取 currentPage prop");
}
// 7. 检查浏览器控制台是否有错误
console.log("\n7⃣ 浏览器错误:");
console.log(" 检查上方是否有红色错误信息");
console.log(' 特别注意 "Failed to resolve component" 错误');
console.log("\n📌 如果 SubSider 不显示:");
console.log(' ❌ activeMenuId = "home" → 需要检查 nav.uts 匹配');
console.log(" ❌ activeSubId 为空 → 检查菜单配置");
console.log(
" ❌ activeGroups.length = 0 → 检查 maintain 菜单的 groups 数组",
);
console.log("\n" + "=".repeat(50));
})();

View File

@@ -1,101 +0,0 @@
// 在浏览器控制台中运行此脚本,诊断 system-info 页面问题
// 复制整个内容到浏览器控制台并按回车执行
(function () {
console.log("=== System-Info 页面诊断工具 ===\n");
// 1. 检查当前路由
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
console.log("1. 当前路由信息:");
console.log(" - route:", currentPage?.route);
console.log(" - options:", currentPage?.options);
console.log("");
// 2. 检查 Vue 组件实例(寻找 AdminLayout
console.log("2. 查找 AdminLayout 组件实例:");
try {
// 通过 DOM 查找 AdminLayout 的根元素
const layoutRoot = document.querySelector(".layout-root");
if (layoutRoot) {
console.log(" ✓ 找到 .layout-root DOM 元素");
// 尝试获取 Vue 实例
const vueInstance = layoutRoot.__vueParentComponent || layoutRoot.__vue__;
if (vueInstance) {
console.log(" ✓ 找到 Vue 实例");
// 检查 props
const props = vueInstance.props || vueInstance.propsData;
console.log(" - currentPage prop:", props?.currentPage);
// 检查内部状态
const setupState = vueInstance.setupState || vueInstance.data;
if (setupState) {
console.log(
" - activeMenuId:",
setupState.activeMenuId?.value || setupState.activeMenuId,
);
console.log(
" - activeSubId:",
setupState.activeSubId?.value || setupState.activeSubId,
);
console.log(
" - activeGroups length:",
(setupState.activeGroups?.value || setupState.activeGroups)?.length,
);
}
} else {
console.log(" ✗ 未找到 Vue 实例(可能是渲染问题)");
}
} else {
console.log(" ✗ 未找到 .layout-root DOM 元素");
console.log(" → AdminLayout 组件可能未被渲染!");
}
} catch (e) {
console.log(" ✗ 检查过程出错:", e.message);
}
console.log("");
// 3. 检查 AdminSubSider
console.log("3. 检查二级侧边栏:");
const subSider = document.querySelector(".sub-sider");
if (subSider) {
console.log(" ✓ 找到 .sub-sider DOM 元素");
console.log(
" - 是否可见:",
getComputedStyle(subSider).display !== "none",
);
} else {
console.log(" ✗ 未找到 .sub-sider DOM 元素");
console.log(' → 可能是因为 v-if="activeGroups.length > 0" 条件不满足');
}
console.log("");
// 4. 检查控制台错误
console.log("4. 建议排查步骤:");
console.log(" a) 打开 Vue DevTools 查看组件树");
console.log(
' b) 检查是否有 "Failed to resolve component: AdminLayout" 错误',
);
console.log(" c) 检查 system-info.uvue 是否正确导入了 AdminLayout");
console.log(" d) 检查 menu.uts 中 system-info 的配置");
console.log(" e) 检查网络请求,确保所有资源加载成功");
console.log("");
// 5. 模拟 nav 匹配逻辑
console.log('5. 模拟导航匹配 (currentPage="system-info"):');
console.log(" 根据 nav.uts 逻辑,应该匹配到:");
console.log(' - activeMenuId: "maintain"');
console.log(' - activeSubId: "system-info"');
console.log(' - 这应该显示 "维护" 菜单的二级侧边栏');
console.log("");
console.log("=== 诊断完成 ===");
console.log("如果 AdminLayout 未渲染,请检查:");
console.log(
"1. system-info.uvue 文件中是否有 <script setup> 导入 AdminLayout",
);
console.log("2. 浏览器控制台是否有编译错误");
console.log("3. 刷新页面后重新运行此诊断脚本");
})();

View File

@@ -1,41 +0,0 @@
// 调试辅助:在浏览器控制台运行此代码来验证 system-info 页面状态
console.log("=== system-info 页面调试信息 ===\n");
// 1. 检查当前页面路由
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
console.log("1. 当前页面路由:", currentPage?.route);
// 2. 检查 AdminLayout 的 props
console.log('2. currentPage prop: "system-info"');
// 3. 模拟 findActiveByCurrentPage 的执行
console.log("\n3. 预期匹配结果:");
console.log(' - activeMenuId: "maintain"');
console.log(' - activeSubId: "system-info"');
// 4. 检查二级侧边栏是否应该显示
console.log("\n4. activeGroups 应该包含:");
console.log(" - dev-config (开发配置)");
console.log(" - dev-tools (开发工具)");
console.log(" - data-management (数据管理)");
console.log(" - security (安全)");
console.log(" - i18n-setting (语言设置)");
console.log(" - external-api (对外接口)");
console.log(" - system-info (系统信息) ← 叶子节点,应该高亮");
console.log("\n5. 二级侧边栏渲染条件:");
console.log(' - v-if="activeGroups.length > 0" → 应该为 true');
console.log(" - activeGroups.length 应该 > 0");
console.log("\n6. 面包屑显示:");
console.log(' - 应该显示: "维护 / 系统信息"');
console.log("\n=== 如果侧边栏仍未显示,请检查: ===");
console.log("1. Vue DevTools 中 AdminLayout 组件的 data:");
console.log(' - activeMenuId 是否为 "maintain"');
console.log(' - activeSubId 是否为 "system-info"');
console.log(" - activeGroups 数组是否非空");
console.log('\n2. Elements 面板中是否存在 class="sub-sider" 的元素');
console.log("\n3. Console 是否有 Vue 警告或错误");

View File

@@ -0,0 +1,247 @@
# Supabase 数据迁移指南
## 概述
本指南将帮助您将当前使用模拟数据的 uni-app 项目迁移到使用 Supabase 数据库。您的项目已经配置了连接到 Ubuntu 服务器上的 SupabaseIP: 192.168.1.61),现在需要创建数据库表并插入测试数据,然后修改前端代码以使用真实数据。
## 第一步:在 Supabase 中创建数据库表
### 方法一:通过 Supabase Dashboard 执行 SQL
1. **打开 Supabase Dashboard**
- 访问http://192.168.1.61:3000
- 使用 Dashboard 用户名和密码登录(位于 `supabase_pro/.env` 中的 `DASHBOARD_USERNAME``DASHBOARD_PASSWORD`
2. **进入 SQL Editor**
- 在左侧菜单中点击 "SQL Editor"
- 点击 "New query" 创建新查询
3. **执行建表脚本**
- 复制 `sql/001_create_tables.sql` 文件中的全部内容
- 粘贴到 SQL Editor 中
- 点击 "Run" 执行
4. **执行插入数据脚本**
- 复制 `sql/002_insert_test_data.sql` 文件中的全部内容
- 粘贴到 SQL Editor 中
- 点击 "Run" 执行
### 方法二:通过命令行执行(如果 Supabase 运行在 Ubuntu 服务器上)
```bash
# 登录到 Ubuntu 服务器
ssh hfkj@192.168.1.61
# 进入 Supabase 项目目录(假设 Supabase 安装在默认位置)
cd ~/supabase
# 使用 psql 连接到数据库执行 SQL 脚本
# 注意:需要知道数据库密码(位于 supabase_pro/.env 中的 POSTGRES_PASSWORD
PGPASSWORD=yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc psql -h localhost -U postgres -d postgres -f /path/to/001_create_tables.sql
PGPASSWORD=yxyHINygZMLSq9jLddrZQBB-CoyGHSF5DwlwWmbrYXc psql -h localhost -U postgres -d postgres -f /path/to/002_insert_test_data.sql
```
## 第二步:验证数据表创建成功
### 在 Supabase Dashboard 中验证
1. **查看 Tables**
- 在左侧菜单中点击 "Table Editor"
- 应该能看到 `categories``products`
2. **查看数据**
- 点击 `categories` 表,应该能看到 10 条分类数据
- 点击 `products` 表,应该能看到 18 条商品数据
### 通过 API 验证
1. **测试分类 API**
```
GET http://192.168.1.61:8000/rest/v1/categories
Headers:
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg
```
2. **测试商品 API**
```
GET http://192.168.1.61:8000/rest/v1/products?category_id=eq.cold
Headers:
apikey: [同上]
```
## 第三步:修改前端代码使用真实数据
### 1. 已创建的服务文件
我已经创建了 `utils/supabaseService.uts` 文件,提供了以下功能:
- `getCategories()` - 获取所有分类
- `getProductsByCategory()` - 获取指定分类的商品
- `searchProducts()` - 搜索商品
- `getProductById()` - 获取单个商品详情
- `getHotProducts()` - 获取热销商品
- `getRecommendedProducts()` - 获取推荐商品
### 2. 修改分类页面 (`pages/mall/consumer/category.uvue`)
需要将硬编码的模拟数据替换为从 Supabase 获取的数据:
```typescript
// 在 script 部分添加导入
import supabaseService from '@/utils/supabaseService.uts'
import type { Category, Product } from '@/utils/supabaseService.uts'
// 替换 medicineCategories 的初始化
// 删除原有的 medicineCategories 数组定义
// 修改 onMounted 或创建新的生命周期函数
onMounted(async () => {
await loadCategories()
await loadProducts()
})
// 添加加载分类的方法
const loadCategories = async () => {
const categories = await supabaseService.getCategories()
if (categories.length > 0) {
primaryCategories.value = categories
// 设置默认选中第一个分类
if (!activePrimary.value && categories[0]) {
activePrimary.value = categories[0].id
}
}
}
// 修改 selectPrimaryCategory 方法
const selectPrimaryCategory = async (categoryId: string) => {
activePrimary.value = categoryId
// 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === categoryId)
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description
}
// 加载对应商品
const response = await supabaseService.getProductsByCategory(categoryId)
productList.value = response.data
hasMore.value = response.hasmore
}
```
### 3. 修改主页 (`pages/mall/consumer/index.uvue`)
如果主页显示商品,也需要修改为从 Supabase 获取:
```typescript
import supabaseService from '@/utils/supabaseService.uts'
// 获取热销商品
const loadHotProducts = async () => {
hotProducts.value = await supabaseService.getHotProducts(6)
}
// 获取推荐商品
const loadRecommendedProducts = async () => {
recommendedProducts.value = await supabaseService.getRecommendedProducts(6)
}
```
## 第四步:测试连接和数据
### 1. 测试 Supabase 连接
创建一个测试页面或使用现有的页面测试连接:
```typescript
// 测试代码示例
const testConnection = async () => {
try {
const categories = await supabaseService.getCategories()
console.log('连接成功,获取到分类数:', categories.length)
uni.showToast({
title: `连接成功,获取到 ${categories.length} 个分类`,
icon: 'success'
})
} catch (error) {
console.error('连接失败:', error)
uni.showToast({
title: '连接失败,请检查配置',
icon: 'error'
})
}
}
```
### 2. 测试数据加载
在分类页面测试:
1. 打开分类页面
2. 检查分类列表是否显示
3. 点击不同分类,检查商品列表是否更新
4. 检查商品图片、价格等信息是否正确显示
### 3. 测试搜索功能
如果项目有搜索页面,测试搜索功能:
1. 输入关键字搜索
2. 检查返回的商品是否相关
## 第五步:处理图片 URL
### 当前情况
- 数据库中的 `image` 字段目前为空或使用本地路径
- 实际项目中,图片应该存储在 Supabase Storage 或 CDN
### 临时解决方案
在显示图片时,如果数据库中没有图片 URL使用默认图片
```typescript
const getProductImage = (product: Product) => {
if (product.image && product.image.startsWith('http')) {
return product.image
}
return '/static/images/default-product.png'
}
```
### 长期解决方案
1. 将图片上传到 Supabase Storage
2. 更新数据库中的 `image` 字段为完整 URL
## 常见问题解决
### 1. 连接超时或失败
- 检查 Ubuntu 服务器上的 Supabase 是否正常运行
- 检查防火墙设置,确保 8000 和 3000 端口可访问
- 检查 `ak/config.uts` 中的 IP 地址是否正确
### 2. 401 未授权错误
- 检查 `SUPA_KEY` 是否正确(与 `supabase_pro/.env` 中的 `ANON_KEY` 一致)
- 检查 Supabase 是否已启用匿名访问
### 3. 表不存在错误
- 确认已执行 SQL 脚本创建表
- 检查表名是否拼写正确(区分大小写)
### 4. 数据不显示
- 检查浏览器控制台是否有错误
- 检查网络请求是否成功
- 确认数据库中有数据
## 下一步优化建议
1. **添加加载状态**:在数据加载时显示加载动画
2. **错误处理**:添加更完善的错误处理和重试机制
3. **数据缓存**:使用本地存储缓存常用数据,减少网络请求
4. **分页加载**:实现滚动加载更多商品
5. **图片优化**:使用图片懒加载和压缩
## 总结
通过以上步骤,您的项目将从使用模拟数据过渡到使用 Supabase 数据库数据。主要工作包括:
1. 在 Supabase 中创建表和插入测试数据
2. 修改前端代码使用新的服务层
3. 测试连接和数据加载
完成后,您的应用将具备完整的后端数据支持,为后续添加用户管理、购物车、订单等功能打下基础。

View File

@@ -331,34 +331,34 @@
"navigationStyle": "custom"
}
},
{
"path": "help-center",
"style": {
"navigationBarTitleText": "帮助中心",
"navigationStyle": "custom"
}
},
{
"path": "about",
"style": {
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
},
{
"path": "feedback",
"style": {
"navigationBarTitleText": "意见反馈",
"navigationStyle": "custom"
}
},
{
"path": "test",
"style": {
"navigationBarTitleText": "test",
"navigationStyle": "custom"
}
},
{
"path": "help-center",
"style": {
"navigationBarTitleText": "帮助中心",
"navigationStyle": "custom"
}
},
{
"path": "about",
"style": {
"navigationBarTitleText": "关于我们",
"navigationStyle": "custom"
}
},
{
"path": "feedback",
"style": {
"navigationBarTitleText": "意见反馈",
"navigationStyle": "custom"
}
},
{
"path": "test",
"style": {
"navigationBarTitleText": "test",
"navigationStyle": "custom"
}
},
{
"path": "settings",
"style": {
@@ -449,6 +449,95 @@
}
]
},
{
"root": "pages/mall/admin",
"pages": [
{
"path": "user-management",
"style": {
"navigationBarTitleText": "用户管理",
"navigationStyle": "custom"
}
},
{
"path": "product-management",
"style": {
"navigationBarTitleText": "商品管理",
"navigationStyle": "custom"
}
},
{
"path": "order-management",
"style": {
"navigationBarTitleText": "订单管理",
"navigationStyle": "custom"
}
},
{
"path": "finance/record",
"style": {
"navigationBarTitleText": "财务管理",
"navigationStyle": "custom"
}
},
{
"path": "user-statistics",
"style": {
"navigationBarTitleText": "用户统计",
"navigationStyle": "custom"
}
},
{
"path": "system-settings",
"style": {
"navigationBarTitleText": "系统设置",
"navigationStyle": "custom"
}
},
{
"path": "subscription/plan-management",
"style": {
"navigationBarTitleText": "订阅方案管理"
}
},
{
"path": "subscription/user-subscriptions",
"style": {
"navigationBarTitleText": "用户订阅管理"
}
},
{
"path": "marketing/coupon/list",
"style": {
"navigationBarTitleText": "优惠券列表"
}
},
{
"path": "marketing/coupon/receive",
"style": {
"navigationBarTitleText": "用户领取记录"
}
},
{
"path": "marketing/points/index",
"style": {
"navigationBarTitleText": "积分管理"
}
},
{
"path": "marketing/signin/rule",
"style": {
"navigationBarTitleText": "签到规则"
}
},
{
"path": "marketing/signin/record",
"style": {
"navigationBarTitleText": "签到记录"
}
}
]
},
{
"root": "pages/mall/merchant",
"pages": [
@@ -544,4 +633,4 @@
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
}
}
}

View File

@@ -1033,4 +1033,94 @@ import i18n from '@/uni_modules/i18n/index.uts' // 保留用于语言切换
.action-btn.primary .action-text {
color: #ffffff;
}
/* 响应式布局优化 */
@media screen and (min-width: 480px) and (max-width: 767px) {
.settings-section {
margin: 12px 20px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
.section-header {
width: 100%;
}
.setting-item {
flex: 1 0 calc(50% - 20px);
margin: 10px;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px;
box-sizing: border-box;
border-bottom: none;
flex-direction: column;
align-items: flex-start;
}
.item-content {
width: 100%;
margin-bottom: 10px;
}
.item-label, .item-desc, .item-value {
display: block;
width: 100%;
}
.toggle-switch, .item-arrow {
align-self: flex-end;
margin-top: auto;
}
}
@media screen and (min-width: 768px) {
.settings-page {
padding: 20px;
background-color: #f5f5f5;
}
.settings-section {
margin: 0 auto 20px;
max-width: 800px;
border-radius: 12px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
.section-header {
width: 100%;
}
.setting-item {
flex: 1 0 calc(33.333% - 20px);
margin: 10px;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px;
box-sizing: border-box;
border-bottom: none;
flex-direction: column;
align-items: flex-start;
}
.item-content {
width: 100%;
margin-bottom: 10px;
}
.item-label, .item-desc, .item-value {
display: block;
width: 100%;
}
.toggle-switch, .item-arrow {
align-self: flex-end;
margin-top: auto;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -53,6 +53,7 @@
<script setup lang="uts">
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService } from '@/utils/supabaseService.uts'
type Address = {
id: string
@@ -94,18 +95,52 @@ onLoad((options) => {
}
})
const loadAddress = (id: string) => {
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const address = addresses.find(item => item.id === id)
const loadAddress = async (id: string) => {
try {
// 从Supabase加载地址详情
const address = await supabaseService.getAddressById(id)
if (address) {
formData.name = address.name
formData.name = address.recipient_name
formData.phone = address.phone
formData.detail = address.detail
formData.isDefault = address.isDefault
formData.detail = address.detail_address
formData.isDefault = address.is_default
formData.label = address.label || ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
} else {
// 如果Supabase没有找到尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const localAddress = addresses.find(item => item.id === id)
if (localAddress) {
formData.name = localAddress.name
formData.phone = localAddress.phone
formData.detail = localAddress.detail
formData.isDefault = localAddress.isDefault
formData.label = localAddress.label || ''
regionString.value = `${localAddress.province} ${localAddress.city} ${localAddress.district}`.trim()
}
}
}
} catch (error) {
console.error('加载地址详情失败:', error)
// 失败时从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as Address[]
const address = addresses.find(item => item.id === id)
if (address) {
formData.name = address.name
formData.phone = address.phone
formData.detail = address.detail
formData.isDefault = address.isDefault
formData.label = address.label || ''
regionString.value = `${address.province} ${address.city} ${address.district}`.trim()
}
} catch (e) {
console.error('解析本地地址数据失败', e)
}
}
}
}
@@ -122,7 +157,7 @@ const onSwitchChange = (e: UniSwitchChangeEvent) => {
formData.isDefault = e.detail.value
}
const saveAddress = () => {
const saveAddress = async () => {
if (!formData.name) {
uni.showToast({ title: '请填写收货人', icon: 'none' })
return
@@ -146,28 +181,66 @@ const saveAddress = () => {
const city = regions[1] || ''
const district = regions.slice(2).join(' ') || ''
const storedAddresses = uni.getStorageSync('addresses')
let addresses: Address[] = []
if (storedAddresses) {
try {
addresses = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
addresses = []
}
}
// 如果设为默认,取消其他默认
if (formData.isDefault) {
addresses.forEach(item => {
item.isDefault = false
})
// 构建地址对象
const addressData = {
recipient_name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail_address: formData.detail,
postal_code: '', // 如果需要可以添加邮政编码字段
is_default: formData.isDefault,
label: formData.label || ''
}
let success = false
if (isEdit.value) {
const index = addresses.findIndex(item => item.id === addressId.value)
if (index !== -1) {
addresses[index] = {
...addresses[index],
// 更新地址
success = await supabaseService.updateAddress(addressId.value, addressData)
} else {
// 添加新地址
success = await supabaseService.addAddress(addressData)
}
if (success) {
// 同时更新本地存储作为缓存
const storedAddresses = uni.getStorageSync('addresses')
let addresses: Address[] = []
if (storedAddresses) {
try {
addresses = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
addresses = []
}
}
// 如果设为默认,取消其他默认
if (formData.isDefault) {
addresses.forEach(item => {
item.isDefault = false
})
}
if (isEdit.value) {
const index = addresses.findIndex(item => item.id === addressId.value)
if (index !== -1) {
addresses[index] = {
...addresses[index],
name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail: formData.detail,
isDefault: formData.isDefault,
label: formData.label
}
}
} else {
const newAddress: Address = {
id: `addr_${Date.now()}`, // 临时ID实际由Supabase生成
name: formData.name,
phone: formData.phone,
province: province,
@@ -177,36 +250,26 @@ const saveAddress = () => {
isDefault: formData.isDefault,
label: formData.label
}
addresses.push(newAddress)
}
uni.setStorageSync('addresses', JSON.stringify(addresses))
uni.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
const newAddress: Address = {
id: `addr_${Date.now()}`,
name: formData.name,
phone: formData.phone,
province: province,
city: city,
district: district,
detail: formData.detail,
isDefault: formData.isDefault,
label: formData.label
}
// 如果是第一个地址,自动设为默认
if (addresses.length === 0) {
newAddress.isDefault = true
}
addresses.push(newAddress)
console.error('保存地址失败')
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
uni.setStorageSync('addresses', JSON.stringify(addresses))
uni.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
const parseSmartInput = () => {
@@ -244,17 +307,23 @@ const parseSmartInput = () => {
formData.detail = addrText
}
}
const deleteAddress = () => {
const deleteAddress = async () => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
let addresses = JSON.parse(storedAddresses as string) as Address[]
addresses = addresses.filter(item => item.id !== addressId.value)
uni.setStorageSync('addresses', JSON.stringify(addresses))
// 调用Supabase服务删除地址
const success = await supabaseService.deleteAddress(addressId.value)
if (success) {
// 同时从本地存储中移除
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
let addresses = JSON.parse(storedAddresses as string) as Address[]
addresses = addresses.filter(item => item.id !== addressId.value)
uni.setStorageSync('addresses', JSON.stringify(addresses))
}
uni.showToast({
title: '删除成功',
@@ -264,6 +333,12 @@ const deleteAddress = () => {
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}

View File

@@ -0,0 +1,219 @@
<template>
<view class="address-list-page">
<view class="address-list">
<view v-if="addresses.length === 0" class="empty-state">
<text class="empty-icon">📍</text>
<text class="empty-text">暂无收货地址</text>
</view>
<view v-else v-for="(item, index) in addresses" :key="item.id" class="address-item" @click="selectAddress(item)">
<view class="item-content">
<view class="item-header">
<text class="user-name">{{ item.name }}</text>
<text class="user-phone">{{ item.phone }}</text>
<text v-if="item.isDefault" class="default-tag">默认</text>
<text v-if="item.label" class="label-tag">{{ item.label }}</text>
</view>
<text class="address-text">{{ getFullAddress(item) }}</text>
</view>
<view class="item-actions">
<view class="action-item" @click.stop="editAddress(item.id)">
<text class="action-icon">📝</text>
</view>
<view class="action-item" @click.stop="deleteAddress(item.id)">
<text class="action-icon"><3E></text>
</view>
</view>
</view>
</view>
<view class="footer-btn">
<button class="add-btn" @click="addAddress">新建收货地址</button>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
type Address = {
id: string
name: string
phone: string
province: string
city: string
district: string
detail: string
isDefault: boolean
label?: string
}
const addresses = ref<Address[]>([])
const selectionMode = ref<boolean>(false)
let openerEventChannel: any = null
onShow(() => {
loadAddresses()
})
onMounted(() => {
try {
const ec = uni.getOpenerEventChannel()
openerEventChannel = ec
ec?.on('setSelectMode', (data: any) => {
if (data && typeof data.selectMode === 'boolean') {
selectionMode.value = data.selectMode
}
})
} catch (e) {
// ignore
}
})
const loadAddresses = () => {
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
addresses.value = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
console.error('Failed to parse addresses', e)
addresses.value = []
}
} else {
// 初始Mock数据
addresses.value = [
{
id: 'addr_001',
name: '张三',
phone: '13800138000',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '三里屯SOHO A座',
isDefault: true,
label: '公司'
}
]
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
}
}
const getFullAddress = (item: Address): string => {
return `${item.province}${item.city}${item.district} ${item.detail}`
}
const addAddress = () => {
uni.navigateTo({
url: '/pages/mall/consumer/address-edit'
})
}
// 删除地址
const deleteAddress = (id: string) => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
if (res.confirm) {
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
}
})
}
const editAddress = (id: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/address-edit?id=${id}`
})
}
const selectAddress = (item: Address) => {
if (selectionMode.value && openerEventChannel) {
openerEventChannel.emit('addressSelected', {
id: item.id,
recipient_name: item.name,
phone: item.phone,
province: item.province,
city: item.city,
district: item.district,
detail: item.detail,
is_default: item.isDefault
})
uni.navigateBack()
} else {
editAddress(item.id)
}
}
</script>
<style>
.item-actions {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.footer-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: 10px 15px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
display: flex;
justify-content: center; /* 居中显示 */
align-items: center;
}
.add-btn {
background-color: #ff5000;
color: white;
border-radius: 25px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: none;
width: 100%; /* 默认占满 */
max-width: 100%;
}
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.address-list {
max-width: 800px;
margin: 0 auto;
}
.address-list-page {
background-color: #f5f5f5;
}
.footer-btn {
max-width: 800px;
margin: 0 auto;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
border-radius: 12px 12px 0 0; /* 桌面端加点圆角更美观 */
}
.add-btn {
width: 300px; /* 桌面端限制宽度 */
}
}
</style>

View File

@@ -36,6 +36,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type UserAddress as SupabaseUserAddress } from '@/utils/supabaseService.uts'
type Address = {
id: string
@@ -71,31 +72,42 @@ onMounted(() => {
}
})
const loadAddresses = () => {
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
addresses.value = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
console.error('Failed to parse addresses', e)
const loadAddresses = async () => {
try {
// 从Supabase加载地址数据
const supabaseAddresses = await supabaseService.getAddresses()
// 转换数据格式以匹配前端界面
const transformedAddresses = supabaseAddresses.map((item: SupabaseUserAddress) => ({
id: item.id,
name: item.recipient_name,
phone: item.phone,
province: item.province,
city: item.city,
district: item.district,
detail: item.detail_address,
isDefault: item.is_default,
label: '' // Supabase表没有label字段可以后续考虑添加或使用其他字段
}))
addresses.value = transformedAddresses
// 同时更新本地存储作为缓存
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
} catch (error) {
console.error('加载地址数据失败:', error)
// 如果API调用失败尝试从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
addresses.value = JSON.parse(storedAddresses as string) as Address[]
} catch (e) {
console.error('解析地址数据失败', e)
addresses.value = []
}
} else {
addresses.value = []
}
} else {
// 初始Mock数据
addresses.value = [
{
id: 'addr_001',
name: '张三',
phone: '13800138000',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '三里屯SOHO A座',
isDefault: true,
label: '公司'
}
]
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
}
}
@@ -110,19 +122,32 @@ const addAddress = () => {
}
// 删除地址
const deleteAddress = (id: string) => {
const deleteAddress = async (id: string) => {
uni.showModal({
title: '提示',
content: '确定要删除该地址吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
// 调用Supabase服务删除地址
const success = await supabaseService.deleteAddress(id)
if (success) {
// 从本地列表移除
const index = addresses.value.findIndex(addr => addr.id === id)
if (index !== -1) {
addresses.value.splice(index, 1)
// 更新本地存储缓存
uni.setStorageSync('addresses', JSON.stringify(addresses.value))
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
} else {
console.error('删除地址失败')
uni.showToast({
title: '删除成功',
icon: 'success'
title: '删除失败',
icon: 'none'
})
}
}
@@ -156,6 +181,42 @@ const selectAddress = (item: Address) => {
</script>
<style>
.item-actions {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.footer-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: 10px 15px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
display: flex;
justify-content: center; /* 居中显示 */
align-items: center;
}
.add-btn {
background-color: #ff5000;
color: white;
border-radius: 25px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: none;
width: 100%; /* 默认占满 */
max-width: 100%;
}
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.address-list {
@@ -173,135 +234,11 @@ const selectAddress = (item: Address) => {
left: 50%;
transform: translateX(-50%);
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
border-radius: 12px 12px 0 0; /* 桌面端加点圆角更美观 */
}
.add-btn {
width: 300px; /* 桌面端限制宽度 */
}
}
.address-list-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 80px;
}
.address-list {
padding: 15px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
color: #999;
font-size: 16px;
}
.address-item {
background-color: white;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.item-content {
flex: 1;
margin-right: 15px;
}
.item-header {
display: flex;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
}
.user-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-right: 10px;
}
.user-phone {
font-size: 14px;
color: #666;
margin-right: 10px;
}
.default-tag {
background-color: #ff5000;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-right: 5px;
}
.label-tag {
background-color: #e0f2f1;
color: #00796b;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
}
.address-text {
font-size: 14px;
color: #333;
line-height: 1.4;
}
.item-actions {
padding: 10px;
border-left: 1px solid #f0f0f0;
display: flex;
flex-direction: column; /* 竖向排列图标 */
justify-content: center;
align-items: center;
gap: 15px;
}
.action-item {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.action-icon {
font-size: 20px;
color: #999;
}
.footer-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: 10px 15px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 10px rgba(0,0,0,0.05);
}
.add-btn {
background-color: #ff5000;
color: white;
border-radius: 25px;
font-size: 16px;
height: 44px;
line-height: 44px;
border: none;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,32 @@
</view>
</view>
<!-- 购物车操作栏 (移至推荐商品上方) -->
<view v-if="cartItems.length > 0" class="cart-action-bar">
<view class="action-bar-content">
<view class="action-left">
<view class="select-all" @click="toggleSelectAll">
<text v-if="allSelected" class="selected-icon">✓</text>
<text v-else class="unselected-icon"></text>
<text class="select-all-text">全选</text>
</view>
</view>
<view class="action-right">
<view v-if="!isManageMode" class="total-info">
<text class="total-text">合计:</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<button v-if="!isManageMode" class="checkout-btn" @click="goToCheckout">
去结算({{ selectedCount }})
</button>
<button v-else class="delete-btn" @click="deleteSelectedItems">
删除({{ selectedCount }})
</button>
</view>
</view>
</view>
<!-- 推荐商品 -->
<view v-if="recommendProducts.length > 0" class="recommend-section">
<view class="section-header">
@@ -111,8 +137,8 @@
</view>
</scroll-view>
<!-- 底部结算栏 -->
<view v-if="cartItems.length > 0" class="cart-footer">
<!-- 底部结算栏 - 已移除,移动到内容区域 -->
<!-- <view v-if="cartItems.length > 0" class="cart-footer">
<view class="footer-content">
<view class="footer-left">
<view class="select-all" @click="toggleSelectAll">
@@ -127,8 +153,6 @@
<text class="total-text">合计:</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
<!-- 结算按钮:即使在管理模式下,如果用户想结算也可以(或者在管理模式下隐藏结算,只显示删除) -->
<!-- 需求加购商品前面的按钮时cart购物车页面顶部的管理点击能对加购商品进行批量删除 -->
<button v-if="!isManageMode" class="checkout-btn" @click="goToCheckout">
去结算({{ selectedCount }})
</button>
@@ -137,13 +161,14 @@
</button>
</view>
</view>
</view>
</view> -->
</view>
</template>
<script setup lang="uts">
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { supabaseService, type CartItem as SupabaseCartItem } from '@/utils/supabaseService.uts'
// 响应式数据
const cartItems = ref<any[]>([])
@@ -245,6 +270,12 @@ const totalPrice = computed(() => {
.toFixed(2)
})
// 检查店铺是否全选
const isShopSelected = (shopId: string) => {
const group = cartGroups.value.find(g => g.shopId === shopId)
return group ? group.items.every(item => item.selected) : false
}
const toggleManageMode = () => {
isManageMode.value = !isManageMode.value
}
@@ -265,48 +296,68 @@ onShow(() => {
})
// 加载数据
const loadCartData = () => {
const loadCartData = async () => {
loading.value = true
// 从本地存储加载购物车数据
const cartData = uni.getStorageSync('cart')
if (cartData) {
try {
cartItems.value = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
cartItems.value = []
}
} else {
// 如果本地没有数据使用Mock数据可选或者直接为空
// 为了演示效果这里可以保留一部分Mock数据或者初始化为空
// cartItems.value = [...mockCartItems]
cartItems.value = []
}
setTimeout(() => {
// 模拟推荐商品加载
try {
// 从Supabase加载购物车数据
const supabaseCartItems = await supabaseService.getCartItems()
// 转换数据格式以匹配前端界面
const transformedItems = supabaseCartItems.map((item: SupabaseCartItem) => ({
id: item.id,
shopId: item.shop_id || 'unknown_shop',
shopName: item.shop_name || '未知店铺',
name: item.product_name || '商品',
price: item.product_price || 0,
image: item.product_image || '/static/product1.jpg',
spec: item.product_specification || '默认规格',
quantity: item.quantity || 1,
selected: item.selected || false,
productId: item.product_id // 保留productId用于后续操作
}))
cartItems.value = transformedItems
// 加载推荐商品暂时保持Mock数据
recommendProducts.value = [...mockRecommendProducts]
} catch (error) {
console.error('加载购物车数据失败:', error)
// 如果API调用失败尝试从本地存储加载
const cartData = uni.getStorageSync('cart')
if (cartData) {
try {
cartItems.value = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
cartItems.value = []
}
}
} finally {
loading.value = false
}, 500)
}
}
// 监听购物车数据变化并保存到本地存储
const saveCartData = () => {
uni.setStorageSync('cart', JSON.stringify(cartItems.value))
}
// 商品操作 - 增加保存逻辑
const toggleSelect = (itemId: string) => {
// 商品操作 - 更新选中状态到Supabase
const toggleSelect = async (itemId: string) => {
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
cartItems.value[index].selected = !cartItems.value[index].selected
const newSelected = !cartItems.value[index].selected
cartItems.value[index].selected = newSelected
cartItems.value = [...cartItems.value] // 触发响应式更新
saveCartData()
// 更新到Supabase
const success = await supabaseService.updateCartItemSelection(itemId, newSelected)
if (!success) {
console.error('更新选中状态失败')
// 恢复状态
cartItems.value[index].selected = !newSelected
cartItems.value = [...cartItems.value]
}
}
}
const toggleShopSelect = (shopId: string) => {
const toggleShopSelect = async (shopId: string) => {
const group = cartGroups.value.find((g: any) => g.shopId === shopId)
if (!group) return
@@ -314,56 +365,110 @@ const toggleShopSelect = (shopId: string) => {
const isAllShopSelected = (group.items as any[]).every((item: any) => item.selected)
const newState = !isAllShopSelected
// 更新该店铺下所有商品的状态
cartItems.value.forEach(item => {
if (item.shopId === shopId) {
item.selected = newState
}
})
cartItems.value = [...cartItems.value]
saveCartData()
}
const toggleSelectAll = () => {
const newSelectedState = !allSelected.value
cartItems.value = cartItems.value.map(item => ({
...item,
selected: newSelectedState
}))
saveCartData()
}
const increaseQuantity = (itemId: string) => {
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
cartItems.value[index].quantity++
// 获取该店铺下所有商品的ID
const shopItemIds = cartItems.value
.filter(item => item.shopId === shopId)
.map(item => item.id)
// 批量更新到Supabase
const success = await supabaseService.batchUpdateCartItemSelection(shopItemIds, newState)
if (success) {
// 更新本地状态
cartItems.value.forEach(item => {
if (item.shopId === shopId) {
item.selected = newState
}
})
cartItems.value = [...cartItems.value]
saveCartData()
} else {
console.error('批量更新店铺商品选中状态失败')
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
const decreaseQuantity = (itemId: string) => {
const toggleSelectAll = async () => {
const newSelectedState = !allSelected.value
const selectedItems = cartItems.value.map(item => ({
...item,
selected: newSelectedState
}))
// 更新到Supabase
const itemIds = cartItems.value.map(item => item.id)
const success = await supabaseService.batchUpdateCartItemSelection(itemIds, newSelectedState)
if (success) {
cartItems.value = selectedItems
} else {
console.error('批量更新选中状态失败')
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
}
const increaseQuantity = async (itemId: string) => {
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
const newQuantity = cartItems.value[index].quantity + 1
cartItems.value[index].quantity = newQuantity
cartItems.value = [...cartItems.value]
// 更新到Supabase
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
if (!success) {
console.error('更新商品数量失败')
// 恢复状态
cartItems.value[index].quantity = newQuantity - 1
cartItems.value = [...cartItems.value]
}
}
}
const decreaseQuantity = async (itemId: string) => {
const index = cartItems.value.findIndex(item => item.id === itemId)
if (index !== -1) {
if (cartItems.value[index].quantity > 1) {
cartItems.value[index].quantity--
const newQuantity = cartItems.value[index].quantity - 1
cartItems.value[index].quantity = newQuantity
cartItems.value = [...cartItems.value]
saveCartData()
// 更新到Supabase
const success = await supabaseService.updateCartItemQuantity(itemId, newQuantity)
if (!success) {
console.error('更新商品数量失败')
// 恢复状态
cartItems.value[index].quantity = newQuantity + 1
cartItems.value = [...cartItems.value]
}
} else {
// 数量为1时询问是否删除
uni.showModal({
title: '提示',
content: '确定要从购物车移除该商品吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value]
saveCartData()
uni.showToast({
title: '已移除',
icon: 'none'
})
// 从Supabase删除
const success = await supabaseService.deleteCartItem(itemId)
if (success) {
cartItems.value.splice(index, 1)
cartItems.value = [...cartItems.value]
uni.showToast({
title: '已移除',
icon: 'none'
})
} else {
console.error('删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}
})
@@ -372,7 +477,7 @@ const decreaseQuantity = (itemId: string) => {
}
// 删除商品 - 增加保存逻辑
const deleteSelectedItems = () => {
const deleteSelectedItems = async () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择要删除的商品',
@@ -384,67 +489,66 @@ const deleteSelectedItems = () => {
uni.showModal({
title: '提示',
content: `确定要删除选中的 ${selectedCount.value} 件商品吗?`,
success: (res) => {
success: async (res) => {
if (res.confirm) {
cartItems.value = cartItems.value.filter(item => !item.selected)
saveCartData()
// 获取选中的商品ID
const selectedItemIds = cartItems.value
.filter(item => item.selected)
.map(item => item.id)
// 如果购物车删空了,退出管理模式
if (cartItems.value.length === 0) {
isManageMode.value = false
// 批量删除到Supabase
const success = await supabaseService.batchDeleteCartItems(selectedItemIds)
if (success) {
// 从本地列表移除
cartItems.value = cartItems.value.filter(item => !item.selected)
// 如果购物车删空了,退出管理模式
if (cartItems.value.length === 0) {
isManageMode.value = false
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
} else {
console.error('批量删除商品失败')
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
}
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let currentItems: any[] = []
if (cartData) {
try {
currentItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
const addToCart = async (product: any) => {
try {
// 调用SupabaseService添加商品到购物车
const success = await supabaseService.addToCart(product.id, 1, product.skuId)
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
// 重新加载购物车数据
loadCartData()
} else {
console.error('添加商品到购物车失败')
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
}
// 检查商品是否已存在
const existingItem = currentItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
currentItems.push({
id: product.id,
shopId: product.shopId || 'shop_recommend',
shopName: product.shopName || '推荐好物',
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格', // 优先使用商品自带的规格
quantity: 1,
selected: true
} catch (error) {
console.error('添加商品到购物车异常:', error)
uni.showToast({
title: '添加失败',
icon: 'none'
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(currentItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
// 立即刷新当前列表
loadCartData()
}
// 导航函数
@@ -453,7 +557,21 @@ const goShopping = () => {
}
const navigateToProduct = (product: any) => {
uni.navigateTo({ url: `/pages/mall/consumer/product-detail?id=${product.id}` })
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
const params = new URLSearchParams()
params.append('id', productId)
params.append('productId', productId)
params.append('price', product.price?.toString() || '0')
// 商品详情页期望的参数名是originalPrice
params.append('originalPrice', (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString())
params.append('name', encodeURIComponent(product.name || ''))
params.append('image', encodeURIComponent(product.image || '/static/product1.jpg'))
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?${params.toString()}`
})
}
const goToCheckout = () => {
@@ -465,7 +583,7 @@ const goToCheckout = () => {
return
}
// 获取选中的商品
// 获取选中的商品 (直接过滤cartItems不依赖cartGroups)
const selectedItems = cartItems.value
.filter(item => item.selected)
.map(item => ({
@@ -475,10 +593,14 @@ const goToCheckout = () => {
product_name: item.name,
product_image: item.image,
sku_specifications: item.spec,
price: item.price,
quantity: item.quantity
price: Number(item.price), // 确保是数字
quantity: Number(item.quantity) // 确保是数字
}))
// 关键修复:将结算数据写入 Storage确保 checkout 页面能稳定获取
uni.setStorageSync('checkout_type', 'cart')
uni.setStorageSync('checkout_items', JSON.stringify(selectedItems))
// 跳转到结算页面并传递数据
uni.navigateTo({
url: '/pages/mall/consumer/checkout',
@@ -1059,83 +1181,171 @@ const goToCheckout = () => {
}
}
/* 底部结算栏 */
.cart-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background-color: white;
border-top: 1px solid #eee;
display: flex;
align-items: center;
justify-content: center; /* 居中内容 */
padding: 0 15px;
z-index: 900;
padding-bottom: env(safe-area-inset-bottom);
}
/* 购物车操作栏样式 - 自适应横向排列 */
.cart-action-bar {
background-color: white;
margin: 10px;
border-radius: 12px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.footer-content {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.action-bar-content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.footer-left {
display: flex;
align-items: center;
}
.action-left, .action-right {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.select-all {
display: flex;
align-items: center;
}
.select-all-text {
margin-left: 8px;
font-size: 14px;
color: #333;
}
.footer-right {
display: flex;
align-items: center;
}
.total-info {
margin-right: 15px;
text-align: right;
}
.total-text {
font-size: 14px;
color: #333;
margin-right: 5px;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
}
.checkout-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 8px 20px;
font-size: 14px;
}
.delete-btn {
background-color: #ff3b30; /* 红色删除按钮 */
color: white;
border: none;
border-radius: 25px;
padding: 8px 25px;
font-size: 14px;
}
.action-right {
justify-content: flex-end;
flex: 1;
min-width: 0; /* 防止溢出 */
}
/* 合计信息区域 - 自适应横向排列 */
.total-info {
display: flex;
align-items: center;
margin-right: 12px;
flex-shrink: 0;
}
.total-text {
font-size: 14px;
color: #333;
margin-right: 5px;
white-space: nowrap;
}
.total-price {
font-size: 18px;
color: #ff5000;
font-weight: bold;
white-space: nowrap;
}
/* 结算按钮 */
.checkout-btn, .delete-btn {
background-color: #ff5000;
color: white;
border: none;
border-radius: 25px;
padding: 8px 20px;
font-size: 14px;
white-space: nowrap;
flex-shrink: 0;
}
.delete-btn {
background-color: #ff3b30; /* 红色删除按钮 */
padding: 8px 25px;
}
/* 全选区域 */
.select-all {
display: flex;
align-items: center;
}
.select-all-text {
margin-left: 8px;
font-size: 14px;
color: #333;
white-space: nowrap;
}
/* 响应式调整 */
/* 手机端小屏幕优化 */
@media screen and (max-width: 375px) {
.action-bar-content {
gap: 8px;
}
.total-text {
font-size: 13px;
}
.total-price {
font-size: 16px;
}
.checkout-btn, .delete-btn {
padding: 8px 15px;
font-size: 13px;
}
.select-all-text {
font-size: 13px;
}
}
/* 平板端优化 */
@media screen and (min-width: 768px) {
.cart-action-bar {
margin: 20px auto;
max-width: 95%;
padding: 20px;
}
.action-bar-content {
gap: 20px;
}
.total-price {
font-size: 20px;
}
.checkout-btn, .delete-btn {
padding: 10px 30px;
font-size: 16px;
}
}
/* 桌面端优化 */
@media screen and (min-width: 1024px) {
.cart-action-bar {
max-width: 1200px;
padding: 20px 30px;
}
.action-bar-content {
justify-content: space-between;
}
.total-info {
margin-right: 30px;
}
.total-text {
font-size: 16px;
}
.total-price {
font-size: 22px;
}
.checkout-btn, .delete-btn {
padding: 12px 40px;
font-size: 16px;
}
.select-all-text {
font-size: 16px;
}
}
/* 大屏幕优化 */
@media screen and (min-width: 1400px) {
.cart-action-bar {
max-width: 1400px;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -111,320 +111,94 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import type { Category, Product } from '@/utils/supabaseService.uts'
// 响应式数据
const statusBarHeight = ref(0)
const headerHeight = ref(44) // 默认头部高度
const primaryCategories = ref<any[]>([])
const productList = ref<any[]>([])
const activePrimary = ref<string>('cold')
const primaryCategories = ref<Category[]>([])
const productList = ref<Product[]>([])
const activePrimary = ref<string>('')
const cartCount = ref(3)
const hasMore = ref(true)
const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载
// 获取当前分类信息
const currentCategoryName = ref('感冒发烧')
const currentCategoryDesc = ref('解热镇痛')
const currentCategoryName = ref('')
const currentCategoryDesc = ref('')
// 页面参数
const pageParams = ref<any>({})
// 医药分类数据(与主页一致)
const medicineCategories = [
{ id: 'cold', name: '感冒发烧', icon: '🤧', desc: '解热镇痛', color: '#2196F3' },
{ id: 'stomach', name: '肠胃用药', icon: '🤢', desc: '消化系统', color: '#4CAF50' },
{ id: 'pain', name: '止痛消炎', icon: '💊', desc: '镇痛消炎', color: '#F44336' },
{ id: 'skin', name: '皮肤用药', icon: '🤕', desc: '皮肤护理', color: '#9C27B0' },
{ id: 'vitamin', name: '维生素', icon: '🍊', desc: '营养补充', color: '#FF9800' },
{ id: 'chronic', name: '慢性病', icon: '🫀', desc: '长期管理', color: '#795548' },
{ id: 'child', name: '儿童用药', icon: '👶', desc: '儿童专用', color: '#00BCD4' },
{ id: 'external', name: '外用药品', icon: '🧴', desc: '外用制剂', color: '#8BC34A' },
{ id: 'device', name: '医疗器械', icon: '🩺', desc: '医疗设备', color: '#607D8B' },
{ id: 'health', name: '健康食品', icon: '🥗', desc: '保健食品', color: '#FFC107' }
]
// Mock 商品数据 - 使用本地图片避免网络请求
const mockProducts = {
cold: [
{
id: 'cold1',
shopId: 'shop_001',
shopName: '修正药业官方旗舰店',
name: '布洛芬缓释胶囊',
specification: '0.3g*24粒',
price: 18.5,
originalPrice: 25.8,
image: '/static/images/default-product.png',
manufacturer: '修正药业',
sales: 2560,
badge: '热销'
},
{
id: 'cold2',
shopId: 'shop_002',
shopName: '白云山大药房',
name: '板蓝根颗粒',
specification: '10g*20袋',
price: 22.8,
originalPrice: 29.9,
image: '/static/images/default-product.png',
manufacturer: '白云山',
sales: 1890,
badge: '推荐'
},
{
id: 'cold3',
shopId: 'shop_003',
shopName: '以岭药业自营店',
name: '连花清瘟胶囊',
specification: '0.35g*36粒',
price: 42.8,
originalPrice: 48.0,
image: '/static/images/default-product.png',
manufacturer: '以岭药业',
sales: 3200,
badge: '爆款'
},
{
id: 'cold4',
shopId: 'shop_004',
shopName: '强生制药旗舰店',
name: '对乙酰氨基酚片',
specification: '0.5g*12片',
price: 8.9,
originalPrice: 12.0,
image: '/static/images/default-product.png',
manufacturer: '强生制药',
sales: 1420,
badge: '特价'
},
{
id: 'cold5',
shopId: 'shop_005',
shopName: '同仁堂大药房',
name: '感冒清热颗粒',
specification: '3g*10袋',
price: 16.5,
originalPrice: 19.9,
image: '/static/images/default-product.png',
manufacturer: '同仁堂',
sales: 980,
badge: '新品'
},
{
id: 'cold6',
shopId: 'shop_006',
shopName: '三九医药旗舰店',
name: '复方氨酚烷胺片',
specification: '10片/盒',
price: 12.8,
originalPrice: 15.0,
image: '/static/images/default-product.png',
manufacturer: '三九医药',
sales: 1650,
badge: '家庭装'
}
],
stomach: [
{
id: 'stomach1',
shopId: 'shop_006',
shopName: '三九医药旗舰店',
name: '胃康灵胶囊',
specification: '0.4g*24粒',
price: 32.8,
originalPrice: 38.5,
image: '/static/images/default-product.png',
manufacturer: '三九医药',
sales: 890,
badge: '热销'
},
{
id: 'stomach2',
shopId: 'shop_007',
shopName: '阿斯利康医药',
name: '奥美拉唑肠溶胶囊',
specification: '20mg*14粒',
price: 28.5,
originalPrice: 35.0,
image: '/static/images/default-product.png',
manufacturer: '阿斯利康',
sales: 1250,
badge: '处方药'
},
{
id: 'stomach3',
shopId: 'shop_008',
shopName: '江中制药旗舰店',
name: '健胃消食片',
specification: '0.8g*32片',
price: 15.9,
originalPrice: 19.9,
image: '/static/images/default-product.png',
manufacturer: '江中制药',
sales: 2100,
badge: '推荐'
},
{
id: 'stomach4',
shopId: 'shop_009',
shopName: '益普生大药房',
name: '蒙脱石散',
specification: '3g*10袋',
price: 18.6,
originalPrice: 22.0,
image: '/static/images/default-product.png',
manufacturer: '益普生',
sales: 1780,
badge: '止泻'
},
{
id: 'stomach5',
shopId: 'shop_010',
shopName: '西安杨森旗舰店',
name: '多潘立酮片',
specification: '10mg*30片',
price: 22.8,
originalPrice: 26.5,
image: '/static/images/default-product.png',
manufacturer: '西安杨森',
sales: 950,
badge: '促消化'
},
{
id: 'stomach6',
shopId: 'shop_011',
shopName: '拜耳医药自营店',
name: '铝碳酸镁咀嚼片',
specification: '0.5g*20片',
price: 25.9,
originalPrice: 29.9,
image: '/static/images/default-product.png',
manufacturer: '拜耳',
sales: 1320,
badge: '护胃'
}
],
pain: [
{
id: 'pain1',
shopId: 'shop_012',
shopName: '华北制药旗舰店',
name: '阿莫西林胶囊',
specification: '0.25g*24粒',
price: 28.5,
originalPrice: 35.0,
image: '/static/images/default-product.png',
manufacturer: '华北制药',
sales: 1560,
badge: '处方药'
},
{
id: 'pain2',
shopId: 'shop_013',
shopName: '诺华制药旗舰店',
name: '双氯芬酸钠缓释片',
specification: '75mg*10片',
price: 19.8,
originalPrice: 24.0,
image: '/static/images/default-product.png',
manufacturer: '诺华制药',
sales: 1280,
badge: '止痛'
},
{
id: 'pain3',
shopId: 'shop_014',
shopName: '云南白药旗舰店',
name: '云南白药胶囊',
specification: '0.25g*32粒',
price: 35.9,
originalPrice: 42.0,
image: '/static/images/default-product.png',
manufacturer: '云南白药',
sales: 2350,
badge: '经典'
},
{
id: 'pain4',
shopId: 'shop_015',
shopName: '辉瑞医药旗舰店',
name: '塞来昔布胶囊',
specification: '0.2g*10粒',
price: 48.6,
originalPrice: 55.0,
image: '/static/images/default-product.png',
manufacturer: '辉瑞',
sales: 890,
badge: '抗炎'
},
{
id: 'pain5',
shopId: 'shop_016',
shopName: '中美史克大药房',
name: '布洛芬片',
specification: '0.1g*24片',
price: 12.5,
originalPrice: 15.0,
image: '/static/images/default-product.png',
manufacturer: '中美史克',
sales: 1680,
badge: '经济装'
},
{
id: 'pain6',
shopId: 'shop_002',
shopName: '白云山大药房',
name: '头孢克肟胶囊',
specification: '0.1g*6粒',
price: 32.8,
originalPrice: 38.0,
image: '/static/images/default-product.png',
manufacturer: '广州白云山',
sales: 1120,
badge: '抗生素'
}
]
}
// 补充其他分类的默认数据(简化版)
const generateDefaultProducts = (categoryId: string) => {
const baseProducts = [
{ name: '通用药品1', price: 25.8, manufacturer: '知名药企', sales: 1200 },
{ name: '通用药品2', price: 18.5, manufacturer: '知名药企', sales: 950 },
{ name: '通用药品3', price: 32.0, manufacturer: '知名药企', sales: 1450 },
{ name: '通用药品4', price: 22.8, manufacturer: '知名药企', sales: 880 },
{ name: '通用药品5', price: 28.9, manufacturer: '知名药企', sales: 1100 },
{ name: '通用药品6', price: 19.9, manufacturer: '知名药企', sales: 920 }
]
return baseProducts.map((product, index) => ({
id: `${categoryId}${index + 1}`,
shopId: `shop_default_${categoryId}_${index}`, // 确保不同分类店铺ID不同
shopName: '平台自营大药房',
...product,
specification: '规格待定',
originalPrice: product.price * 1.2,
image: '/static/images/default-product.png',
badge: index === 0 ? '热销' : index === 1 ? '推荐' : ''
}))
}
// 生命周期
onMounted(() => {
initPage()
console.log('=== category页面onMounted被调用 ===')
// 在onMounted中只初始化页面不处理分类参数
// 分类参数的处理交给onLoad函数因为onLoad在页面加载时执行
console.log('onMounted中不处理分类参数等待onLoad处理')
// 注意这里不再默认选择分类让onLoad函数处理分类选择
// 如果onLoad没有设置分类则保持默认状态
onMounted(async() => {
await loadCategories()
// 等待分类加载完成后,再检查是否需要加载默认分类的商品
// 延迟一点时间,确保页面参数处理完成
setTimeout(async () => {
if (!hasLoadedFromParams.value && activePrimary.value) {
await loadProducts()
}
}, 300)
})
// 添加加载分类的方法
const loadCategories = async () => {
try {
const categories = await supabaseService.getCategories()
console.log('加载分类数据成功,数量:', categories.length)
if (categories.length > 0) {
primaryCategories.value = categories
// 如果没有通过参数设置分类,则设置默认选中第一个分类
if (!activePrimary.value && categories[0]) {
activePrimary.value = categories[0].id
console.log('设置默认分类为:', categories[0].name, 'ID:', categories[0].id)
}
} else {
console.warn('从Supabase获取的分类数据为空')
}
} catch (error) {
console.error('加载分类数据失败:', error)
}
}
// 加载商品数据
const loadProducts = async () => {
try {
if (activePrimary.value) {
console.log('开始加载商品分类ID:', activePrimary.value)
const response = await supabaseService.getProductsByCategory(activePrimary.value)
console.log('商品加载结果:', {
dataCount: response.data.length,
total: response.total,
hasmore: response.hasmore
})
productList.value = response.data
hasMore.value = response.hasmore
// 更新当前分类信息
const category = primaryCategories.value.find(cat => cat.id === activePrimary.value)
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.description || ''
console.log('当前分类信息:', category.name, '描述:', category.description)
} else {
console.warn('未找到对应的分类信息分类ID:', activePrimary.value)
}
console.log('商品列表加载完成,数量:', productList.value.length)
} else {
console.warn('activePrimary为空无法加载商品')
}
} catch (error) {
console.error('加载商品数据失败:', error)
productList.value = []
}
}
// 页面加载时处理参数 - 这是处理分类切换的主要入口
onLoad((options: any) => {
console.log('=== category页面onLoad被调用 ===')
@@ -460,6 +234,7 @@ onLoad((options: any) => {
// 如果有找到分类ID则选中对应的分类
if (categoryId) {
hasLoadedFromParams.value = true
console.log('✅ 准备选中分类:', categoryId)
console.log('分类名称:', categoryName || '未指定')
@@ -478,15 +253,8 @@ onLoad((options: any) => {
}, 100)
}
} else {
console.log('⚠️ onLoad中未找到分类参数使用默认分类')
// 默认选中第一个分类
const defaultCategory = 'cold'
console.log('默认分类:', defaultCategory)
// 无论如何都重新加载一次默认分类的数据
setTimeout(() => {
selectPrimaryCategory(defaultCategory)
}, 100)
console.log('⚠️ onLoad中未找到分类参数使用从数据库加载的第一个分类')
// 不再使用硬编码的默认分类loadCategories 会设置第一个分类
}
console.log('=== category页面onLoad执行完成 ===')
@@ -511,6 +279,7 @@ onShow(() => {
// 检查是否有分类参数
if (pageOptions.categoryId) {
hasLoadedFromParams.value = true
const categoryId = pageOptions.categoryId
const categoryName = pageOptions.name || ''
@@ -555,6 +324,7 @@ onShow(() => {
const params = new URLSearchParams(queryString)
const urlCategoryId = params.get('categoryId')
if (urlCategoryId) {
hasLoadedFromParams.value = true
console.log('✅ 从URL解析到分类参数:', urlCategoryId)
selectPrimaryCategory(urlCategoryId)
}
@@ -566,36 +336,22 @@ onShow(() => {
console.log('=== category页面onShow执行完成 ===')
})
// 初始化页面
const initPage = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
// 保持与主页一致的固定高度计算,不进行动态调整
// 这样在移动端会与主页的视觉体验保持一致主页占位符固定为44px
headerHeight.value = 10
// 获取页面参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
pageParams.value = currentPage.options || {}
}
// 加载分类数据
primaryCategories.value = medicineCategories
}
// 选择一级分类
const selectPrimaryCategory = (categoryId: string) => {
const selectPrimaryCategory = async (categoryId: string) => {
console.log('=== selectPrimaryCategory函数开始执行 ===')
console.log('传入的categoryId:', categoryId)
console.log('当前时间:', Date.now())
// 验证categoryId是否有效
if (!categoryId) {
console.error('categoryId为空使用默认分类')
categoryId = 'cold'
console.error('categoryId为空尝试使用第一个分类')
if (primaryCategories.value.length > 0) {
categoryId = primaryCategories.value[0].id
} else {
console.error('没有可用的分类')
return
}
}
console.log('验证后的categoryId:', categoryId)
@@ -606,18 +362,18 @@ const selectPrimaryCategory = (categoryId: string) => {
console.log('更新后的activePrimary:', activePrimary.value)
// 更新当前分类信息
const category = medicineCategories.find(cat => cat.id === categoryId)
const category = primaryCategories.value.find(cat => cat.id === categoryId)
if (category) {
currentCategoryName.value = category.name
currentCategoryDesc.value = category.desc
console.log('✅ 找到分类:', category.name, '描述:', category.desc)
currentCategoryDesc.value = category.description
console.log('✅ 找到分类:', category.name, '描述:', category.description)
} else {
console.error('❌ 未找到分类ID:', categoryId, ',使用默认分类')
console.error('❌ 未找到分类ID:', categoryId, ',使用第一个分类')
// 如果找不到对应的分类,使用第一个分类
if (medicineCategories.length > 0) {
const firstCategory = medicineCategories[0]
if (primaryCategories.value.length > 0) {
const firstCategory = primaryCategories.value[0]
currentCategoryName.value = firstCategory.name
currentCategoryDesc.value = firstCategory.desc
currentCategoryDesc.value = firstCategory.description
activePrimary.value = firstCategory.id
categoryId = firstCategory.id
console.log('使用默认分类:', firstCategory.name)
@@ -626,22 +382,15 @@ const selectPrimaryCategory = (categoryId: string) => {
console.log('准备加载商品数据...')
// 加载对应商品
if (mockProducts[categoryId]) {
productList.value = mockProducts[categoryId]
console.log('✅ 加载mock商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', mockProducts[categoryId].length)
console.log('商品列表:', mockProducts[categoryId])
} else {
productList.value = generateDefaultProducts(categoryId)
console.log('✅ 加载默认商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', productList.value.length)
console.log('商品列表:', productList.value)
}
// 加载对应商品 - 使用 Supabase 服务
const response = await supabaseService.getProductsByCategory(categoryId)
productList.value = response.data
hasMore.value = response.hasmore
hasMore.value = true
console.log('✅ 加载商品数据成功')
console.log('分类:', categoryId)
console.log('商品数量:', response.data.length)
console.log('商品列表:', response.data)
// 验证数据是否已正确更新
console.log('数据更新验证:')
@@ -702,7 +451,7 @@ const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/searc
const navigateToCart = () => uni.navigateTo({ url: '/pages/medicine/cart' })
const navigateToProduct = (product: any) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}`
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}&name=${encodeURIComponent(product.name)}&image=${encodeURIComponent(product.image || '')}`
})
}
@@ -1416,4 +1165,4 @@ const onScan = () => {
font-size: 24px;
}
}
</style>
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -120,8 +120,42 @@
</view>
<scroll-view class="address-list-container" scroll-y>
<!-- 登录提示 -->
<view v-if="!isLoggedIn" class="login-prompt" @click="goToLogin">
<text class="login-prompt-icon">🔒</text>
<text class="login-prompt-text">您尚未登录,点击登录以同步服务器地址</text>
<text class="login-prompt-arrow"></text>
</view>
<!-- 地址列表 -->
<view v-if="addressList.length > 0">
<view v-if="isLoggedIn">
<view v-if="addressList.length > 0">
<view v-for="address in addressList" :key="address.id"
class="popup-address-item" @click="handleSelectAddress(address)">
<view class="popup-address-header">
<text class="popup-address-name">{{ address.recipient_name }}</text>
<text class="popup-address-phone">{{ address.phone }}</text>
<view v-if="address.is_default" class="popup-default-tag">
<text class="popup-tag-text">默认</text>
</view>
</view>
<text class="popup-address-detail">{{ getFullAddress(address) }}</text>
<view v-if="selectedAddress && selectedAddress.id === address.id" class="popup-selected-indicator">
<text>✓</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="popup-empty-address">
<text class="popup-empty-icon">📍</text>
<text class="popup-empty-text">暂无收货地址</text>
</view>
</view>
<!-- 未登录时的本地地址展示 -->
<view v-if="!isLoggedIn && addressList.length > 0">
<text class="local-address-title">本地地址(未同步)</text>
<view v-for="address in addressList" :key="address.id"
class="popup-address-item" @click="handleSelectAddress(address)">
<view class="popup-address-header">
@@ -138,8 +172,8 @@
</view>
</view>
<!-- 状态 -->
<view v-else class="popup-empty-address">
<!-- 完全无地址状态 -->
<view v-if="isLoggedIn && addressList.length === 0" class="popup-empty-address">
<text class="popup-empty-icon">📍</text>
<text class="popup-empty-text">暂无收货地址</text>
</view>
@@ -241,6 +275,7 @@
<script setup lang="uts">
import { ref, onMounted, computed, watch, onUnmounted, getCurrentInstance } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { supabaseService, type UserAddress as SupabaseUserAddress } from '@/utils/supabaseService.uts'
type CheckoutItemType = {
id: string
@@ -394,45 +429,49 @@ onLoad(() => {
// 从上一页获取数据
const eventChannel = uni.getEventChannel ? uni.getEventChannel() : null
// 默认先尝试从本地存储加载(确保有数据)
loadFromLocalStorage()
if (eventChannel) {
eventChannel.on('acceptData', (data: any) => {
console.log('接收到商品数据:', data)
let items = data.selectedItems || []
processCheckoutItems(items)
if (items.length > 0) {
processCheckoutItems(items)
}
loadDefaultAddress()
})
} else {
// 如果没有eventChannel尝试从本地存储加载例如从购物车进入
loadFromLocalStorage()
}
})
// 处理商品数据清洗
const processCheckoutItems = (items: any[]) => {
// 数据清洗:确保价格和数量是数字类型
// 数据清洗:确保价格和数量是数字类型
if (items && items.length > 0) {
items = items.map((item: any) => {
// 确保价格是数字
let price = item.price
if (price !== undefined && price !== null) {
price = typeof price === 'string' ? parseFloat(price) : Number(item.price)
let price = 0
if (item.price !== undefined && item.price !== null) {
price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
if (isNaN(price)) price = 0
} else {
price = 0
}
// 确保数量是数字
let quantity = item.quantity
if (quantity !== undefined && quantity !== null) {
quantity = typeof quantity === 'string' ? parseInt(quantity) : Number(item.quantity)
let quantity = 1
if (item.quantity !== undefined && item.quantity !== null) {
quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
} else {
quantity = 1
}
return {
...item,
price: Number(price.toFixed(2)), // 保留两位小数
id: item.id,
product_id: item.product_id || item.id,
sku_id: item.sku_id || item.id,
product_name: item.product_name || item.name || '',
product_image: item.product_image || item.image || '',
sku_specifications: item.sku_specifications || item.spec || {},
price: Number(price.toFixed(2)),
quantity: quantity
}
})
@@ -463,9 +502,29 @@ onMounted(() => {
})
})
// 页面显示时触发
const onShow = () => {
console.log('checkout页面显示检查登录状态并重新加载地址')
// 检查用户登录状态
const userId = getCurrentUserId()
if (userId) {
console.log('用户已登录,重新加载地址数据')
// 重新加载默认地址和地址列表
loadDefaultAddress()
loadAddressList()
}
}
// 注册页面显示事件
uni.$on('checkoutPageShow', onShow)
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('addressUpdated')
uni.$off('checkoutPageShow')
// 离开页面时清除结算数据,防止下次进入时显示旧数据
uni.removeStorageSync('checkout_type')
uni.removeStorageSync('checkout_items')
})
// 从本地存储加载结算数据(例如从购物车进入)
@@ -479,17 +538,27 @@ const loadFromLocalStorage = () => {
const selectedCartItems = cartItems.filter(item => item.selected === true)
if (selectedCartItems.length > 0) {
// 转换为CheckoutItemType格式
const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => ({
id: item.id,
product_id: item.productId,
sku_id: item.id, // 购物车中item.id就是SKU ID
product_name: item.name,
product_image: item.image,
sku_specifications: item.spec ? { spec: item.spec } : {},
price: item.price,
quantity: item.quantity
}))
checkoutItems.value = convertedItems
const convertedItems: CheckoutItemType[] = selectedCartItems.map(item => {
// 确保价格和数量是数字
let price = typeof item.price === 'string' ? parseFloat(item.price) : Number(item.price)
if (isNaN(price)) price = 0
let quantity = typeof item.quantity === 'string' ? parseInt(item.quantity) : Number(item.quantity)
if (isNaN(quantity) || quantity < 1) quantity = 1
return {
id: item.id,
product_id: item.product_id || item.productId || item.id,
sku_id: item.sku_id || item.id,
product_name: item.name || '',
product_image: item.image || '',
sku_specifications: item.spec ? { spec: item.spec } : {},
price: price,
quantity: quantity
}
})
// 再次经过process处理确保类型正确
processCheckoutItems(convertedItems)
}
} catch (e) {
console.error('解析购物车数据失败:', e)
@@ -506,76 +575,215 @@ const loadCheckoutData = () => {
// 加载默认地址
const loadDefaultAddress = async () => {
// 从本地存储加载地址数据
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
try {
// 首先检查用户是否登录
const currentUserId = getCurrentUserId()
console.log('loadDefaultAddress: 当前用户ID:', currentUserId)
// 如果用户已登录尝试从Supabase加载地址数据
if (currentUserId) {
console.log('loadDefaultAddress: 用户已登录从Supabase加载地址')
const supabaseAddresses = await supabaseService.getAddresses()
console.log('loadDefaultAddress: Supabase返回地址:', supabaseAddresses)
if (supabaseAddresses && supabaseAddresses.length > 0) {
// 查找默认地址
const defaultAddress = addresses.find((addr: any) => addr.isDefault === true)
const defaultAddress = supabaseAddresses.find((addr: SupabaseUserAddress) => addr.is_default === true)
if (defaultAddress) {
// 转换地址格式以匹配selectedAddress的结构
selectedAddress.value = {
id: defaultAddress.id,
recipient_name: defaultAddress.name,
recipient_name: defaultAddress.recipient_name,
phone: defaultAddress.phone,
province: defaultAddress.province,
city: defaultAddress.city,
district: defaultAddress.district,
detail: defaultAddress.detail,
is_default: defaultAddress.isDefault
detail: defaultAddress.detail_address,
is_default: defaultAddress.is_default
}
console.log('loadDefaultAddress: 找到默认地址:', selectedAddress.value)
} else {
// 如果没有默认地址,使用第一个地址
const firstAddress = addresses[0]
const firstAddress = supabaseAddresses[0]
selectedAddress.value = {
id: firstAddress.id,
recipient_name: firstAddress.name,
recipient_name: firstAddress.recipient_name,
phone: firstAddress.phone,
province: firstAddress.province,
city: firstAddress.city,
district: firstAddress.district,
detail: firstAddress.detail,
is_default: firstAddress.isDefault
detail: firstAddress.detail_address,
is_default: firstAddress.is_default
}
console.log('loadDefaultAddress: 使用第一个地址:', selectedAddress.value)
}
// 同时更新本地存储缓存
const localAddresses = supabaseAddresses.map((addr: SupabaseUserAddress) => ({
id: addr.id,
name: addr.recipient_name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail_address,
isDefault: addr.is_default
}))
uni.setStorageSync('addresses', JSON.stringify(localAddresses))
console.log('loadDefaultAddress: 地址已保存到本地存储')
} else {
console.log('loadDefaultAddress: Supabase未返回地址数据')
}
}
// 如果Supabase没有地址数据或用户未登录尝试从本地存储加载
if (!selectedAddress.value) {
console.log('loadDefaultAddress: 尝试从本地存储加载地址')
const storedAddresses = uni.getStorageSync('addresses')
console.log('loadDefaultAddress: 本地存储地址数据:', storedAddresses)
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
// 查找默认地址
const defaultAddress = addresses.find((addr: any) => addr.isDefault === true)
if (defaultAddress) {
selectedAddress.value = {
id: defaultAddress.id,
recipient_name: defaultAddress.name,
phone: defaultAddress.phone,
province: defaultAddress.province,
city: defaultAddress.city,
district: defaultAddress.district,
detail: defaultAddress.detail,
is_default: defaultAddress.isDefault
}
console.log('loadDefaultAddress: 从本地存储找到默认地址:', selectedAddress.value)
} else {
// 如果没有默认地址,使用第一个地址
const firstAddress = addresses[0]
selectedAddress.value = {
id: firstAddress.id,
recipient_name: firstAddress.name,
phone: firstAddress.phone,
province: firstAddress.province,
city: firstAddress.city,
district: firstAddress.district,
detail: firstAddress.detail,
is_default: firstAddress.isDefault
}
console.log('loadDefaultAddress: 从本地存储使用第一个地址:', selectedAddress.value)
}
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
}
}
} catch (err) {
console.error('解析地址数据失败:', err)
}
} else {
// 如果没有地址数据尝试使用Mock数据初始化为了演示效果
const mockAddress = {
id: 'addr_mock_default',
name: '测试用户',
phone: '13800138000',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '三里屯SOHO A座',
isDefault: true
}
uni.setStorageSync('addresses', JSON.stringify([mockAddress]))
selectedAddress.value = {
id: mockAddress.id,
recipient_name: mockAddress.name,
phone: mockAddress.phone,
province: mockAddress.province,
city: mockAddress.city,
district: mockAddress.district,
detail: mockAddress.detail,
is_default: mockAddress.isDefault
}
}
// 如果仍然没有地址,使用模拟地址数据
if (!selectedAddress.value) {
console.log('loadDefaultAddress: 使用模拟地址数据')
// 模拟地址数据
const mockAddresses = [
{
id: 'addr_001',
name: '张三',
phone: '13800138001',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '建国路88号SOHO现代城A座1001',
isDefault: true
},
{
id: 'addr_002',
name: '李四',
phone: '13900139001',
province: '上海市',
city: '上海市',
district: '浦东新区',
detail: '陆家嘴环路1000号汇亚大厦20层',
isDefault: false
}
]
// 保存模拟地址到本地存储
uni.setStorageSync('addresses', JSON.stringify(mockAddresses))
console.log('loadDefaultAddress: 模拟地址已保存到本地存储')
// 使用第一个地址作为默认地址
selectedAddress.value = {
id: mockAddresses[0].id,
recipient_name: mockAddresses[0].name,
phone: mockAddresses[0].phone,
province: mockAddresses[0].province,
city: mockAddresses[0].city,
district: mockAddresses[0].district,
detail: mockAddresses[0].detail,
is_default: mockAddresses[0].isDefault
}
console.log('loadDefaultAddress: 使用模拟地址:', selectedAddress.value)
}
// 如果仍然没有地址selectedAddress.value将保持为null
// 用户可以在结算页面点击地址区域添加新地址
} catch (error) {
console.error('从Supabase加载默认地址失败:', error)
// 失败时从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
const defaultAddress = addresses.find((addr: any) => addr.isDefault === true)
if (defaultAddress) {
selectedAddress.value = {
id: defaultAddress.id,
recipient_name: defaultAddress.name,
phone: defaultAddress.phone,
province: defaultAddress.province,
city: defaultAddress.city,
district: defaultAddress.district,
detail: defaultAddress.detail,
is_default: defaultAddress.isDefault
}
} else {
const firstAddress = addresses[0]
selectedAddress.value = {
id: firstAddress.id,
recipient_name: firstAddress.name,
phone: firstAddress.phone,
province: firstAddress.province,
city: firstAddress.city,
district: firstAddress.district,
detail: firstAddress.detail,
is_default: firstAddress.isDefault
}
}
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
}
}
}
}
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
// 使用 SupabaseService 获取当前用户ID
const userId = supabaseService.getCurrentUserId()
console.log('getCurrentUserId: 从SupabaseService获取到用户ID:', userId)
return userId ?? ''
}
// 用户登录状态
const isLoggedIn = computed(() => {
const userId = getCurrentUserId()
return !!userId
})
// 获取完整地址
const getFullAddress = (address: any): string => {
return `${address.province}${address.city}${address.district}${address.detail}`
@@ -583,32 +791,159 @@ const getFullAddress = (address: any): string => {
// 加载地址列表
const loadAddressList = async () => {
// 从本地存储加载地址数据
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
try {
// 首先检查用户是否登录
const currentUserId = getCurrentUserId()
console.log('loadAddressList: 当前用户ID:', currentUserId)
// 如果用户已登录尝试从Supabase加载地址数据
if (currentUserId) {
console.log('loadAddressList: 用户已登录从Supabase加载地址')
const supabaseAddresses = await supabaseService.getAddresses()
console.log('loadAddressList: Supabase返回地址:', supabaseAddresses)
if (supabaseAddresses && supabaseAddresses.length > 0) {
// 转换地址格式以匹配addressList的结构
addressList.value = addresses.map((addr: any) => ({
addressList.value = supabaseAddresses.map((addr: SupabaseUserAddress) => ({
id: addr.id,
recipient_name: addr.name,
recipient_name: addr.recipient_name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
detail: addr.detail_address,
is_default: addr.is_default
}))
console.log('loadAddressList: 从Supabase加载地址成功数量:', addressList.value.length)
// 同时更新本地存储缓存
const localAddresses = supabaseAddresses.map((addr: SupabaseUserAddress) => ({
id: addr.id,
name: addr.recipient_name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail_address,
isDefault: addr.is_default
}))
uni.setStorageSync('addresses', JSON.stringify(localAddresses))
console.log('loadAddressList: 地址已保存到本地存储')
} else {
console.log('loadAddressList: Supabase未返回地址数据')
}
}
// 如果Supabase没有地址数据或用户未登录尝试从本地存储加载
if (!addressList.value || addressList.value.length === 0) {
console.log('loadAddressList: 尝试从本地存储加载地址')
const storedAddresses = uni.getStorageSync('addresses')
console.log('loadAddressList: 本地存储地址数据:', storedAddresses)
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
// 转换地址格式以匹配addressList的结构
addressList.value = addresses.map((addr: any) => ({
id: addr.id,
recipient_name: addr.name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
}))
console.log('loadAddressList: 从本地存储加载地址成功,数量:', addressList.value.length)
} else {
addressList.value = []
console.log('loadAddressList: 本地存储地址为空数组')
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
addressList.value = []
}
} else {
addressList.value = []
console.log('loadAddressList: 本地存储没有地址数据')
}
} catch (err) {
console.error('解析地址数据失败:', err)
}
// 如果仍然没有地址使用模拟地址数据与loadDefaultAddress保持一致
if (!addressList.value || addressList.value.length === 0) {
console.log('loadAddressList: 使用模拟地址数据')
// 模拟地址数据与loadDefaultAddress中保持一致
const mockAddresses = [
{
id: 'addr_001',
name: '张三',
phone: '13800138001',
province: '北京市',
city: '北京市',
district: '朝阳区',
detail: '建国路88号SOHO现代城A座1001',
isDefault: true
},
{
id: 'addr_002',
name: '李四',
phone: '13900139001',
province: '上海市',
city: '上海市',
district: '浦东新区',
detail: '陆家嘴环路1000号汇亚大厦20层',
isDefault: false
}
]
// 保存模拟地址到本地存储
uni.setStorageSync('addresses', JSON.stringify(mockAddresses))
console.log('loadAddressList: 模拟地址已保存到本地存储')
// 转换为checkout页面格式
addressList.value = mockAddresses.map((addr: any) => ({
id: addr.id,
recipient_name: addr.name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
}))
console.log('loadAddressList: 模拟地址已加载到地址列表,数量:', addressList.value.length)
}
console.log('loadAddressList: 最终地址列表:', addressList.value)
} catch (error) {
console.error('从Supabase加载地址列表失败:', error)
// 失败时从本地存储加载
const storedAddresses = uni.getStorageSync('addresses')
if (storedAddresses) {
try {
const addresses = JSON.parse(storedAddresses as string) as any[]
if (addresses && addresses.length > 0) {
// 转换地址格式以匹配addressList的结构
addressList.value = addresses.map((addr: any) => ({
id: addr.id,
recipient_name: addr.name,
phone: addr.phone,
province: addr.province,
city: addr.city,
district: addr.district,
detail: addr.detail,
is_default: addr.isDefault
}))
} else {
addressList.value = []
}
} catch (err) {
console.error('解析本地地址数据失败:', err)
addressList.value = []
}
} else {
addressList.value = []
}
} else {
addressList.value = []
}
}
@@ -891,65 +1226,89 @@ const selectCoupon = () => {
// 提交订单
const submitOrder = async () => {
if (!selectedAddress.value) {
uni.showToast({
title: '请选择收货地址',
icon: 'none'
})
return
}
// 校验地址
if (!selectedAddress.value) {
uni.showToast({
title: '请选择收货地址',
icon: 'none'
})
return
}
// 校验商品
if (checkoutItems.value.length === 0) {
uni.showToast({
title: '订单中没有商品',
icon: 'none'
})
return
}
uni.showLoading({ title: '提交中...' })
// MOCK ORDER SUBMISSION
// 模拟创建成功
try {
const mockOrderId = `order_${Date.now()}`
const userId = getCurrentUserId()
// 确保使用当前登录用户ID (如果本地存储为空,可能需要处理)
if (!userId) {
uni.hideLoading()
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
// 创建订单对象
const newOrder = {
id: mockOrderId,
order_no: generateOrderNo(),
user_id: getCurrentUserId() || 'user_001',
merchant_id: checkoutItems.value[0]?.product_id || 'merchant_001', // 简化处理取第一个商品的merchant
status: 1, // 待支付
total_amount: totalAmount.value,
discount_amount: discountAmount.value,
delivery_fee: deliveryFee.value,
actual_amount: actualAmount.value,
payment_method: 0,
payment_status: 0,
delivery_address: selectedAddress.value,
items: checkoutItems.value,
created_at: new Date().toISOString()
}
// 准备订单项数据
// 注意:需根据 checkoutItems 的实际结构转换为 createOrder 需要的 CartItem 结构
// 假设 checkoutItems 已经包含了 product_id, quantity, price, name, image 等字段
const orderItems = checkoutItems.value.map((item: any): any => ({
id: item.id || '', // 这是一个临时ID或者购物车IDcreateOrder 中会使用 product_id
product_id: item.product_id || item.id, // 确保有 product_id
quantity: item.quantity,
price: item.price,
product_name: item.name,
product_image: item.image,
spec: item.spec,
checked: true
}))
// 保存到本地存储
const storedOrders = uni.getStorageSync('orders')
let orders: any[] = []
if (storedOrders) {
try {
orders = JSON.parse(storedOrders as string) as any[]
} catch (e) {
console.error('解析订单数据失败', e)
}
}
orders.unshift(newOrder)
uni.setStorageSync('orders', JSON.stringify(orders))
uni.showLoading({ title: '提交中...' })
await new Promise(resolve => setTimeout(resolve, 500))
// 调用 Supabase 服务创建订单
const result = await supabaseService.createOrder(
userId,
selectedAddress.value!.id, // 地址ID
actualAmount.value, // 实付金额
orderItems
)
uni.hideLoading()
// 携带价格详情跳转
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
})
} catch (err) {
console.error('创建订单失败:', err)
uni.showToast({
title: '订单创建失败',
icon: 'none'
})
}
if (result.success) {
// 清除购买的商品 (如果来自购物车,应该在 createOrder 成功后清除,或者这里手动清除本地存储)
// 这里我们假设购物车清理逻辑可能在 createOrder 后端处理,或者需要在这里清除本地
try {
uni.removeStorageSync('checkout_items')
} catch(e) {
console.error('清除结算商品失败', e)
}
const activeOrderId = result.data as string
// 跳转支付页面
uni.navigateTo({
url: `/pages/mall/consumer/payment?orderId=${activeOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}`
})
} else {
throw new Error(result.error)
}
} catch (err: any) {
uni.hideLoading()
console.error('创建订单失败:', err)
uni.showToast({
title: err.message || '订单创建失败',
icon: 'none'
})
}
}
// 生成订单号
@@ -984,6 +1343,7 @@ const clearShoppingCart = async () => {
}
}
// 返回
const goBack = () => {
uni.navigateBack()

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { supabaseService } from '@/utils/supabaseService.uts'
type Product = {
id: string
@@ -48,61 +49,46 @@ onMounted(() => {
loadFavorites()
})
const addToCart = (product: Product) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
const addToCart = async (product: Product) => {
uni.showLoading({ title: '添加中' })
const success = await supabaseService.addToCart(product.id, 1)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_favorite_default',
shopName: product.shopName || '收藏店铺',
name: product.name,
price: product.price,
image: product.image,
spec: '默认规格',
quantity: 1,
selected: true
})
uni.showToast({ title: '添加失败', icon: 'none' })
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
}
const loadFavorites = () => {
// 从本地存储获取收藏列表
const storedFavorites = uni.getStorageSync('favorites')
if (storedFavorites) {
try {
favorites.value = JSON.parse(storedFavorites as string) as Product[]
} catch (e) {
console.error('Failed to parse favorites', e)
favorites.value = []
}
} else {
favorites.value = []
}
const loadFavorites = async () => {
const res = await supabaseService.getFavorites()
// Map response
favorites.value = res.map((item: any): Product => {
const prod = item.ml_products
let image = '/static/default-product.png'
if (prod) {
if (prod.main_image_url) image = prod.main_image_url
else if (prod.image_url) image = prod.image_url
else if (prod.image_urls) {
// Try parse
try {
const arr = JSON.parse(prod.image_urls)
if (Array.isArray(arr) && arr.length > 0) image = arr[0]
} catch(e) {}
}
}
return {
id: prod?.id || item.target_id,
name: prod?.name || '未知商品',
price: prod?.price || 0,
image: image,
sales: prod?.sales || 0,
shopId: '',
shopName: ''
}
})
}
const goShopping = () => {
@@ -111,26 +97,29 @@ const goShopping = () => {
})
}
const goToDetail = (product: Product) => {
const goToDetail = (id: string) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.original_price || ''}`
url: `/pages/mall/consumer/product-detail?productId=${id}`
})
}
const removeFavorite = (id: string) => {
const removeFavorite = async (id: string) => {
uni.showModal({
title: '取消收藏',
content: '确定要取消收藏该商品吗?',
success: (res) => {
success: async (res) => {
if (res.confirm) {
const index = favorites.value.findIndex(item => item.id === id)
if (index !== -1) {
favorites.value.splice(index, 1)
uni.setStorageSync('favorites', JSON.stringify(favorites.value))
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
const success = await supabaseService.toggleFavorite(id) // Toggle removes if exists
if (success) {
// Remove from local list
const index = favorites.value.findIndex(item => item.id === id)
if (index !== -1) {
favorites.value.splice(index, 1)
}
uni.showToast({
title: '已取消收藏',
icon: 'none'
})
}
}
}

View File

@@ -244,85 +244,7 @@
</view>
</view>
<!-- 智能推荐 -->
<view class="smart-recommend">
<view class="section-header">
<view class="title-section">
<text class="section-icon">✨</text>
<text class="section-title">智能推荐</text>
</view>
<view class="recommend-filters">
<text
v-for="filter in recommendFilters"
:key="filter.id"
:class="['filter-item', { active: activeFilter === filter.id }]"
@click="switchFilter(filter.id)"
>
{{ filter.name }}
</text>
</view>
</view>
<view class="recommend-grid">
<view
v-for="product in recommendedProducts"
:key="product.id"
class="recommend-product"
@click="navigateToProduct(product)"
>
<view class="product-image-container">
<image
class="product-image"
:src="product.image"
mode="aspectFill"
/>
<view class="product-tags">
<text v-if="product.tag" class="product-tag">{{ product.tag }}</text>
<text v-if="product.featured" class="featured-tag">{{ product.featured }}</text>
</view>
</view>
<view class="product-details">
<text class="product-title">{{ product.name }}</text>
<text class="product-specification">{{ product.specification }}</text>
<view class="product-rating">
<view class="rating-stars">
<text class="star-icon">⭐</text>
<text class="rating-value">{{ product.rating }}</text>
</view>
<text class="reviews-count">{{ product.reviews }}条评价</text>
</view>
<view class="price-section">
<view class="current-price">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
</view>
<text class="original-price" v-if="product.originalPrice > product.price">
¥{{ product.originalPrice }}
</text>
</view>
<view class="product-actions">
<view class="add-to-cart" @click.stop="addToCart(product)">
<text class="cart-icon">🛒</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && recommendedProducts.length > 0" class="no-more">
<text class="no-more-text">--- 已加载全部内容 ---</text>
</view>
</view>
<!-- 智能推荐模块已隐藏 -->
<!-- 健康提醒 -->
<view class="health-reminder">
@@ -346,21 +268,25 @@
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import supabaseService from '@/utils/supabaseService.uts'
import type { Product, Category } from '@/utils/supabaseService.uts'
import { getCurrentUser } from '@/utils/store.uts'
// 响应式数据
const statusBarHeight = ref(0)
const scrollHeight = ref(0)
const refreshing = ref(false)
const loading = ref(false)
const isFirstShow = ref(true)
const hasMore = ref(true)
const activeSort = ref('sales')
const activeFilter = ref('recommend')
const currentPage = ref(1)
// 数据源
const allProducts = ref<any[]>([])
const hotProducts = ref<any[]>([])
const recommendedProducts = ref<any[]>([])
const hotProducts = ref<Product[]>([])
const recommendedProducts = ref<Product[]>([])
// 屏幕尺寸检测
const isMobile = ref(false)
@@ -371,35 +297,18 @@ const lastScrollTop = ref(0)
const scrollThreshold = 30 // 降低滚动阈值,使其更灵敏
const scrollingUp = ref(false)
// 分类数据
const categories = [
{ id: 'cold', name: '感冒发烧', icon: '🤧', desc: '解热镇痛', color: '#2196F3' },
{ id: 'stomach', name: '肠胃用药', icon: '🤢', desc: '消化系统', color: '#4CAF50' },
{ id: 'pain', name: '止痛消炎', icon: '💊', desc: '镇痛消炎', color: '#F44336' },
{ id: 'skin', name: '皮肤用药', icon: '🤕', desc: '皮肤护理', color: '#9C27B0' },
{ id: 'vitamin', name: '维生素', icon: '🍊', desc: '营养补充', color: '#FF9800' },
{ id: 'chronic', name: '慢性病', icon: '🫀', desc: '长期管理', color: '#795548' },
{ id: 'child', name: '儿童用药', icon: '👶', desc: '儿童专用', color: '#00BCD4' },
{ id: 'external', name: '外用药品', icon: '🧴', desc: '外用制剂', color: '#8BC34A' },
{ id: 'device', name: '医疗器械', icon: '🩺', desc: '医疗设备', color: '#607D8B' },
{ id: 'health', name: '健康食品', icon: '🥗', desc: '保健食品', color: '#FFC107' }
]
// 分类数据 - 从Supabase获取
const categories = ref<Category[]>([])
// 排序标签
const sortTabs = [
{ id: 'recommend', name: '智能推荐' },
{ id: 'sales', name: '销量' },
{ id: 'price', name: '价格' },
{ id: 'new', name: '新品' },
{ id: 'recommend', name: '推荐' }
{ id: 'discount', name: '特价' }
]
// 推荐筛选器
const recommendFilters = [
{ id: 'recommend', name: '智能推荐' },
{ id: 'hot', name: '热门商品' },
{ id: 'discount', name: '限时优惠' },
{ id: 'quality', name: '品质优选' }
]
// 健康资讯
const healthNews = [
@@ -423,100 +332,96 @@ const healthNews = [
}
]
// 获取分类数据
const loadCategories = async () => {
try {
const categoriesData = await supabaseService.getCategories()
// 映射字段根据ml_categories表结构映射
categories.value = categoriesData.map((cat: any) => ({
id: cat.id,
name: cat.name,
icon: cat.icon_url || '📦', // 使用icon_url字段
desc: cat.description || '', // 使用description字段
color: '#4CAF50' // 默认颜色表中可能没有color字段
}))
} catch (error) {
console.error('加载分类数据失败:', error)
// 如果加载失败,使用默认分类作为后备
categories.value = [
{ id: 'cold', name: '感冒发烧', icon: '🤧', desc: '解热镇痛', color: '#2196F3' },
{ id: 'stomach', name: '肠胃用药', icon: '🤢', desc: '消化系统', color: '#4CAF50' },
{ id: 'pain', name: '止痛消炎', icon: '💊', desc: '镇痛消炎', color: '#F44336' },
{ id: 'skin', name: '皮肤用药', icon: '🤕', desc: '皮肤护理', color: '#9C27B0' },
{ id: 'vitamin', name: '维生素', icon: '🍊', desc: '营养补充', color: '#FF9800' }
]
}
}
// 获取热销商品(根据当前排序方式)
const loadHotProducts = async () => {
try {
let products: Product[] = []
const limit = 6
console.log('加载热销商品,当前排序方式:', activeSort.value)
switch (activeSort.value) {
case 'sales':
console.log('调用 getHotProducts')
products = await supabaseService.getHotProducts(limit)
break
case 'price':
console.log('调用 getProductsByPrice')
// 按价格升序(从低到高)
products = await supabaseService.getProductsByPrice(limit, true)
break
case 'new':
console.log('调用 getProductsByNewest')
// 按创建时间,最新的在前
products = await supabaseService.getProductsByNewest(limit)
break
case 'recommend':
console.log('调用 getRecommendedProducts')
// 推荐商品带badge的商品
products = await supabaseService.getRecommendedProducts(limit)
break
case 'discount':
console.log('调用 getDiscountProducts')
// 特价商品badge为'特价'
products = await supabaseService.getDiscountProducts(limit)
break
default:
console.log('调用默认 getHotProducts')
products = await supabaseService.getHotProducts(limit)
}
console.log('加载到的商品数量:', products.length)
hotProducts.value = products
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
}
}
// 获取推荐商品
const loadRecommendedProducts = async (limit: number = 6) => {
recommendedProducts.value = await supabaseService.getRecommendedProducts(limit)
}
// 初始化数据
const initData = () => {
const manufacturers = ['修正药业', '白云山', '养生堂', '三九医药', '同仁堂', '云南白药', '拜耳', '辉瑞']
const names = ['布洛芬', '板蓝根', '维生素C', '胃康灵', '阿莫西林', '连花清瘟', '氨溴索', '氯雷他定', '感冒灵', '健胃消食片', '阿司匹林', '蒙脱石散']
const tags = ['处方药', '中成药', '止咳化痰', '抗过敏', '感冒发烧', '肠胃用药', '消炎镇痛']
const featureds = ['医生推荐', '热销爆款', '家庭必备', '季节必备', '店长推荐']
const products = [] as any[]
for (let i = 0; i < 50; i++) {
const nameIdx = Math.floor(Math.random() * names.length)
const name = names[nameIdx]
const price = parseFloat((10 + Math.random() * 100).toFixed(1))
const originalPrice = parseFloat((price * (1.1 + Math.random() * 0.5)).toFixed(1))
const sales = Math.floor(Math.random() * 5000)
// 随机店铺ID避免全部是同一家
const randomShopSuffix = Math.floor(Math.random() * 20) + 1
products.push({
id: `prod_${i}`,
shopId: `shop_${randomShopSuffix}`,
shopName: manufacturers[Math.floor(Math.random() * manufacturers.length)] + '官方旗舰店',
name: name + (Math.random() > 0.5 ? '胶囊' : '颗粒'),
specification: Math.random() > 0.5 ? '0.3g*24粒' : '10g*10袋',
price: price,
originalPrice: originalPrice,
image: '/static/images/default-product.png',
manufacturer: manufacturers[Math.floor(Math.random() * manufacturers.length)],
sales: sales,
rating: (3.5 + Math.random() * 1.5).toFixed(1),
reviews: Math.floor(Math.random() * 500),
tag: tags[Math.floor(Math.random() * tags.length)],
featured: Math.random() > 0.7 ? featureds[Math.floor(Math.random() * featureds.length)] : '',
badge: sales > 3000 ? '热销' : (price < 20 ? '特价' : (Math.random() > 0.8 ? '新品' : '')),
// Attributes for filtering
isNew: Math.random() > 0.8,
isRecommend: Math.random() > 0.6,
isHot: sales > 2000,
isDiscount: (originalPrice - price) > 15,
isQuality: price > 60
})
const initData = async () => {
// 首先确保用户资料已加载
try {
await getCurrentUser()
console.log('主页初始化:用户资料加载完成')
} catch (error) {
console.error('加载用户资料失败:', error)
}
allProducts.value = products
filterHotProducts()
filterRecommendedProducts()
await loadCategories()
await loadHotProducts()
await loadRecommendedProducts()
}
// 筛选热销商品
const filterHotProducts = () => {
let list = [...allProducts.value]
if (activeSort.value === 'sales') {
list.sort((a, b) => b.sales - a.sales)
} else if (activeSort.value === 'price') {
list.sort((a, b) => a.price - b.price)
} else if (activeSort.value === 'new') {
list = list.filter(p => p.isNew)
} else if (activeSort.value === 'recommend') {
list = list.filter(p => p.isRecommend)
}
// 如果筛选后数量不足4个补足
if (list.length < 4) {
const remaining = allProducts.value.filter(p => !list.includes(p))
list = [...list, ...remaining]
}
hotProducts.value = list.slice(0, 4)
}
// 筛选推荐商品
const filterRecommendedProducts = () => {
let list = [...allProducts.value]
if (activeFilter.value === 'hot') {
list = list.filter(p => p.isHot)
} else if (activeFilter.value === 'discount') {
list = list.filter(p => p.isDiscount)
} else if (activeFilter.value === 'quality') {
list = list.filter(p => p.isQuality)
} else {
// 默认随机排序
list.sort(() => Math.random() - 0.5)
}
// 如果筛选后数量不足4个补足
if (list.length < 4) {
const remaining = allProducts.value.filter(p => !list.includes(p))
list = [...list, ...remaining]
}
recommendedProducts.value = list.slice(0, 4)
}
// 家庭常备药
const familyItems = [
@@ -571,9 +476,9 @@ const familyItems = [
]
// 生命周期
onMounted(() => {
onMounted(async () => {
initPage()
initData()
await initData()
})
// 页面显示时重置状态
@@ -593,6 +498,22 @@ onShow(() => {
// 让分类页面在成功读取后自行清除
// 这样可以确保分类页面能正确读取到传递的数据
// 每次页面显示时尝试更新用户资料
if (!isFirstShow.value) {
getCurrentUser().then(profile => {
if (profile) {
console.log('主页onShow用户资料更新成功')
} else {
console.log('主页onShow用户资料为空可能未登录')
}
}).catch(error => {
console.error('主页onShow加载用户资料失败:', error)
})
} else {
isFirstShow.value = false
console.log('主页首次显示跳过onShow中的用户资料检查交由initData处理')
}
console.log('=== index页面onShow执行完成 ===')
})
@@ -688,13 +609,15 @@ const switchCategory = (category: any) => {
// 切换排序
const switchSort = (sortId: string) => {
activeSort.value = sortId
filterHotProducts()
// 重新加载热销商品,排序由 Supabase 服务处理
loadHotProducts()
}
// 切换筛选器
const switchFilter = (filterId: string) => {
activeFilter.value = filterId
filterRecommendedProducts()
// 重新加载推荐商品,筛选由 Supabase 服务处理
loadRecommendedProducts()
}
// 查看新闻详情
@@ -717,27 +640,33 @@ const onRefresh = () => {
}
// 加载更多
const loadMore = () => {
const loadMore = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
setTimeout(() => {
// 模拟加载更多数据
const newProducts = [...recommendedProducts].map((item, index) => ({
...item,
id: `new${index}`,
price: Math.floor(item.price * 0.9 + Math.random() * 10)
}))
try {
// 增加限制以加载更多推荐商品
const currentLimit = recommendedProducts.value.length + 6
await loadRecommendedProducts(currentLimit)
// 实际项目中应该合并数据
loading.value = false
hasMore.value = recommendedProducts.length < 20
// 假设如果返回的商品数量小于请求的限制,则没有更多数据
if (recommendedProducts.value.length < currentLimit) {
hasMore.value = false
}
uni.showToast({
title: '加载完成',
icon: 'success'
})
}, 2000)
} catch (error) {
console.error('加载更多失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 添加到购物车
@@ -787,8 +716,20 @@ const addToCart = (product: any) => {
const navigateToSearch = () => uni.navigateTo({ url: '/pages/mall/consumer/search' })
const navigateToNews = () => uni.navigateTo({ url: '/pages/news/list' })
const navigateToProduct = (product: any) => {
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${product.id}&price=${product.price}&originalPrice=${product.originalPrice || ''}`
// 使用productId如果存在作为跳转的商品ID否则使用id
const productId = product.productId || product.id
// 传递完整的参数,确保商品详情页能正确加载
// 移除 URLSearchParams 内部的 encodeURIComponent因为 append 会自动编码
// 或者直接构建 URL 字符串以确保兼容性
const name = product.name || ''
const image = product.image || '/static/product1.jpg'
const price = product.price?.toString() || '0'
const originalPrice = (product.original_price || product.originalPrice || (product.price * 1.2).toFixed(2))?.toString()
// 手动构建URL避免双重编码问题
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?id=${productId}&productId=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
})
}
const navigateToCategory = (item: any) => {

File diff suppressed because it is too large Load Diff

View File

@@ -156,10 +156,11 @@
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
// // import supa from '@/components/supadb/aksupainstance.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
// 响应式数据
const orders = ref<any[]>([])
const allOrdersList = ref<any[]>([]) // Store all fetched orders for client-side filtering
const loading = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const hasMore = ref<boolean>(true)
@@ -170,144 +171,16 @@ const searchKeyword = ref<string>('')
// 订单标签页
const orderTabs = reactive([
{ id: 'all', name: '全部', count: 12 },
{ id: 'pending', name: '待付款', count: 2 },
{ id: 'shipping', name: '待发货', count: 1 },
{ id: 'delivering', name: '待收货', count: 3 },
{ id: 'completed', name: '已完成', count: 5 },
{ id: 'cancelled', name: '已取消', count: 1 }
{ id: 'all', name: '全部', count: 0 },
{ id: 'pending', name: '待付款', count: 0 },
{ id: 'shipping', name: '待发货', count: 0 },
{ id: 'delivering', name: '待收货', count: 0 },
{ id: 'completed', name: '已完成', count: 0 },
{ id: 'cancelled', name: '已取消', count: 0 }
])
// Mock 订单数据
const mockOrders = [
{
id: '202311230001',
order_no: '202311230001',
status: 1, // 1:待付款 2:待发货 3:待收货 4:已完成 5:已取消
create_time: '2023-11-23 14:30:22',
product_amount: 378.00,
shipping_fee: 0.00,
total_amount: 378.00,
products: [
{
id: '1001',
name: '无线蓝牙耳机 降噪版',
price: 299.00,
image: 'https://picsum.photos/80/80?random=1',
spec: '白色',
quantity: 1
},
{
id: '1002',
name: '耳机保护套',
price: 29.00,
image: 'https://picsum.photos/80/80?random=2',
spec: '黑色',
quantity: 1
},
{
id: '1003',
name: '数据线',
price: 19.00,
image: 'https://picsum.photos/80/80?random=3',
spec: '1米',
quantity: 2
}
]
},
{
id: '202311220001',
order_no: '202311220001',
status: 2,
create_time: '2023-11-22 10:15:33',
product_amount: 199.00,
shipping_fee: 10.00,
total_amount: 209.00,
products: [
{
id: '2001',
name: '运动T恤 速干面料',
price: 79.00,
image: 'https://picsum.photos/80/80?random=4',
spec: '黑色 L',
quantity: 2
},
{
id: '2002',
name: '运动短裤',
price: 59.00,
image: 'https://picsum.photos/80/80?random=5',
spec: '黑色 M',
quantity: 1
}
]
},
{
id: '202311210001',
order_no: '202311210001',
status: 3,
create_time: '2023-11-21 16:45:12',
product_amount: 299.00,
shipping_fee: 0.00,
total_amount: 299.00,
products: [
{
id: '3001',
name: '智能手环 心率监测',
price: 199.00,
image: 'https://picsum.photos/80/80?random=6',
spec: '黑色',
quantity: 1
},
{
id: '3002',
name: '手环腕带',
price: 29.00,
image: 'https://picsum.photos/80/80?random=7',
spec: '蓝色',
quantity: 2
}
]
},
{
id: '202311200001',
order_no: '202311200001',
status: 4,
create_time: '2023-11-20 09:30:45',
product_amount: 99.00,
shipping_fee: 0.00,
total_amount: 99.00,
products: [
{
id: '4001',
name: '保温杯 500ml',
price: 49.00,
image: 'https://picsum.photos/80/80?random=8',
spec: '白色',
quantity: 2
}
]
},
{
id: '202311190001',
order_no: '202311190001',
status: 5,
create_time: '2023-11-19 14:20:18',
product_amount: 599.00,
shipping_fee: 0.00,
total_amount: 599.00,
products: [
{
id: '5001',
name: '蓝牙音箱 便携式',
price: 199.00,
image: 'https://picsum.photos/80/80?random=9',
spec: '黑色',
quantity: 3
}
]
}
]
// Removed Mock Data
// 计算属性:根据当前标签筛选订单
const filteredOrders = computed(() => {
@@ -350,83 +223,79 @@ onShow(() => {
// 加载订单数据
const loadOrders = async () => {
loading.value = true
const userStore = uni.getStorageSync('userInfo')
const userId = userStore?.id
if (!userId) {
loading.value = false
return
}
try {
// 从本地存储获取订单
const ordersStr = uni.getStorageSync('orders')
let localOrders: any[] = []
if (ordersStr) {
localOrders = JSON.parse(ordersStr as string) as any[]
}
// Fetch all orders from Supabase (status=0)
const fetchedOrders = await supabaseService.getOrders(0)
// 过滤当前用户的订单
// const userOrders = localOrders.filter((o: any) => o.user_id === userId)
// 暂时显示所有订单用于测试
let userOrders = localOrders
// 根据标签页过滤
let filtered = userOrders
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value !== 'all') {
const targetStatus = statusMap[activeTab.value]
filtered = userOrders.filter((o: any) => o.status === targetStatus)
}
// 按时间倒序
filtered.sort((a: any, b: any) => {
const timeA = new Date(a.created_at || a.create_time).getTime()
const timeB = new Date(b.created_at || b.create_time).getTime()
return timeB - timeA
})
// 处理数据格式以适配当前页面
orders.value = filtered.map((order: any) => ({
// Map to View Model
const mappedOrders = fetchedOrders.map((order: any) => ({
id: order.id,
order_no: order.order_no,
status: order.status,
create_time: order.created_at || order.create_time,
product_amount: order.total_amount,
status: order.order_status,
create_time: order.created_at,
product_amount: order.product_amount || order.actual_amount,
shipping_fee: order.delivery_fee,
total_amount: order.actual_amount,
products: (order.items || order.products || []).map((item: any) => ({
id: item.product_id || item.id,
name: item.product_name || item.name,
products: (order.ml_order_items || []).map((item: any) => ({
id: item.product_id,
name: item.product_name,
price: item.price,
image: item.product_image || item.image || '/static/default-product.png',
spec: item.sku_specifications ? formatSpec(item.sku_specifications) : (item.spec || ''),
image: item.product_image,
spec: item.spec || '',
quantity: item.quantity
}))
}))
// Sort by created_at desc
mappedOrders.sort((a: any, b: any) => {
const timeA = new Date(a.create_time).getTime()
const timeB = new Date(b.create_time).getTime()
return timeB - timeA
})
allOrdersList.value = mappedOrders
// 更新统计数据
orderTabs[0].count = userOrders.length
orderTabs[1].count = userOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = userOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = userOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = userOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = userOrders.filter((o: any) => o.status === 5).length
// Update tab counts
updateTabsCounts(mappedOrders)
// Apply current tab filter
filterOrdersByTab()
} catch (err) {
console.error('加载订单异常:', err)
uni.showToast({ title: '加载订单失败', icon: 'none' })
} finally {
loading.value = false
}
}
const updateTabsCounts = (allOrders: any[]) => {
orderTabs[0].count = allOrders.length
orderTabs[1].count = allOrders.filter((o: any) => o.status === 1).length
orderTabs[2].count = allOrders.filter((o: any) => o.status === 2).length
orderTabs[3].count = allOrders.filter((o: any) => o.status === 3).length
orderTabs[4].count = allOrders.filter((o: any) => o.status === 4).length
orderTabs[5].count = allOrders.filter((o: any) => o.status === 5).length
}
const filterOrdersByTab = () => {
const statusMap: Record<string, number> = {
'pending': 1,
'shipping': 2,
'delivering': 3,
'completed': 4,
'cancelled': 5
}
if (activeTab.value === 'all') {
orders.value = allOrdersList.value
} else {
const targetStatus = statusMap[activeTab.value]
orders.value = allOrdersList.value.filter((o: any) => o.status === targetStatus)
}
}
const formatDate = (isoString: string): string => {
if (!isoString) return ''
const date = new Date(isoString)
@@ -477,9 +346,7 @@ const performSearch = () => {
}
const getCurrentOrderData = () => {
// 这里应该从本地存储或API获取完整订单数据
// 暂时返回当前orders.value
return orders.value
return allOrdersList.value
}
const formatSpec = (specs: any): string => {
@@ -493,9 +360,7 @@ const formatSpec = (specs: any): string => {
// 切换标签
const switchTab = (tabId: string) => {
activeTab.value = tabId
page.value = 1
orders.value = []
loadOrders()
filterOrdersByTab()
}
// 获取状态文本

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<view class="product-detail-page">
<!-- 商品图片轮播 -->
<view class="product-images">
<swiper class="image-swiper" :indicator-dots="true" :autoplay="false">
<swiper class="image-swiper" :indicator-dots="true" :autoplay="false" @change="onSwiperChange">
<swiper-item v-for="(image, index) in product.images" :key="index">
<image :src="image" class="product-image" mode="aspectFit" />
</swiper-item>
@@ -34,6 +34,23 @@
<text class="enter-shop" @click.stop="goToShop">进店 ></text>
</view>
<!-- 功能主治(药品功能) -->
<view class="function-section" v-if="product.usage">
<text class="function-title">功能主治</text>
<text class="function-content">{{ product.usage }}</text>
</view>
<!-- 商品参数 -->
<view class="params-section" @click="showParamsModal">
<text class="params-title">商品参数</text>
<view class="params-summary">
<text class="params-item" v-if="product.specification">规格: {{ product.specification }}</text>
<text class="params-item" v-if="product.expiry_date">有效期: {{ product.expiry_date }}</text>
<text class="params-item" v-if="product.approval_number">批准文号: {{ product.approval_number }}</text>
</view>
<text class="params-arrow">></text>
</view>
<!-- 规格选择 -->
<view class="spec-section" @click="showSpecModal">
<text class="spec-title">规格</text>
@@ -65,6 +82,15 @@
<view class="product-description">
<view class="section-title">商品详情</view>
<text class="description-text">{{ product.description || '暂无详细描述' }}</text>
<!-- 商品详情图片 -->
<view class="detail-images" v-if="product.images && product.images.length > 0">
<image v-for="(img, index) in product.images"
:key="index"
:src="img"
class="detail-image"
mode="widthFix"
@click="previewImage(index)" />
</view>
</view>
<!-- 底部操作栏 -->
@@ -104,11 +130,56 @@
</view>
</view>
</view>
<!-- 商品参数弹窗 -->
<view v-if="showParams" class="params-modal" @click="hideParamsModal">
<view class="params-content" @click.stop>
<view class="params-header">
<text class="params-title">商品参数</text>
<text class="close-btn" @click="hideParamsModal">×</text>
</view>
<view class="params-list">
<view class="params-item" v-if="product.specification">
<text class="params-label">规格</text>
<text class="params-value">{{ product.specification }}</text>
</view>
<view class="params-item" v-if="product.usage">
<text class="params-label">功能主治</text>
<text class="params-value">{{ product.usage }}</text>
</view>
<view class="params-item" v-if="product.side_effects">
<text class="params-label">副作用</text>
<text class="params-value">{{ product.side_effects }}</text>
</view>
<view class="params-item" v-if="product.precautions">
<text class="params-label">注意事项</text>
<text class="params-value">{{ product.precautions }}</text>
</view>
<view class="params-item" v-if="product.expiry_date">
<text class="params-label">有效期</text>
<text class="params-value">{{ product.expiry_date }}</text>
</view>
<view class="params-item" v-if="product.storage_conditions">
<text class="params-label">储存条件</text>
<text class="params-value">{{ product.storage_conditions }}</text>
</view>
<view class="params-item" v-if="product.approval_number">
<text class="params-label">批准文号</text>
<text class="params-value">{{ product.approval_number }}</text>
</view>
<view class="params-item" v-if="product.tags && product.tags.length > 0">
<text class="params-label">标签</text>
<text class="params-value">{{ product.tags.join(', ') }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { ProductType, MerchantType, ProductSkuType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
export default {
data() {
@@ -147,15 +218,34 @@ export default {
selectedSkuId: '',
selectedSpec: '',
quantity: 1,
isFavorite: false
isFavorite: false,
showParams: false
}
},
onLoad(options: any) {
const productId = options.productId as string || options.id as string
const productPrice = options.price ? parseFloat(options.price) : null
const productOriginalPrice = options.original_price ? parseFloat(options.original_price) : null
const productName = options.name as string
const productImage = options.image as string
const productOriginalPrice = options.originalPrice ? parseFloat(options.originalPrice) : null
// 处理商品名称:如果是编码的则解码,否则直接使用
let productName = options.name as string
if (productName) {
try {
// 尝试解码如果失败不是有效的URI组件则使用原值
productName = decodeURIComponent(productName)
} catch (e) {
console.warn('ProductName decode failed, using original:', productName)
}
}
let productImage = options.image as string
if (productImage) {
try {
productImage = decodeURIComponent(productImage)
} catch (e) {
console.warn('ProductImage decode failed, using original:', productImage)
}
}
if (productId) {
this.loadProductDetail(productId, {
@@ -166,6 +256,13 @@ export default {
})
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
// 设置导航栏标题为商品名称
if (productName) {
uni.setNavigationBarTitle({
title: productName
})
}
}
},
computed: {
@@ -210,106 +307,380 @@ export default {
if (footprints.length > 50) {
footprints = footprints.slice(0, 50)
}
uni.setStorageSync('footprints', JSON.stringify(footprints))
},
loadProductDetail(productId: string, options: any = {}) {
// 根据商品ID生成一个基础价格如果没有传入价格
const generatePriceFromId = (id: string): number => {
// 简单哈希函数将字符串转换为一个在50-500之间的价格
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0 // 转换为32位整数
async loadProductDetail(productId: string, options: any = {}) {
// 尝试从数据库加载
let dbProductRaw = null
try {
console.log('正在尝试从数据库加载商品详情:', productId)
dbProductRaw = await supabaseService.getProductById(productId)
console.log('数据库返回的商品详情 (原始数据):', dbProductRaw)
// 调试:打印数据库返回的所有字段
if (dbProductRaw) {
console.log('数据库返回字段详情:')
if (Array.isArray(dbProductRaw)) {
console.log('返回数据是数组,长度:', dbProductRaw.length)
if (dbProductRaw.length > 0) {
const firstItem = dbProductRaw[0]
console.log('数组第一个元素:', firstItem)
for (const key in firstItem) {
console.log(` ${key}:`, firstItem[key], typeof firstItem[key])
}
}
} else {
console.log('返回数据是对象')
for (const key in dbProductRaw) {
console.log(` ${key}:`, dbProductRaw[key], typeof dbProductRaw[key])
}
}
}
// 将哈希值映射到50-500之间
const price = 50 + Math.abs(hash % 450)
// 保留两位小数
return parseFloat(price.toFixed(2))
} catch (e) {
console.error('Failed to load product from DB', e)
}
// 优先使用传入的参数否则根据商品ID生成价格
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
// 原价比现价高20%左右
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
// 根据商品ID生成不同的商品名称使其更真实
const productNames = [
'高品质运动休闲鞋',
'时尚简约双肩背包',
'多功能智能手环',
'便携式蓝牙音箱',
'全自动雨伞',
'抗菌防螨床上四件套',
'不锈钢保温杯',
'无线充电器',
'高清行车记录仪',
'智能体脂秤'
]
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
const productName = options.name ? options.name : productNames[nameIndex]
// 模拟销量和库存,使其更真实
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [
'/static/product1.jpg',
'/static/product2.jpg',
'/static/product3.jpg'
],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
// 处理数据库返回数据:可能是数组或对象
let dbProduct = null
if (dbProductRaw) {
if (Array.isArray(dbProductRaw)) {
if (dbProductRaw.length > 0) {
dbProduct = dbProductRaw[0] // 取数组第一个元素
} else {
console.warn('数据库返回空数组')
}
} else {
dbProduct = dbProductRaw // 已经是对象
}
}
if (dbProduct) {
console.log('使用数据库数据渲染页面')
// 调试打印dbProduct的详细结构和类型
console.log('dbProduct类型:', typeof dbProduct)
console.log('dbProduct原型:', Object.getPrototypeOf(dbProduct))
console.log('dbProduct的键:')
for (let key in dbProduct) {
console.log(' ', key, ':', dbProduct[key], '类型:', typeof dbProduct[key])
}
// 验证必要字段,如果关键字段缺失则使用模拟数据
// 注意数据库返回的字段可能与本地ProductType不完全匹配
console.log('验证必要字段dbProduct:', dbProduct)
// 尝试多种方式访问属性
const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined)
const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined)
// 价格字段兼容性处理:优先查找 price其次查找 base_price
let priceValue = dbProduct.price
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct.base_price
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['price']
}
if (priceValue === undefined || priceValue === null) {
priceValue = dbProduct['base_price']
}
const hasId = idValue !== undefined && idValue !== null
const hasName = nameValue !== undefined && nameValue !== null
const hasPrice = priceValue !== undefined && priceValue !== null
const hasRequiredFields = dbProduct && hasId && hasName && hasPrice
console.log('字段检查 - id:', idValue, 'hasId:', hasId, 'name:', nameValue, 'hasName:', hasName, 'price:', priceValue, 'hasPrice:', hasPrice)
console.log('hasRequiredFields:', hasRequiredFields)
if (!hasRequiredFields) {
console.warn('数据库返回数据缺少必要字段,使用模拟数据')
// 继续执行会进入下面的else分支
dbProduct = null
} else {
// 更新dbProduct的字段为实际值确保后续使用正确的属性访问
if (dbProduct.id === undefined && idValue !== undefined) dbProduct.id = idValue
if (dbProduct.name === undefined && nameValue !== undefined) dbProduct.name = nameValue
if (dbProduct.price === undefined && priceValue !== undefined) dbProduct.price = priceValue
// 使用数据库数据 - 处理字段映射
// 数据库Product接口和本地ProductType接口字段可能不同
const images = [] as Array<string>
// 处理图片字段优先使用image_urls字段其次使用main_image_url
console.log('处理数据库图片字段')
// 尝试从数据库的image_urls字段获取图片JSON字符串或对象
if (dbProduct.image_urls) {
let imagesArray: any[] = []
if (typeof dbProduct.image_urls === 'string') {
try {
imagesArray = JSON.parse(dbProduct.image_urls)
} catch (e) {
console.error('解析image_urls字段失败:', e, dbProduct.image_urls)
// 尝试逗号分割
if (dbProduct.image_urls.includes(',')) {
imagesArray = dbProduct.image_urls.split(',').map((img: string) => img.trim())
}
}
} else if (Array.isArray(dbProduct.image_urls)) {
imagesArray = dbProduct.image_urls
}
if (imagesArray.length > 0) {
for (const img of imagesArray) {
if (typeof img === 'string' && img) {
images.push(img)
}
}
}
}
// 如果没有获取到相册图,但有主图,放入相册
if (dbProduct.main_image_url) {
// 如果相册里没有这张图,把它加到第一位
if (!images.includes(dbProduct.main_image_url)) {
images.unshift(dbProduct.main_image_url)
}
}
// 兼容旧字段 image
if (images.length === 0 && dbProduct.image) {
images.push(dbProduct.image)
}
// 如果仍然没有图片,使用传入的图片或默认图片
if (images.length === 0) {
if (options.image) {
images.push(decodeURIComponent(options.image as string))
} else {
images.push('/static/product1.jpg')
}
}
// 补充模拟图片如果图片数量不足3张
const needSupplementCount = 3 - images.length
if (needSupplementCount > 0) {
const supplementalImages = ['/static/product2.jpg', '/static/product3.jpg']
for (let i = 0; i < needSupplementCount && i < supplementalImages.length; i++) {
images.push(supplementalImages[i])
}
}
console.log('最终图片数组:', images)
// 映射字段数据库shop_id对应本地merchant_id
const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001'
// 确保数值字段有效
// 优先使用 price不存在则使用 base_price
let productPrice = 0
if (typeof dbProduct.price === 'number') {
productPrice = dbProduct.price
} else if (typeof dbProduct.base_price === 'number') {
productPrice = dbProduct.base_price
} else if (priceValue !== undefined) {
// 使用上面校验时获取到的 priceValue
productPrice = Number(priceValue)
}
const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : ((dbProduct.total_stock != null && !isNaN(Number(dbProduct.total_stock))) ? Math.floor(Number(dbProduct.total_stock)) : 100)
const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : ((dbProduct.sale_count != null && !isNaN(Number(dbProduct.sale_count))) ? Math.floor(Number(dbProduct.sale_count)) : 50)
// 解析 attributes
let attributes: any = {}
if (dbProduct.attributes) {
try {
if (typeof dbProduct.attributes === 'string') {
attributes = JSON.parse(dbProduct.attributes)
} else {
attributes = dbProduct.attributes
}
} catch (e) {
console.error('解析 attributes 失败', e)
}
}
this.product = {
id: dbProduct.id || productId,
merchant_id: merchantId,
category_id: dbProduct.category_id || 'cat_001',
name: dbProduct.name || '商品名称',
description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: images,
price: productPrice,
original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : ((dbProduct.market_price != null && !isNaN(Number(dbProduct.market_price))) ? Number(dbProduct.market_price) : null),
stock: stock,
sales: sales,
status: 1,
created_at: dbProduct.created_at || '2024-01-01',
// 药品相关字段
specification: attributes.specification || dbProduct.specification || null,
usage: attributes.usage || dbProduct.usage || null,
side_effects: attributes.side_effects || dbProduct.side_effects || null,
precautions: attributes.precautions || dbProduct.precautions || null,
expiry_date: attributes.expiry_date || dbProduct.expiry_date || null,
storage_conditions: attributes.storage_conditions || dbProduct.storage_conditions || null,
approval_number: attributes.approval_number || dbProduct.approval_number || null,
tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : []
} as ProductType
console.log('页面 product 对象已更新:', this.product)
console.log('商品图片数组:', this.product.images)
console.log('商品价格:', this.product.price, '库存:', this.product.stock, '销量:', this.product.sales)
}
} else {
console.log('数据库无数据或加载失败,使用模拟数据')
// 数据库无数据时,使用原有模拟逻辑
const generatePriceFromId = (id: string): number => {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i)
hash |= 0
}
const price = 50 + Math.abs(hash % 450)
return parseFloat(price.toFixed(2))
}
const basePrice = options.price ? parseFloat(options.price) : generatePriceFromId(productId)
const originalPrice = options.originalPrice ? parseFloat(options.originalPrice) : parseFloat((basePrice * 1.2).toFixed(2))
const productName = options.name ? decodeURIComponent(options.name) : (() => {
const productNames = ['高品质运动休闲鞋', '时尚简约双肩背包', '多功能智能手环', '便携式蓝牙音箱', '全自动雨伞', '抗菌防螨床上四件套', '不锈钢保温杯', '无线充电器', '高清行车记录仪', '智能体脂秤']
const nameIndex = Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % productNames.length
return productNames[nameIndex]
})()
const productImage = options.image ? decodeURIComponent(options.image) : '/static/product1.jpg'
const sales = 1000 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5000
const stock = 50 + Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 200
this.product = {
id: productId,
merchant_id: 'merchant_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + 1).toString().padStart(3, '0'),
category_id: 'cat_' + (Math.abs(productId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 10 + 1).toString().padStart(3, '0'),
name: productName,
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。',
images: [productImage, '/static/product2.jpg', '/static/product3.jpg'],
price: basePrice,
original_price: originalPrice,
stock: stock,
sales: sales,
status: 1,
created_at: '2024-01-15'
}
}
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_status: 1,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
// 尝试加载真实商户信息
let realMerchantLoaded = false
// 只有当 ID 是 UUID 格式(包含-)或者是真实数据时才尝试查询
if (this.product.merchant_id && (this.product.merchant_id.includes('-') || !this.product.merchant_id.startsWith('merchant_'))) {
console.log('尝试加载商户信息:', this.product.merchant_id)
try {
const shop = await supabaseService.getShopByMerchantId(this.product.merchant_id)
if (shop) {
console.log('加载到商户信息:', shop.shop_name)
// 确保字段存在,避免 undefined 导致构造失败
this.merchant = {
id: shop.id || '',
user_id: shop.merchant_id || '',
shop_name: shop.shop_name || '未命名店铺',
shop_logo: shop.shop_logo || '/static/default-shop.png',
shop_banner: shop.shop_banner || '/static/default-banner.png',
shop_description: shop.description || '',
contact_name: shop.contact_name || '店主',
contact_phone: shop.contact_phone || '',
shop_status: 1,
// 优先使用 avg_rating没有则使用默认值
rating: shop.rating_avg !== undefined && shop.rating_avg !== null ? shop.rating_avg : 4.8,
// 使用 order_count 或 product_count 作为销量/活跃度指标,如果没有则默认 0
total_sales: shop.total_sales !== undefined ? shop.total_sales : (shop.order_count !== undefined ? shop.order_count : 0),
created_at: shop.created_at || new Date().toISOString()
} as MerchantType
realMerchantLoaded = true
}
} catch (e) {
console.error('加载商户信息失败', e)
}
}
if (!realMerchantLoaded) {
// 根据商家ID生成不同的商家信息
const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5
const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店']
const shopDescriptions = [
'专注品质生活',
'品牌官方直营,正品保障',
'厂家直销,价格优惠',
'专注本领域十年老店',
'用心服务每一位顾客'
]
const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱']
this.merchant = {
id: this.product.merchant_id,
user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'),
shop_name: shopNames[merchantIndex],
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: shopDescriptions[merchantIndex],
contact_name: contactNames[merchantIndex],
contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8),
shop_status: 1,
rating: 4.5 + (merchantIndex * 0.1),
total_sales: 10000 + merchantIndex * 5000,
created_at: '2023-06-01'
}
}
this.loadProductSkus(productId)
},
loadProductSkus(productId: string) {
async loadProductSkus(productId: string) {
// 尝试从数据库加载SKU
try {
const skus = await supabaseService.getProductSkus(productId)
if (skus.length > 0) {
console.log('加载到商品SKU:', skus.length)
this.productSkus = skus.map((sku): ProductSkuType => {
let specs: UTSJSONObject = {}
if (sku.specifications) {
try {
if (typeof sku.specifications === 'string') {
specs = JSON.parse(sku.specifications) as UTSJSONObject
} else {
// 假设已经是对象
specs = sku.specifications as unknown as UTSJSONObject
}
} catch(e) {
console.error('解析SKU规格失败', e)
}
}
return {
id: sku.id,
product_id: sku.product_id,
sku_code: sku.sku_code,
specifications: specs,
price: sku.price,
stock: sku.stock !== undefined ? sku.stock : 0,
image_url: sku.image_url || '',
status: sku.status !== undefined ? sku.status : 1
} as ProductSkuType
})
return
}
} catch (e) {
console.error('Fetch SKUs error', e)
}
// 模拟加载商品SKU数据
const basePrice = this.product.price
// 使用 productId 作为前缀生成唯一的 SKU ID防止不同商品的 SKU ID 冲突
this.productSkus = [
{
id: 'sku_001',
id: `${productId}_sku_001`,
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
@@ -319,7 +690,7 @@ export default {
status: 1
},
{
id: 'sku_002',
id: `${productId}_sku_002`,
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
@@ -330,6 +701,10 @@ export default {
}
]
},
onSwiperChange(e: any) {
this.currentImageIndex = e.detail.current
},
showSpecModal() {
this.showSpec = true
@@ -353,7 +728,7 @@ export default {
return sku.sku_code
},
addToCart() {
async addToCart() {
if (!this.selectedSkuId) {
uni.showToast({
title: '请选择规格',
@@ -362,50 +737,42 @@ export default {
return
}
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在 (同一SKU)
const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId)
if (existingItem) {
existingItem.quantity += this.quantity
} else {
// 查找SKU信息
const sku = this.productSkus.find(s => s.id === this.selectedSkuId)
// 添加新商品
cartItems.push({
id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID
productId: this.product.id,
shopId: this.merchant.id,
shopName: this.merchant.shop_name,
name: this.product.name,
price: sku ? sku.price : this.product.price,
image: (sku && sku.image_url) ? sku.image_url : this.product.images[0],
spec: this.selectedSpec,
quantity: this.quantity,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
// 模拟添加到购物车
uni.showToast({
title: '已添加到购物车',
icon: 'success'
// 显示加载中
uni.showLoading({
title: '添加中...'
})
try {
// 调用 Supabase 服务添加到购物车
// 传递 productId, quantity, skuId
const success = await supabaseService.addToCart(
this.product.id,
this.quantity,
this.selectedSkuId
)
uni.hideLoading()
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
console.error('添加购物车返回失败')
uni.showToast({
title: '添加失败,请登录重试',
icon: 'none'
})
}
} catch (e) {
uni.hideLoading()
console.error('添加购物车异常', e)
uni.showToast({
title: '添加异常',
icon: 'none'
})
}
},
buyNow() {
@@ -520,6 +887,14 @@ export default {
})
},
goToShop() {
if (this.merchant.user_id) {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.user_id}`
})
}
},
goToCart() {
uni.switchTab({
url: '/pages/mall/consumer/cart'
@@ -578,6 +953,21 @@ export default {
getAvailableStock() {
return this.getMaxQuantity()
},
previewImage(index: number) {
uni.previewImage({
current: index,
urls: this.product.images
})
},
showParamsModal() {
this.showParams = true
},
hideParamsModal() {
this.showParams = false
}
}
}
@@ -945,4 +1335,166 @@ export default {
width: 100rpx;
text-align: right;
}
/* 功能主治样式 */
.function-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.function-title {
font-size: 30rpx;
color: #333;
font-weight: bold;
margin-bottom: 15rpx;
display: block;
}
.function-content {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
/* 商品参数样式 */
.params-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.params-title {
font-size: 30rpx;
color: #333;
width: 120rpx;
flex-shrink: 0;
}
.params-summary {
flex: 1;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.params-item {
font-size: 26rpx;
color: #666;
line-height: 1.5;
margin-right: 20rpx;
margin-bottom: 5rpx;
white-space: nowrap;
}
.params-arrow {
font-size: 28rpx;
color: #999;
flex-shrink: 0;
margin-left: 10rpx;
}
/* 商品参数弹窗样式 */
.params-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 1000;
}
.params-content {
background-color: #fff;
width: 100%;
max-height: 80vh;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
}
.params-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #eee;
}
.params-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
width: auto;
}
.params-list {
max-height: 60vh;
overflow-y: auto;
}
.params-item {
display: flex;
align-items: flex-start;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.params-label {
font-size: 28rpx;
color: #333;
font-weight: bold;
width: 150rpx;
flex-shrink: 0;
}
.params-value {
flex: 1;
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
/* 商品详情图片样式 */
.detail-images {
margin-top: 30rpx;
}
.detail-image {
width: 100%;
margin-bottom: 20rpx;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
/* 电脑端适配 */
@media (min-width: 768px) {
.params-section {
padding: 20rpx 30rpx;
}
.params-summary {
flex-wrap: nowrap;
justify-content: space-between;
}
.params-item {
flex: 1;
margin-right: 0;
text-align: center;
white-space: normal;
word-break: break-word;
padding: 0 10rpx;
}
.params-arrow {
margin-left: 20rpx;
}
}
</style>

View File

@@ -229,6 +229,8 @@
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import supabaseService from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
// 状态定义
const statusBarHeight = ref(0)
@@ -437,36 +439,48 @@ const selectSuggestion = (suggestion: string) => {
performSearch()
}
const currentPage = ref(1)
const performSearch = () => {
// 再次强制设置状态,确保万无一失
showResults.value = true
loading.value = true
// 注意:这里不要清空 searchResults.value = [],否则如果 loading 状态切换有微小延迟,可能会短暂满足 "无数据且非加载" 的条件
// 重置页码
currentPage.value = 1
// 保持旧数据直到新数据回来,或者依靠 loading 状态完全遮罩
// 模拟搜索请求
setTimeout(() => {
// 生成模拟结果
const newResults = Array.from({ length: 6 }, (_, i) => ({
id: `s${Date.now()}${i}`, // 确保ID唯一
shopId: i % 2 === 0 ? 'shop_self' : `shop_${i}_${Date.now()}`,
shopName: i % 2 === 0 ? '平台自营大药房' : '阿里健康大药房',
name: `${searchKeyword.value}相关药品-${i+1}`,
specification: '10g*12袋',
price: (Math.random() * 50 + 10).toFixed(1),
image: '/static/images/default-product.png', // 使用本地默认图片
sales: Math.floor(Math.random() * 1000),
tag: i % 2 === 0 ? '自营' : ''
}))
// 数据准备好后再关闭 loading确保无缝衔接
searchResults.value = newResults
// 应用当前排序
sortResults()
// 使用 Supabase 搜索真实数据
const keyword = searchKeyword.value.trim()
if (!keyword) {
loading.value = false
hasMore.value = true
}, 800)
return
}
// 确定排序方式
let sortBy = 'sales'
let ascending = false
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value = response.data as any[]
hasMore.value = response.hasmore
loading.value = false
// 如果无结果,显示空状态
if (searchResults.value.length === 0) {
// empty-result 组件会自动显示
}
})
.catch((error) => {
console.error('搜索失败:', error)
loading.value = false
// 可以显示错误提示,但为了用户体验,先不显示
// 保持搜索结果为空让empty-result显示
})
}
// 切换排序
@@ -481,48 +495,38 @@ const switchSort = (type: string) => {
} else {
activeSort.value = type
}
sortResults()
}
// 执行排序逻辑
const sortResults = () => {
const list = [...searchResults.value]
if (activeSort.value === 'sales') {
// 销量降序
list.sort((a, b) => b.sales - a.sales)
} else if (activeSort.value === 'price') {
// 价格排序
list.sort((a, b) => {
const p1 = parseFloat(a.price)
const p2 = parseFloat(b.price)
return priceSortAsc.value ? (p1 - p2) : (p2 - p1)
})
} else {
// 综合排序这里简单按ID倒序模拟
list.sort((a, b) => (a.id > b.id ? -1 : 1))
}
searchResults.value = list
// 重新执行搜索以获取正确排序的数据
performSearch()
}
const loadMore = () => {
if (loading.value || !hasMore.value) return
if (loading.value || !hasMore.value || !searchKeyword.value.trim()) return
loading.value = true
setTimeout(() => {
const newItems = Array.from({ length: 4 }, (_, i) => ({
id: `more${Date.now()}${i}`,
shopId: i % 2 === 0 ? 'shop_self' : `shop_more_${i}_${Date.now()}`,
shopName: i % 2 === 0 ? '平台自营大药房' : '好药师大药房',
name: `${searchKeyword.value}更多药品-${i+1}`,
specification: '盒装',
price: (Math.random() * 50 + 10).toFixed(1),
image: '/static/images/default-product.png',
sales: Math.floor(Math.random() * 500),
tag: ''
}))
searchResults.value.push(...newItems)
loading.value = false
if (searchResults.value.length > 20) hasMore.value = false
}, 1000)
// 增加页码
currentPage.value++
const keyword = searchKeyword.value.trim()
// 确定排序方式
let sortBy = 'sales'
let ascending = false
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value.push(...(response.data as any[]))
hasMore.value = response.hasmore
loading.value = false
})
.catch((error) => {
console.error('加载更多失败:', error)
loading.value = false
// 加载失败时,假设没有更多数据
hasMore.value = false
})
}
const refreshGuessList = () => {
@@ -537,7 +541,7 @@ const viewProductDetail = (item: any) => {
// 跳转详情页逻辑
console.log('查看商品', item)
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.originalPrice || ''}`
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}&name=${encodeURIComponent(item.name)}&image=${encodeURIComponent(item.image)}`
})
}
@@ -1230,4 +1234,4 @@ const goBack = () => {
.safe-area {
height: 20px;
}
</style>
</style>

View File

@@ -2,10 +2,10 @@
<template>
<view class="settings-page">
<!-- 顶部栏 -->
<view class="settings-header">
<!--<view class="settings-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">设置</text>
</view>
</view>-->
<scroll-view class="settings-content" scroll-y>
<!-- 账户设置 -->
@@ -17,11 +17,11 @@
<text class="item-text">个人资料</text>
<text class="item-arrow"></text>
</view>
<view class="list-item" @click="goToAddressList">
<!--<view class="list-item" @click="goToAddressList">
<text class="item-icon">📍</text>
<text class="item-text">收货地址</text>
<text class="item-arrow"></text>
</view>
</view>-->
<view class="list-item" @click="changePassword">
<text class="item-icon">🔒</text>
<text class="item-text">修改密码</text>
@@ -192,8 +192,20 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onBackPress } from '@dcloudio/uni-app'
// import supa from '@/components/supadb/aksupainstance.uts'
// 拦截返回事件,强制跳转到个人中心页
onBackPress((options) => {
// 无论是什么触发的返回系统返回键或导航栏返回按钮都跳转到profile
// 注意onBackPress 只能在 page 中使用component 中无效
uni.switchTab({
url: '/pages/mall/consumer/profile'
})
// 返回 true 表示阻止默认返回行为
return true
})
type UserType = {
id: string
phone: string | null
@@ -236,8 +248,12 @@ const currentLanguage = ref<string>('简体中文')
const currentTheme = ref<string>('自动')
const appVersion = ref<string>('1.0.0')
const statusBarHeight = ref(0)
// 生命周期
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
loadUserInfo()
loadSettings()
})
@@ -283,7 +299,7 @@ const calculateCacheSize = () => {
// 跳转到个人资料
const goToProfile = () => {
uni.navigateTo({
url: '/pages/mall/consumer/profile'
url: '/pages/user/profile'
})
}
@@ -564,56 +580,130 @@ const deleteAccount = () => {
}
})
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.settings-content {
padding: 20px;
background-color: #f5f5f5;
}
.section-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
.settings-section {
.list-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f5f5f5;
background-color: #ffffff;
/* 手机端每行显示4个自适应排到下一行 */
width: 25%;
flex-direction: column; /* 内容改为垂直排列,图标在上文字在下 */
justify-content: center;
text-align: center;
box-sizing: border-box;
border-right: 1px solid #f5f5f5; /* 添加右边框分隔 */
}
.item-icon {
font-size: 24px;
margin-right: 0; /* 移除右侧间距 */
margin-bottom: 5px; /* 添加底部间距 */
}
.item-text {
font-size: 12px;
color: #333333;
/* 文字太长可能需要处理,这里暂时不做截断 */
}
.item-arrow {
display: none; /* 网格模式下通常不需要箭头 */
}
.item-right {
display: none; /* 简化显示,隐藏右侧状态/箭头等复杂内容 */
}
/* 针对 switch 组件的特殊处理,如果需要显示开关,可能需要调整布局 */
.list-item switch {
transform: scale(0.7);
margin-top: 5px;
}
/* 屏幕宽度大于 480px (大屏手机/平板/PC) 时,启用更宽的网格布局或列表布局 */
@media screen and (min-width: 480px) {
.list-item {
width: calc(50% - 10px); /* 每行两个,留出间隙 */
margin: 5px;
border: 1px solid #f0f0f0;
border-radius: 8px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
border-bottom: 1px solid #f0f0f0;
flex-direction: row; /* 恢复水平排列 */
text-align: left;
justify-content: flex-start;
}
/* 电脑端横向排列部分内容 */
.section-list {
display: flex;
flex-direction: column;
.item-icon {
margin-right: 15px;
margin-bottom: 0;
}
.logout-section, .delete-account-section {
max-width: 800px;
margin-left: auto;
margin-right: auto;
.item-text {
font-size: 14px;
}
.item-arrow, .item-right {
display: flex; /* 恢复显示 */
margin-left: auto; /* 推到右侧 */
}
}
/* 增加针对手机横屏的媒体查询 */
@media screen and (orientation: landscape) and (max-height: 500px) {
.list-item {
width: calc(25% - 10px); /* 横屏也保持4个一行或者根据需要调整 */
margin: 5px;
border: 1px solid #f0f0f0;
border-radius: 8px;
flex-direction: column;
}
}
/* 屏幕宽度大于 1024px (大屏PC) 时 */
@media screen and (min-width: 1024px) {
.settings-page {
flex-direction: row; /* 大屏下改为横向布局,左侧导航,右侧内容 */
flex-direction: row; /* 整体左右布局 */
}
.settings-header {
display: none; /* 大屏下隐藏顶部栏,可能使用侧边栏或其他导航 */
display: none;
}
/* 这里只是简单示例,实际可能需要更复杂的布局调整 */
.settings-content {
width: 100%;
max-width: 1000px;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.list-item {
width: calc(33.33% - 10px); /* 每行三个 */
flex-direction: row; /* PC端保持水平排列 */
justify-content: flex-start;
text-align: left;
}
.item-icon {
margin-right: 15px;
margin-bottom: 0;
}
.item-arrow, .item-right {
display: flex;
margin-left: auto;
}
}
@@ -663,21 +753,9 @@ const goBack = () => {
margin-bottom: 15px;
}
.section-list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.list-item:last-child {
border-bottom: none;
}
/* 删除多余的 .section-list 定义 */
/* 删除多余的 .list-item 定义 */
/* 删除多余的 .list-item:last-child 定义 */
.item-icon {
font-size: 20px;
@@ -744,4 +822,4 @@ const goBack = () => {
font-size: 14px;
text-decoration: underline;
}
</style>
</style>

View File

@@ -44,6 +44,7 @@
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { MerchantType, ProductType } from '@/types/mall-types.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
const merchant = ref<MerchantType>({
id: '',
@@ -74,73 +75,81 @@ onMounted(() => {
}
})
const loadShopData = (id: string) => {
// 模拟加载店铺数据
merchant.value = {
id: id,
user_id: 'user_001',
shop_name: '优质好店',
shop_logo: '/static/shop-logo.png',
shop_banner: '/static/shop-banner.png',
shop_description: '专注品质生活,为您提供最优质的商品和服务。',
contact_name: '店主小王',
contact_phone: '13800138000',
shop_status: 1,
rating: 4.8,
total_sales: 15680,
created_at: '2023-06-01'
const loadShopData = async (id: string) => {
const shop = await supabaseService.getShopByMerchantId(id)
if (shop) {
merchant.value = {
id: shop.id,
user_id: shop.merchant_id, // 映射关系
shop_name: shop.shop_name,
shop_logo: shop.shop_logo || '/static/default-shop.png',
shop_banner: shop.shop_banner || '/static/default-banner.png',
shop_description: shop.description || '',
contact_name: shop.contact_name || '',
contact_phone: shop.contact_phone || '',
shop_status: 1, // 默认正常
rating: shop.rating_avg || 5.0,
total_sales: shop.total_sales || 0,
created_at: shop.created_at || ''
}
}
}
const loadShopProducts = (id: string) => {
// 模拟加载店铺商品列表
products.value = [
{
id: 'prod_001',
merchant_id: id,
category_id: 'cat_001',
name: '精选好物商品 A',
description: '商品描述 A',
images: ['/static/product1.jpg'],
price: 199.99,
original_price: 299.99,
stock: 100,
sales: 1256,
status: 1,
created_at: '2024-01-15'
},
{
id: 'prod_002',
merchant_id: id,
category_id: 'cat_001',
name: '精选好物商品 B',
description: '商品描述 B',
images: ['/static/product2.jpg'],
price: 299.00,
original_price: 399.00,
stock: 50,
sales: 856,
status: 1,
created_at: '2024-01-16'
},
{
id: 'prod_003',
merchant_id: id,
category_id: 'cat_002',
name: '精选好物商品 C',
description: '商品描述 C',
images: ['/static/product3.jpg'],
price: 99.00,
original_price: 129.00,
stock: 200,
sales: 3256,
status: 1,
created_at: '2024-01-17'
}
]
const loadShopProducts = async (id: string) => {
const res = await supabaseService.getProductsByMerchantId(id)
if (res.data.length > 0) {
products.value = res.data.map((item): ProductType => {
// 解析图片数组
let images: string[] = []
if (item.image_urls) {
try {
const rawUrl = item.image_urls
if (Array.isArray(rawUrl)) {
// 已经是数组
images = rawUrl as string[]
} else if (typeof rawUrl === 'string') {
if (rawUrl.startsWith('[')) {
images = JSON.parse(rawUrl) as string[]
} else {
// 单个图片路径字符串
images = [rawUrl]
}
}
} catch(e) {
console.error('解析图片数组失败:', e)
// 降级处理:尝试直接作为单个图片
if (typeof item.image_urls === 'string') {
images = [item.image_urls!]
}
}
}
if (images.length === 0 && item.image) {
images.push(item.image!)
}
if (images.length === 0 && item.main_image_url) {
images.push(item.main_image_url!)
}
return {
id: item.id,
merchant_id: item.merchant_id,
category_id: item.category_id,
name: item.name,
description: item.description || '',
images: images,
price: item.price,
original_price: item.original_price || item.price,
stock: item.stock || 0,
sales: item.sales || 0,
status: 1,
created_at: item.created_at || ''
}
})
}
}
const toggleFollow = () => {
// TODO: Implement actual follow logic with Supabase
isFollowed.value = !isFollowed.value
uni.showToast({
title: isFollowed.value ? '关注成功' : '已取消关注',
@@ -148,47 +157,24 @@ const toggleFollow = () => {
})
}
const addToCart = (product: ProductType) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
const addToCart = async (product: ProductType) => {
uni.showLoading({ title: '添加中...' })
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
const success = await supabaseService.addToCart(product.id, 1)
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.productId === product.id)
uni.hideLoading()
if (existingItem) {
existingItem.quantity++
if (success) {
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
} else {
// 添加新商品
cartItems.push({
id: product.id, // 简单使用产品ID作为购物车ID实际可能有规格
productId: product.id,
shopId: merchant.value.id,
shopName: merchant.value.shop_name,
name: product.name,
price: product.price,
image: product.images[0],
spec: '默认规格',
quantity: 1,
selected: true
uni.showToast({
title: '添加失败,请重试',
icon: 'none'
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
}
const goToProduct = (id: string) => {

View File

@@ -2,120 +2,126 @@
<template>
<view class="wallet-page">
<!-- 顶部栏 -->
<view class="wallet-header">
<!--<view class="wallet-header">
<text class="back-btn" @click="goBack"></text>
<text class="header-title">我的钱包</text>
<text class="more-btn" @click="showMoreActions">···</text>
</view>
</view>-->
<scroll-view class="wallet-content" scroll-y>
<!-- 余额概览 -->
<view class="balance-overview">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
<view class="balance-actions">
<button class="action-btn recharge" @click="recharge">充值</button>
<button class="action-btn withdraw" @click="withdraw">提现</button>
</view>
</view>
<!-- 资产统计 -->
<view class="assets-stats">
<view class="stat-item">
<text class="stat-label">累计充值</text>
<text class="stat-value">¥{{ stats.totalRecharge.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计消费</text>
<text class="stat-value">¥{{ stats.totalConsume.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计提现</text>
<text class="stat-value">¥{{ stats.totalWithdraw.toFixed(2) }}</text>
</view>
</view>
<!-- 快捷功能 -->
<view class="quick-actions">
<view class="action-grid">
<view class="action-item" @click="goToCoupons">
<text class="action-icon">🎫</text>
<text class="action-text">优惠券</text>
<view class="dashboard-container">
<!-- 左侧/顶部区域:资产信息 -->
<view class="dashboard-main">
<!-- 余额概览 -->
<view class="balance-overview">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
<view class="balance-actions">
<button class="action-btn recharge" @click="recharge">充值</button>
<button class="action-btn withdraw" @click="withdraw">提现</button>
</view>
</view>
<view class="action-item" @click="goToRedPackets">
<text class="action-icon">🧧</text>
<text class="action-text">红包</text>
<!-- 资产统计 -->
<view class="assets-stats">
<view class="stat-item">
<text class="stat-label">累计充值</text>
<text class="stat-value">¥{{ stats.totalRecharge.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计消费</text>
<text class="stat-value">¥{{ stats.totalConsume.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计提现</text>
<text class="stat-value">¥{{ stats.totalWithdraw.toFixed(2) }}</text>
</view>
</view>
<view class="action-item" @click="goToPoints">
<text class="action-icon">⭐</text>
<text class="action-text">积分</text>
</view>
<view class="action-item" @click="goToBankCards">
<text class="action-icon">💳</text>
<text class="action-text">银行卡</text>
</view>
</view>
</view>
<!-- 交易记录 -->
<view class="transactions-section">
<view class="section-header">
<text class="section-title">交易记录</text>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@click="changeFilter('expense')">支出</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
<text class="empty-icon">💰</text>
<text class="empty-text">暂无交易记录</text>
<text class="empty-subtext">快去使用钱包功能吧</text>
</view>
<!-- 交易列表 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item">
<view class="transaction-left">
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
<view class="transaction-info">
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
<!-- 快捷功能 -->
<view class="quick-actions">
<view class="action-grid">
<view class="action-item" @click="goToCoupons">
<text class="action-icon">🎫</text>
<text class="action-text">优惠券</text>
</view>
<view class="action-item" @click="goToRedPackets">
<text class="action-icon">🧧</text>
<text class="action-text">红包</text>
</view>
<view class="action-item" @click="goToPoints">
<text class="action-icon">⭐</text>
<text class="action-text">积分</text>
</view>
<view class="action-item" @click="goToBankCards">
<text class="action-icon">💳</text>
<text class="action-text">银行卡</text>
</view>
</view>
<view class="transaction-right">
<text :class="['transaction-amount',
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
{{ transaction.amount > 0 ? '+' : '' }}¥{{ Math.abs(transaction.amount).toFixed(2) }}
</text>
<text class="transaction-balance">余额: ¥{{ transaction.current_balance.toFixed(2) }}</text>
</view>
<!-- 安全提示 (移动端在底部PC端在左侧底部) -->
<view class="security-tips">
<text class="tip-title">安全提示</text>
<text class="tip-item">1. 请妥善保管您的支付密码</text>
<text class="tip-item">2. 不要向他人透露您的账户信息</text>
<text class="tip-item">3. 定期修改密码以确保账户安全</text>
</view>
</view>
<!-- 右侧/底部区域:交易记录 -->
<view class="dashboard-side">
<!-- 交易记录 -->
<view class="transactions-section">
<view class="section-header">
<text class="section-title">交易记录</text>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@click="changeFilter('expense')">支出</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
<text class="empty-icon">💰</text>
<text class="empty-text">暂无交易记录</text>
<text class="empty-subtext">快去使用钱包功能吧</text>
</view>
<!-- 交易列表 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item">
<view class="transaction-left">
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
<view class="transaction-info">
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
</view>
</view>
<view class="transaction-right">
<text :class="['transaction-amount',
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
{{ transaction.amount > 0 ? '+' : '' }}¥{{ Math.abs(transaction.amount).toFixed(2) }}
</text>
<text class="transaction-balance">余额: ¥{{ transaction.current_balance.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && transactions.length > 0" class="no-more">
<text class="no-more-text">没有更多记录了</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && transactions.length > 0" class="no-more">
<text class="no-more-text">没有更多记录了</text>
</view>
</view>
<!-- 安全提示 -->
<view class="security-tips">
<text class="tip-title">安全提示</text>
<text class="tip-item">1. 请妥善保管您的支付密码</text>
<text class="tip-item">2. 不要向他人透露您的账户信息</text>
<text class="tip-item">3. 定期修改密码以确保账户安全</text>
</view>
</scroll-view>
@@ -162,7 +168,7 @@
<script setup lang="uts">
import { ref, onMounted, computed, watch } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
//import supa from '@/components/supadb/aksupainstance.uts'
type WalletType = {
id: string
@@ -253,23 +259,26 @@ const loadBalance = async () => {
if (!userId) return
try {
const { data, error } = await supa
.from('user_wallets')
.select('*')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载钱包失败:', error)
return
// 使用本地模拟数据
const mockBalance = {
balance: 12580.00,
total_recharge: 20000.00,
total_consume: 7420.00,
total_withdraw: 0.00
}
// 尝试从本地存储获取
const storedWallet = uni.getStorageSync(`wallet_${userId}`)
const data = storedWallet ? JSON.parse(storedWallet as string) : mockBalance
if (data) {
balance.value = data.balance || 0
// 类型断言,处理 any 类型
const walletData = data as any
balance.value = Number(walletData.balance || 0)
stats.value = {
totalRecharge: data.total_recharge || 0,
totalConsume: data.total_consume || 0,
totalWithdraw: data.total_withdraw || 0
totalRecharge: Number(walletData.total_recharge || 0),
totalConsume: Number(walletData.total_consume || 0),
totalWithdraw: Number(walletData.total_withdraw || 0)
}
}
} catch (err) {
@@ -291,30 +300,49 @@ const loadTransactions = async (loadMore: boolean = false) => {
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('balance_records')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 模拟交易记录数据
const mockTransactions: TransactionType[] = [
{
id: 't1',
user_id: userId,
change_amount: -128.00,
current_balance: 12580.00,
change_type: 'consume',
related_id: 'ord_001',
remark: '购买药品',
created_at: new Date().toISOString()
},
{
id: 't2',
user_id: userId,
change_amount: 500.00,
current_balance: 12708.00,
change_type: 'recharge',
related_id: 'rec_001',
remark: '账户充值',
created_at: new Date(Date.now() - 86400000).toISOString()
},
{
id: 't3',
user_id: userId,
change_amount: -58.50,
current_balance: 12208.00,
change_type: 'consume',
related_id: 'ord_002',
remark: '购买保健品',
created_at: new Date(Date.now() - 172800000).toISOString()
}
]
// 根据过滤器筛选
// 简单模拟分页和筛选
let filtered = mockTransactions
if (activeFilter.value === 'income') {
query = query.gt('change_amount', 0)
filtered = filtered.filter(t => t.change_amount > 0)
} else if (activeFilter.value === 'expense') {
query = query.lt('change_amount', 0)
filtered = filtered.filter(t => t.change_amount < 0)
}
// 分页
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const { data, error } = await query
if (error !== null) {
console.error('加载交易记录失败:', error)
return
}
const newTransactions = data || []
const newTransactions = filtered
if (loadMore) {
transactions.value.push(...newTransactions)
@@ -324,7 +352,8 @@ const loadTransactions = async (loadMore: boolean = false) => {
currentPage.value = 1
}
hasMore.value = newTransactions.length === pageSize.value
// 模拟没有更多数据
hasMore.value = false
} catch (err) {
console.error('加载交易记录异常:', err)
} finally {
@@ -486,6 +515,34 @@ const goBack = () => {
</script>
<style scoped>
/* 基础样式 */
.wallet-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.wallet-content {
flex: 1;
}
.dashboard-container {
display: flex;
flex-direction: column;
padding-bottom: 20px;
}
.dashboard-main {
display: flex;
flex-direction: column;
}
.dashboard-side {
display: flex;
flex-direction: column;
}
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.wallet-content {
@@ -493,22 +550,16 @@ const goBack = () => {
background-color: #f5f5f5;
}
.balance-overview {
.dashboard-container {
max-width: 800px;
margin: 0 auto;
width: 100%;
}
.balance-overview, .assets-stats, .quick-actions, .transactions-section, .security-tips {
border-radius: 12px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.assets-stats, .quick-actions, .transactions-section, .security-tips {
border-radius: 8px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.popup-content {
width: 400px;
left: 50%;
@@ -520,58 +571,48 @@ const goBack = () => {
@media screen and (min-width: 1024px) {
.wallet-page {
flex-direction: row; /* 大屏下改为横向布局 */
}
.wallet-header {
display: none; /* 大屏下隐藏顶部栏 */
flex-direction: column; /* 保持纵向,内容区内部处理横向 */
}
.wallet-content {
width: 100%;
max-width: 1000px;
max-width: 1200px;
margin: 0 auto;
}
.dashboard-container {
flex-direction: row; /* 横向排列 */
align-items: flex-start;
gap: 20px;
max-width: 100%;
}
.dashboard-main {
width: 400px; /* 左侧固定宽度 */
flex-shrink: 0;
}
.dashboard-side {
flex: 1; /* 右侧自适应 */
min-width: 0;
}
/* 调整各模块间距 */
.balance-overview,
.assets-stats,
.quick-actions,
.security-tips {
margin-bottom: 20px;
}
.transactions-section {
margin-top: 0; /* 移除顶部间距,与左侧对齐 */
height: 100%;
min-height: 600px; /* 保证右侧高度 */
}
}
.wallet-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.wallet-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
}
.header-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.more-btn {
color: #333333;
font-size: 20px;
padding: 5px;
}
.wallet-content {
flex: 1;
}
/* 模块样式 */
.balance-overview {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
@@ -596,6 +637,7 @@ const goBack = () => {
.balance-actions {
display: flex;
flex-direction: row;
gap: 20px;
}
@@ -623,6 +665,7 @@ const goBack = () => {
background-color: #ffffff;
padding: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
@@ -654,13 +697,16 @@ const goBack = () => {
.action-grid {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.action-icon {
@@ -681,6 +727,7 @@ const goBack = () => {
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
@@ -694,6 +741,7 @@ const goBack = () => {
.filter-tabs {
display: flex;
flex-direction: row;
gap: 15px;
}
@@ -750,6 +798,7 @@ const goBack = () => {
.transaction-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
padding: 15px 0;
@@ -762,6 +811,7 @@ const goBack = () => {
.transaction-left {
display: flex;
flex-direction: row;
align-items: flex-start;
}
@@ -887,6 +937,7 @@ const goBack = () => {
.popup-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
@@ -919,6 +970,7 @@ const goBack = () => {
.amount-input {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
padding: 10px;
@@ -941,6 +993,7 @@ const goBack = () => {
.quick-amounts {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
@@ -968,6 +1021,7 @@ const goBack = () => {
.popup-footer {
display: flex;
flex-direction: row;
gap: 15px;
}

View File

@@ -0,0 +1,984 @@
<!-- 钱包页面 -->
<template>
<view class="wallet-page">
<!-- 顶部栏 -->
<!--<view class="wallet-header">
<text class="back-btn" @click="goBack"></text>
</view>-->
<scroll-view class="wallet-content" scroll-y>
<!-- 余额概览 -->
<view class="balance-overview">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ balance.toFixed(2) }}</text>
<view class="balance-actions">
<button class="action-btn recharge" @click="recharge">充值</button>
<button class="action-btn withdraw" @click="withdraw">提现</button>
</view>
</view>
<!-- 资产统计 -->
<view class="assets-stats">
<view class="stat-item">
<text class="stat-label">累计充值</text>
<text class="stat-value">¥{{ stats.totalRecharge.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计消费</text>
<text class="stat-value">¥{{ stats.totalConsume.toFixed(2) }}</text>
</view>
<view class="stat-item">
<text class="stat-label">累计提现</text>
<text class="stat-value">¥{{ stats.totalWithdraw.toFixed(2) }}</text>
</view>
</view>
<!-- 快捷功能 -->
<view class="quick-actions">
<view class="action-grid">
<view class="action-item" @click="goToCoupons">
<text class="action-icon">🎫</text>
<text class="action-text">优惠券</text>
</view>
<view class="action-item" @click="goToRedPackets">
<text class="action-icon">🧧</text>
<text class="action-text">红包</text>
</view>
<view class="action-item" @click="goToPoints">
<text class="action-icon">⭐</text>
<text class="action-text">积分</text>
</view>
<view class="action-item" @click="goToBankCards">
<text class="action-icon">💳</text>
<text class="action-text">银行卡</text>
</view>
</view>
</view>
<!-- 交易记录 -->
<view class="transactions-section">
<view class="section-header">
<text class="section-title">交易记录</text>
<view class="filter-tabs">
<text :class="['filter-tab', { active: activeFilter === 'all' }]"
@click="changeFilter('all')">全部</text>
<text :class="['filter-tab', { active: activeFilter === 'income' }]"
@click="changeFilter('income')">收入</text>
<text :class="['filter-tab', { active: activeFilter === 'expense' }]"
@click="changeFilter('expense')">支出</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="transactions.length === 0 && !isLoading" class="empty-transactions">
<text class="empty-icon">💰</text>
<text class="empty-text">暂无交易记录</text>
<text class="empty-subtext">快去使用钱包功能吧</text>
</view>
<!-- 交易列表 -->
<view class="transactions-list">
<view v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item">
<view class="transaction-left">
<text class="transaction-icon">{{ getTransactionIcon(transaction.type) }}</text>
<view class="transaction-info">
<text class="transaction-title">{{ getTransactionTitle(transaction.type) }}</text>
<text class="transaction-time">{{ formatTime(transaction.created_at) }}</text>
<text v-if="transaction.remark" class="transaction-remark">{{ transaction.remark }}</text>
</view>
</view>
<view class="transaction-right">
<text :class="['transaction-amount',
{ income: transaction.amount > 0, expense: transaction.amount < 0 }]">
{{ transaction.amount > 0 ? '+' : '' }}¥{{ Math.abs(transaction.amount).toFixed(2) }}
</text>
<text class="transaction-balance">余额: ¥{{ transaction.current_balance.toFixed(2) }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && transactions.length > 0" class="no-more">
<text class="no-more-text">没有更多记录了</text>
</view>
</view>
<!-- 安全提示 -->
<view class="security-tips">
<text class="tip-title">安全提示</text>
<text class="tip-item">1. 请妥善保管您的支付密码</text>
<text class="tip-item">2. 不要向他人透露您的账户信息</text>
<text class="tip-item">3. 定期修改密码以确保账户安全</text>
</view>
</scroll-view>
<!-- 充值弹窗 -->
<view v-if="showRechargePopup" class="recharge-popup">
<view class="popup-mask" @click="closeRechargePopup"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">充值</text>
<text class="popup-close" @click="closeRechargePopup">×</text>
</view>
<view class="popup-body">
<text class="amount-label">充值金额</text>
<view class="amount-input">
<text class="currency-symbol">¥</text>
<input class="amount-field"
v-model="rechargeAmount"
type="number"
placeholder="请输入充值金额"
focus />
</view>
<view class="quick-amounts">
<text v-for="amount in quickAmounts"
:key="amount"
:class="['quick-amount', { active: rechargeAmount === amount.toString() }]"
@click="selectQuickAmount(amount)">
¥{{ amount }}
</text>
</view>
<text class="recharge-tip">单笔充值最低10元最高5000元</text>
</view>
<view class="popup-footer">
<button class="cancel-btn" @click="closeRechargePopup">取消</button>
<button class="confirm-btn"
:class="{ disabled: !canRecharge }"
@click="confirmRecharge">
确认充值
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed, watch } from 'vue'
//import supa from '@/components/supadb/aksupainstance.uts'
type WalletType = {
id: string
user_id: string
balance: number
total_recharge: number
total_consume: number
total_withdraw: number
updated_at: string
}
type TransactionType = {
id: string
user_id: string
change_amount: number
current_balance: number
change_type: string // 'recharge' | 'consume' | 'withdraw' | 'refund' | 'reward'
related_id: string | null
remark: string | null
created_at: string
}
type StatsType = {
totalRecharge: number
totalConsume: number
totalWithdraw: number
}
const balance = ref<number>(0)
const stats = ref<StatsType>({
totalRecharge: 0,
totalConsume: 0,
totalWithdraw: 0
})
const transactions = ref<Array<TransactionType>>([])
const activeFilter = ref<string>('all')
const isLoading = ref<boolean>(false)
const currentPage = ref<number>(1)
const pageSize = ref<number>(20)
const hasMore = ref<boolean>(true)
const showRechargePopup = ref<boolean>(false)
const rechargeAmount = ref<string>('')
const quickAmounts = [50, 100, 200, 500, 1000]
// 计算属性
const canRecharge = computed(() => {
const amount = parseFloat(rechargeAmount.value)
return !isNaN(amount) && amount >= 10 && amount <= 5000
})
// 监听过滤器变化
watch(activeFilter, () => {
resetTransactions()
loadTransactions()
})
// 生命周期
onMounted(() => {
loadWalletData()
})
// 重置交易记录
const resetTransactions = () => {
transactions.value = []
currentPage.value = 1
hasMore.value = true
}
// 加载钱包数据
const loadWalletData = async () => {
const userId = getCurrentUserId()
if (!userId) {
uni.navigateTo({
url: '/pages/user/login'
})
return
}
await Promise.all([
loadBalance(),
loadTransactions()
])
}
// 加载余额信息
const loadBalance = async () => {
const userId = getCurrentUserId()
if (!userId) return
try {
const { data, error } = await supa
.from('user_wallets')
.select('*')
.eq('user_id', userId)
.single()
if (error !== null) {
console.error('加载钱包失败:', error)
return
}
if (data) {
balance.value = data.balance || 0
stats.value = {
totalRecharge: data.total_recharge || 0,
totalConsume: data.total_consume || 0,
totalWithdraw: data.total_withdraw || 0
}
}
} catch (err) {
console.error('加载钱包异常:', err)
}
}
// 加载交易记录
const loadTransactions = async (loadMore: boolean = false) => {
if (isLoading.value || (!hasMore.value && loadMore)) {
return
}
isLoading.value = true
try {
const userId = getCurrentUserId()
if (!userId) return
const page = loadMore ? currentPage.value + 1 : 1
let query = supa
.from('balance_records')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 根据过滤器筛选
if (activeFilter.value === 'income') {
query = query.gt('change_amount', 0)
} else if (activeFilter.value === 'expense') {
query = query.lt('change_amount', 0)
}
// 分页
query = query.range((page - 1) * pageSize.value, page * pageSize.value - 1)
const { data, error } = await query
if (error !== null) {
console.error('加载交易记录失败:', error)
return
}
const newTransactions = data || []
if (loadMore) {
transactions.value.push(...newTransactions)
currentPage.value = page
} else {
transactions.value = newTransactions
currentPage.value = 1
}
hasMore.value = newTransactions.length === pageSize.value
} catch (err) {
console.error('加载交易记录异常:', err)
} finally {
isLoading.value = false
}
}
// 获取当前用户ID
const getCurrentUserId = (): string => {
const userStore = uni.getStorageSync('userInfo')
return userStore?.id || ''
}
// 获取交易图标
const getTransactionIcon = (type: string): string => {
const icons: Record<string, string> = {
recharge: '💳',
consume: '🛒',
withdraw: '🏦',
refund: '🔄',
reward: '🎁',
income: '💰',
expense: '📤'
}
return icons[type] || '💰'
}
// 获取交易标题
const getTransactionTitle = (type: string): string => {
const titles: Record<string, string> = {
recharge: '账户充值',
consume: '商品消费',
withdraw: '余额提现',
refund: '订单退款',
reward: '活动奖励',
income: '收入',
expense: '支出'
}
return titles[type] || '交易'
}
// 格式化时间
const formatTime = (timeStr: string): string => {
const date = new Date(timeStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${month}-${day} ${hours}:${minutes}`
}
// 显示更多操作
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['交易记录', '安全设置', '帮助中心'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 交易记录已经在当前页
break
case 1:
uni.navigateTo({
url: '/pages/mall/consumer/settings'
})
break
case 2:
uni.navigateTo({
url: '/pages/info/help'
})
break
}
}
})
}
// 充值
const recharge = () => {
showRechargePopup.value = true
rechargeAmount.value = ''
}
// 提现
const withdraw = () => {
uni.navigateTo({
url: '/pages/mall/consumer/withdraw'
})
}
// 跳转到优惠券
const goToCoupons = () => {
uni.navigateTo({
url: '/pages/mall/consumer/coupons'
})
}
// 跳转到红包
const goToRedPackets = () => {
uni.navigateTo({
url: '/pages/mall/consumer/red-packets'
})
}
// 跳转到积分
const goToPoints = () => {
uni.navigateTo({
url: '/pages/mall/consumer/points'
})
}
// 跳转到银行卡
const goToBankCards = () => {
uni.navigateTo({
url: '/pages/mall/consumer/bank-cards'
})
}
// 切换过滤器
const changeFilter = (filter: string) => {
activeFilter.value = filter
}
// 加载更多
const loadMore = () => {
if (hasMore.value && !isLoading.value) {
loadTransactions(true)
}
}
// 选择快捷金额
const selectQuickAmount = (amount: number) => {
rechargeAmount.value = amount.toString()
}
// 确认充值
const confirmRecharge = async () => {
if (!canRecharge.value) return
const amount = parseFloat(rechargeAmount.value)
if (isNaN(amount)) return
// 这里应该跳转到支付页面进行充值
uni.navigateTo({
url: `/pages/mall/consumer/payment?type=recharge&amount=${amount}`
})
closeRechargePopup()
}
// 关闭充值弹窗
const closeRechargePopup = () => {
showRechargePopup.value = false
rechargeAmount.value = ''
}
// 返回
const goBack = () => {
uni.navigateBack()
}
</script>
<style scoped>
/* 响应式布局优化 */
@media screen and (min-width: 768px) {
.wallet-content {
padding: 20px;
background-color: #f5f5f5;
}
.balance-overview {
border-radius: 12px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.assets-stats, .quick-actions, .transactions-section, .security-tips {
border-radius: 8px;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.popup-content {
width: 400px;
left: 50%;
bottom: 50%;
transform: translate(-50%, 50%);
border-radius: 15px;
}
}
@media screen and (min-width: 1024px) {
.wallet-page {
flex-direction: row; /* 大屏下改为横向布局 */
}
.wallet-header {
display: none; /* 大屏下隐藏顶部栏 */
}
.wallet-content {
width: 100%;
max-width: 1000px;
margin: 0 auto;
}
}
.wallet-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.wallet-header {
background-color: #ffffff;
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e5e5e5;
}
.back-btn {
font-size: 24px;
color: #333333;
padding: 5px;
}
.wallet-content {
flex: 1;
}
.balance-overview {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30px 20px;
color: #ffffff;
}
.balance-label {
display: block;
font-size: 14px;
opacity: 0.9;
margin-bottom: 10px;
text-align: center;
}
.balance-value {
display: block;
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.balance-actions {
display: flex;
gap: 20px;
}
.action-btn {
flex: 1;
height: 40px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
border: none;
}
.action-btn.recharge {
background-color: #ffffff;
color: #667eea;
}
.action-btn.withdraw {
background-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.assets-stats {
background-color: #ffffff;
padding: 20px;
display: flex;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-label {
display: block;
font-size: 12px;
color: #666666;
margin-bottom: 8px;
}
.stat-value {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
}
.quick-actions {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.action-grid {
display: flex;
justify-content: space-between;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
}
.action-icon {
font-size: 28px;
margin-bottom: 8px;
}
.action-text {
font-size: 12px;
color: #666666;
}
.transactions-section {
background-color: #ffffff;
margin-top: 10px;
padding: 15px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.filter-tabs {
display: flex;
gap: 15px;
}
.filter-tab {
font-size: 14px;
color: #666666;
padding: 5px 0;
position: relative;
}
.filter-tab.active {
color: #007aff;
font-weight: bold;
}
.filter-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #007aff;
}
.empty-transactions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.empty-icon {
font-size: 60px;
margin-bottom: 20px;
}
.empty-text {
font-size: 16px;
color: #666666;
margin-bottom: 10px;
}
.empty-subtext {
font-size: 14px;
color: #999999;
}
.transactions-list {
display: flex;
flex-direction: column;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 15px 0;
border-bottom: 1px solid #f5f5f5;
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-left {
display: flex;
align-items: flex-start;
}
.transaction-icon {
font-size: 24px;
margin-right: 15px;
}
.transaction-info {
display: flex;
flex-direction: column;
}
.transaction-title {
font-size: 14px;
color: #333333;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-time {
font-size: 12px;
color: #999999;
margin-bottom: 3px;
}
.transaction-remark {
font-size: 12px;
color: #666666;
}
.transaction-right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.transaction-amount {
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.transaction-amount.income {
color: #4caf50;
}
.transaction-amount.expense {
color: #333333;
}
.transaction-balance {
font-size: 12px;
color: #999999;
}
.loading-more,
.no-more {
padding: 20px;
text-align: center;
}
.loading-text,
.no-more-text {
color: #999999;
font-size: 14px;
}
.security-tips {
background-color: #ffffff;
margin-top: 10px;
padding: 20px;
}
.tip-title {
display: block;
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 15px;
}
.tip-item {
display: block;
font-size: 12px;
color: #666666;
line-height: 1.6;
margin-bottom: 8px;
}
.tip-item:last-child {
margin-bottom: 0;
}
.recharge-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.popup-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
padding: 20px;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e5e5e5;
}
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.popup-close {
font-size: 24px;
color: #999999;
padding: 5px;
}
.popup-body {
margin-bottom: 20px;
}
.amount-label {
display: block;
font-size: 14px;
color: #333333;
margin-bottom: 10px;
}
.amount-input {
display: flex;
align-items: center;
margin-bottom: 20px;
padding: 10px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
.currency-symbol {
font-size: 20px;
color: #333333;
margin-right: 10px;
}
.amount-field {
flex: 1;
font-size: 24px;
font-weight: bold;
color: #333333;
}
.quick-amounts {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.quick-amount {
padding: 8px 15px;
border: 1px solid #e5e5e5;
border-radius: 15px;
font-size: 14px;
color: #333333;
}
.quick-amount.active {
background-color: #007aff;
color: #ffffff;
border-color: #007aff;
}
.recharge-tip {
display: block;
font-size: 12px;
color: #999999;
}
.popup-footer {
display: flex;
gap: 15px;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 45px;
border-radius: 22.5px;
font-size: 16px;
font-weight: bold;
border: none;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666666;
}
.confirm-btn {
background-color: #007aff;
color: #ffffff;
}
.confirm-btn.disabled {
background-color: #cccccc;
opacity: 0.6;
}
</style>

View File

@@ -7,8 +7,11 @@
},
"pages": [
{
"name": "管理端首页",
"path": "pages/mall/admin/index"
"path": "pages/mall/admin/index",
"style": {
"navigationBarTitleText": "管理后台",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/index",
@@ -73,13 +76,6 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/admin/index",
"style": {
"navigationBarTitleText": "管理后台",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/service/index",
"style": {
@@ -516,4 +512,4 @@
}
]
}
}
}

135
pages/user/bind-email.uvue Normal file
View File

@@ -0,0 +1,135 @@
<template>
<view class="page-container">
<view class="form-group">
<view class="input-item">
<text class="label">邮箱</text>
<input class="input" type="text" placeholder="请输入邮箱地址" v-model="email" />
</view>
<view class="input-item">
<text class="label">验证码</text>
<input class="input" type="number" placeholder="请输入验证码" v-model="code" maxlength="6" />
<text class="code-btn" @click="sendCode">{{ counting ? `${count}s` : '获取验证码' }}</text>
</view>
</view>
<button class="submit-btn" @click="handleSubmit">确认绑定</button>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const email = ref('')
const code = ref('')
const counting = ref(false)
const count = ref(60)
const sendCode = () => {
if (counting.value) return
if (!email.value || !email.value.includes('@')) {
uni.showToast({
title: '请输入正确的邮箱',
icon: 'none'
})
return
}
counting.value = true
count.value = 60
const timer = setInterval(() => {
count.value--
if (count.value <= 0) {
clearInterval(timer)
counting.value = false
}
}, 1000)
uni.showToast({
title: '验证码已发送',
icon: 'none'
})
}
const handleSubmit = () => {
if (!email.value || !code.value) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
// TODO: Call API to bind email
uni.showLoading({ title: '提交中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '绑定成功',
icon: 'success'
})
// 更新本地存储的用户信息
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
// @ts-ignore
userInfo.email = email.value
uni.setStorageSync('userInfo', userInfo)
}
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
}
</script>
<style>
.page-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.form-group {
background-color: #fff;
border-radius: 8px;
padding: 0 15px;
margin-bottom: 30px;
}
.input-item {
display: flex;
align-items: center;
height: 50px;
border-bottom: 1px solid #eee;
}
.input-item:last-child {
border-bottom: none;
}
.label {
width: 70px;
font-size: 14px;
color: #333;
}
.input {
flex: 1;
font-size: 14px;
}
.code-btn {
color: #007aff;
font-size: 14px;
padding: 5px 10px;
}
.submit-btn {
background-color: #007aff;
color: #fff;
border-radius: 25px;
font-size: 16px;
}
</style>

135
pages/user/bind-phone.uvue Normal file
View File

@@ -0,0 +1,135 @@
<template>
<view class="page-container">
<view class="form-group">
<view class="input-item">
<text class="label">手机号</text>
<input class="input" type="number" placeholder="请输入新手机号" v-model="phone" maxlength="11" />
</view>
<view class="input-item">
<text class="label">验证码</text>
<input class="input" type="number" placeholder="请输入验证码" v-model="code" maxlength="6" />
<text class="code-btn" @click="sendCode">{{ counting ? `${count}s` : '获取验证码' }}</text>
</view>
</view>
<button class="submit-btn" @click="handleSubmit">确认绑定</button>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const phone = ref('')
const code = ref('')
const counting = ref(false)
const count = ref(60)
const sendCode = () => {
if (counting.value) return
if (!phone.value || phone.value.length !== 11) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
})
return
}
counting.value = true
count.value = 60
const timer = setInterval(() => {
count.value--
if (count.value <= 0) {
clearInterval(timer)
counting.value = false
}
}, 1000)
uni.showToast({
title: '验证码已发送',
icon: 'none'
})
}
const handleSubmit = () => {
if (!phone.value || !code.value) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
// TODO: Call API to bind phone
uni.showLoading({ title: '提交中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '绑定成功',
icon: 'success'
})
// 更新本地存储的用户信息
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
// @ts-ignore
userInfo.phone = phone.value
uni.setStorageSync('userInfo', userInfo)
}
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
}
</script>
<style>
.page-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.form-group {
background-color: #fff;
border-radius: 8px;
padding: 0 15px;
margin-bottom: 30px;
}
.input-item {
display: flex;
align-items: center;
height: 50px;
border-bottom: 1px solid #eee;
}
.input-item:last-child {
border-bottom: none;
}
.label {
width: 70px;
font-size: 14px;
color: #333;
}
.input {
flex: 1;
font-size: 14px;
}
.code-btn {
color: #007aff;
font-size: 14px;
padding: 5px 10px;
}
.submit-btn {
background-color: #007aff;
color: #fff;
border-radius: 25px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<view class="page-container">
<view class="form-group">
<view class="input-item">
<text class="label">旧密码</text>
<input class="input" type="password" placeholder="请输入旧密码" v-model="oldPassword" />
</view>
<view class="input-item">
<text class="label">新密码</text>
<input class="input" type="password" placeholder="请输入新密码" v-model="newPassword" />
</view>
<view class="input-item">
<text class="label">确认密码</text>
<input class="input" type="password" placeholder="请再次输入新密码" v-model="confirmPassword" />
</view>
</view>
<button class="submit-btn" @click="handleSubmit">确认修改</button>
</view>
</template>
<script setup lang="uts">
import { ref } from 'vue'
const oldPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const handleSubmit = () => {
if (!oldPassword.value || !newPassword.value || !confirmPassword.value) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
})
return
}
if (newPassword.value !== confirmPassword.value) {
uni.showToast({
title: '两次输入的密码不一致',
icon: 'none'
})
return
}
// TODO: Call API to change password
uni.showLoading({ title: '提交中...' })
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '修改成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
}
</script>
<style>
.page-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.form-group {
background-color: #fff;
border-radius: 8px;
padding: 0 15px;
margin-bottom: 30px;
}
.input-item {
display: flex;
align-items: center;
height: 50px;
border-bottom: 1px solid #eee;
}
.input-item:last-child {
border-bottom: none;
}
.label {
width: 80px;
font-size: 14px;
color: #333;
}
.input {
flex: 1;
font-size: 14px;
}
.submit-btn {
background-color: #007aff;
color: #fff;
border-radius: 25px;
font-size: 16px;
}
</style>

View File

@@ -255,6 +255,35 @@ const getCode = async () => {
const handleLogin = async () => {
if (!validateAccount()) return
// 特殊账号处理admin/admin 直接跳转
if (account.value === 'admin' && password.value === 'admin') {
setIsLoggedIn(true)
const adminProfile = {
id: 'admin',
username: 'Admin',
email: 'admin@mall.com',
gender: 'unknown',
birthday: '',
height_cm: 0,
weight_kg: 0,
bio: 'Administrator',
avatar_url: '/static/logo.png',
preferred_language: 'zh-CN',
role: 'admin',
school_id: '',
grade_id: '',
class_id: ''
} as UserProfile
setUserProfile(adminProfile)
uni.showToast({ title: '管理员登录成功', icon: 'success' })
setTimeout(() => {
uni.switchTab({ url: '/pages/mall/consumer/index' })
}, 500)
return
}
if (loginType.value === 0) {
if (!validatePassword()) return
} else {
@@ -307,6 +336,16 @@ const handleLogin = async () => {
console.error('获取用户信息失败(忽略,不阻塞登录):', e)
}
// 显式保存用户ID到本地存储确保页面刷新或重启后 SupabaseService 能恢复身份
const currentSession = supa.getSession()
if (currentSession.user != null) {
const uid = currentSession.user?.getString('id')
if (uid != null) {
uni.setStorageSync('user_id', uid)
console.log('用户ID已保存到本地存储:', uid)
}
}
uni.showToast({ title: '登录成功', icon: 'success' })
if (!IS_TEST_MODE) {
setTimeout(() => {

690
pages/user/loginn.uvue Normal file
View File

@@ -0,0 +1,690 @@
<template>
<view class="page-wrapper">
<scroll-view class="login-container" scroll-y="true" show-scrollbar="false">
<!-- Language switch button -->
<view class="language-switch">
<button class="language-btn" @click="toggleLanguage">
{{ currentLocale === 'zh-CN' ? 'EN' : '中' }}
</button>
</view>
<!-- Unified content container -->
<view class="content-wrapper">
<!-- Logo section -->
<view class="logo-section">
<text class="app-title">Trainning Monitor</text>
<text class="page-title">{{ $t('user.login.title') }}</text>
<text class="page-subtitle">{{ $t('user.login.subtitle') }}</text>
</view>
<!-- Form container -->
<view class="form-container">
<form @submit="onSubmit">
<!-- Email input -->
<view class="input-group" :class="{ 'input-error': emailError }">
<text class="input-label">{{ $t('user.login.email') }}</text>
<input class="input-field" name="email" type="text" :value="email"
:placeholder="$t('user.login.email_placeholder')" @blur="validateEmail" />
<text v-if="emailError" class="error-text">{{ emailError }}</text>
</view>
<!-- Password input -->
<view class="input-group" :class="{ 'input-error': passwordError }">
<text class="input-label">{{ $t('user.login.password') }}</text>
<view class="password-input-container">
<input class="input-field" name="password" :type="showPassword ? 'text' : 'password'"
:value="password" :placeholder="$t('user.login.password_placeholder')"
@blur="validatePassword" />
<view class="password-toggle" @click="showPassword = !showPassword">
<text class="toggle-icon">{{ showPassword ? '👁' : '🙈' }}</text>
</view>
</view>
<text v-if="passwordError" class="error-text">{{ passwordError }}</text>
</view>
<!-- Additional options -->
<view class="options-row">
<view class="remember-me">
<checkbox value="rememberMe" color="#2196f3" />
<text class="remember-me-text">{{ $t('user.login.remember_me') }}</text>
</view>
<text class="forgot-password" @click="navigateToForgotPassword">
{{ $t('user.login.forgot_password') }}
</text>
</view>
<!-- Login button -->
<button form-type="submit" class="login-button" :disabled="isLoading" :loading="isLoading">
{{ $t('user.login.login_button') }}
</button>
<!-- General error message -->
<text v-if="generalError" class="general-error">{{ generalError }}</text>
</form>
<!-- Social login options -->
<view class="social-login">
<text class="social-login-text">{{ $t('user.login.or_login_with') }}</text>
<view class="social-buttons">
<button class="social-button wechat" @click="socialLogin('WeChat')">
<text class="social-icon">🟩</text>
</button>
<button class="social-button qq" @click="socialLogin('QQ')">
<text class="social-icon">🔵</text>
</button>
<button class="social-button sms" @click="socialLogin('SMS')">
<text class="social-icon">✉️</text>
</button>
</view>
</view>
<!-- Register option -->
<view class="register-option">
<text class="register-text">{{ $t('user.login.no_account') }}</text>
<text class="register-link"
@click="navigateToRegister">{{ $t('user.login.register_now') }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script lang="uts">
import {HOME_REDIRECT,TABORPAGE} from '@/ak/config.uts'
import type { AkReqOptions, AkReqResponse, AkReqError } from '@/uni_modules/ak-req/index.uts';
import supa from '@/components/supadb/aksupainstance.uts';
import { getCurrentUser, logout } from '@/utils/store.uts';
import { switchLocale, getCurrentLocale } from '@/utils/utils.uts';
export default {
data() {
return {
// email: "akoo@163.com",
// password: "Hf2152111",
email: "am@163.com",
password: "kookoo",
emailError: "",
passwordError: "",
generalError: "",
isLoading: false,
showPassword: false,
rememberMe: false,
currentLocale: getCurrentLocale()
};
},
onLoad() {
// Try to restore saved email if "remember me" was selected
// this.tryRestoreEmail();
console.log('akkkk')
}, methods: {
toggleLanguage() {
const newLocale = this.currentLocale === 'zh-CN' ? 'en-US' : 'zh-CN';
switchLocale(newLocale);
this.currentLocale = newLocale;
uni.showToast({
title: this.$t('user.login.language_switched'),
icon: 'success'
});
}, tryRestoreEmail() {
try {
const savedEmail = uni.getStorageSync('rememberEmail') as string;
if (savedEmail.trim() !== "") {
this.email = savedEmail;
this.rememberMe = true;
}
} catch (e) {
console.error("Error restoring email:", e);
}
},
onSubmit(e : UniFormSubmitEvent) {
this.handleLogin();
},
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (this.email.trim() === "") {
this.emailError = this.$t('user.login.email_required');
return false;
} else if (!emailRegex.test(this.email)) {
this.emailError = this.$t('user.login.email_invalid');
return false;
} else {
this.emailError = '';
return true;
}
}, validatePassword() {
if (this.password.trim() === "") {
this.passwordError = this.$t('user.login.password_required');
return false;
} else if (this.password.length < 6) {
this.passwordError = this.$t('user.login.password_too_short');
return false;
} else {
this.passwordError = '';
return true;
}
},
validateForm() {
const emailValid = this.validateEmail();
const passwordValid = this.validatePassword();
return emailValid && passwordValid;
},
async handleLogin() {
this.generalError = '';
// 清除旧用户数据
logout();
if (!this.validateForm()) {
return;
}
this.isLoading = true;
try {
// Save email if remember me is checked
if (this.rememberMe) {
uni.setStorageSync('rememberEmail', this.email);
} else {
uni.removeStorageSync('rememberEmail');
} // Call signin method from Supabase
const result = await supa.signIn(
this.email,
this.password);
if (result.user !== null) {
// 获取并更新当前用户,确保数据已生效
const profile = await getCurrentUser();
if (profile==null) throw new Error(this.$t('user.login.profile_update_failed'));
// 登录成功提示
uni.showToast({ title: this.$t('user.login.login_success'), icon: 'success' });
// 跳转至运动页面
if(TABORPAGE)
{
uni.switchTab({ url: HOME_REDIRECT });
}
else{
uni.navigateTo({ url: HOME_REDIRECT });
}
return;
} else {
throw new Error(this.$t('user.login.login_failed'));
}
} catch (err) {
console.error("Login error:", err);
// Show error dialog to user
let errorMessage = this.$t('user.login.login_failed');
if (err !== null && typeof err === 'object') {
const error = err as Error;
if (error.message !== null && error.message.trim() !== '') {
errorMessage = error.message;
}
}
uni.showModal({
title: this.$t('user.login.error_title'),
content: errorMessage,
showCancel: false,
confirmText: this.$t('user.login.confirm'),
success: () => {
// Clear any existing error states
this.generalError = '';
this.emailError = '';
this.passwordError = '';
}
});
} finally {
this.isLoading = false;
}
},
socialLogin(provider : string) {
// This would be implemented to handle OAuth login with different providers
uni.showToast({
title: `${provider} ${this.$t('user.login.coming_soon')}`,
icon: 'none'
});
},
navigateToRegister() {
uni.navigateTo({
url: '/pages/user/register'
});
},
navigateToForgotPassword() {
uni.navigateTo({
url: '/pages/user/forgot-password'
});
}
}
};
</script>
<style>
/* Page wrapper for full screen */
.page-wrapper {
width: 100%;
}
.login-container {
width: 100%;
background-image: linear-gradient(to bottom right, #f8f9fa, #e9ecef);
}
/* Content wrapper - single scrollable container */
.content-wrapper {
width: 100%;
/* #ifdef APP-PLUS */
padding: 20rpx 15rpx;
min-height: 800rpx;
/* #endif */
/* #ifndef APP-PLUS */
padding: 30rpx 25rpx;
min-height: 1000rpx;
/* #endif */
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
/* Language switch button - adjusted for single container */
.language-switch {
position: absolute;
/* #ifdef APP-PLUS */
top: 30rpx;
right: 25rpx;
/* #endif */
/* #ifndef APP-PLUS */
top: 40rpx;
right: 35rpx;
/* #endif */
z-index: 100;
}
.language-btn {
/* #ifdef APP-PLUS */
width: 50rpx;
height: 50rpx;
border-radius: 25rpx;
font-size: 18rpx;
/* #endif */
/* #ifndef APP-PLUS */
width: 70rpx;
height: 70rpx;
border-radius: 35rpx;
font-size: 24rpx;
/* #endif */
background-color: rgba(33, 150, 243, 0.8);
color: #fff;
font-weight: normal;
border: 1rpx solid rgba(255, 255, 255, 0.3);
text-align: center;
box-shadow: 0 3rpx 10rpx rgba(33, 150, 243, 0.3);
}
/* Logo section - very compact */
.logo-section {
display: flex;
flex-direction: column;
align-items: center;
/* #ifdef APP-PLUS */
margin: 15rpx 0 20rpx;
/* #endif */
/* #ifndef APP-PLUS */
margin: 25rpx 0 30rpx;
/* #endif */
}
.app-title {
/* #ifdef APP-PLUS */
font-size: 28rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 36rpx;
/* #endif */
font-weight: bold;
color: #2196f3;
}
.page-title {
/* #ifdef APP-PLUS */
font-size: 32rpx;
margin-top: 8rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 48rpx;
margin-top: 15rpx;
/* #endif */
font-weight: bold;
color: #333;
}
.page-subtitle {
/* #ifdef APP-PLUS */
font-size: 18rpx;
margin-top: 4rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
margin-top: 8rpx;
/* #endif */
color: #666;
}
/* Form container - optimized for tablets */
.form-container {
width: 100%;
/* #ifdef APP-PLUS */
max-width: 620rpx;
padding: 18rpx;
margin: 0 auto;
/* #endif */
/* #ifndef APP-PLUS */
max-width: 560rpx;
padding: 32rpx;
margin: 0 auto;
/* #endif */
background-color: #ffffff;
border-radius: 12rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.06);
box-sizing: border-box;
}
/* Input groups - more compact */
.input-group {
/* #ifdef APP-PLUS */
margin-bottom: 18rpx;
/* #endif */
/* #ifndef APP-PLUS */
margin-bottom: 25rpx;
/* #endif */
}
.input-label {
/* #ifdef APP-PLUS */
font-size: 20rpx;
margin-bottom: 6rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 26rpx;
margin-bottom: 8rpx;
/* #endif */
font-weight: normal;
color: #333;
display: block;
}
.input-field {
width: 100%;
/* #ifdef APP-PLUS */
height: 60rpx;
padding: 0 20rpx;
font-size: 22rpx;
/* #endif */
/* #ifndef APP-PLUS */
height: 80rpx;
padding: 0 25rpx;
font-size: 26rpx;
/* #endif */
border-radius: 8rpx;
border: 2rpx solid #ddd;
background-color: #f9f9f9;
box-sizing: border-box;
}
.input-error .input-field {
border-color: #f44336;
}
.error-text {
/* #ifdef APP-PLUS */
font-size: 20rpx;
margin-top: 5rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
margin-top: 6rpx;
/* #endif */
color: #f44336;
}
/* Password input */
.password-input-container {
position: relative;
}
.password-toggle {
position: absolute;
/* #ifdef APP-PLUS */
right: 12rpx;
top: 16rpx;
/* #endif */
/* #ifndef APP-PLUS */
right: 18rpx;
top: 22rpx;
/* #endif */
z-index: 1;
}
.toggle-icon {
/* #ifdef APP-PLUS */
font-size: 20rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 26rpx;
/* #endif */
color: #666;
}
/* Options row - more compact */
.options-row {
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: row;
/* #ifdef APP-PLUS */
margin: 10rpx 0 18rpx;
/* #endif */
/* #ifndef APP-PLUS */
margin: 15rpx 0 25rpx;
/* #endif */
}
.remember-me {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.remember-me-text {
/* #ifdef APP-PLUS */
font-size: 18rpx;
margin-left: 6rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
margin-left: 8rpx;
/* #endif */
color: #666;
}
.forgot-password {
/* #ifdef APP-PLUS */
font-size: 18rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
/* #endif */
color: #2196f3;
}
/* Login button - more compact */
.login-button {
width: 100%;
/* #ifdef APP-PLUS */
height: 60rpx;
font-size: 24rpx;
margin: 12rpx 0;
border-radius: 30rpx;
/* #endif */
/* #ifndef APP-PLUS */
height: 80rpx;
font-size: 30rpx;
margin: 18rpx 0; border-radius: 40rpx;
/* #endif */
background-image: linear-gradient(to right, #2196f3, #03a9f4);
color: #fff;
font-weight: normal;
text-align: center;
box-shadow: 0 8rpx 16rpx rgba(3, 169, 244, 0.2);
}
.login-button:disabled {
background: #ccc;
box-shadow: none;
}
/* General error */
.general-error {
width: 100%;
text-align: center;
color: #f44336;
/* #ifdef APP-PLUS */
font-size: 18rpx;
margin-top: 10rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
margin-top: 15rpx;
/* #endif */
}
/* Social login - very compact */
.social-login {
/* #ifdef APP-PLUS */
margin-top: 15rpx;
/* #endif */
/* #ifndef APP-PLUS */
margin-top: 25rpx;
/* #endif */
display: flex;
flex-direction: column;
align-items: center;
}
.social-login-text {
/* #ifdef APP-PLUS */
font-size: 18rpx;
margin-bottom: 10rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
margin-bottom: 15rpx;
/* #endif */
color: #666;
}
.social-buttons {
display: flex;
flex-direction: row;
justify-content: center;
}
.social-buttons .social-button {
/* #ifdef APP-PLUS */
margin-left: 10rpx;
/* #endif */
/* #ifndef APP-PLUS */
margin-left: 12rpx;
/* #endif */
}
.social-buttons .social-button:first-child {
margin-left: 0;
}
.social-button {
/* #ifdef APP-PLUS */
width: 50rpx;
height: 50rpx;
border-radius: 25rpx;
/* #endif */
/* #ifndef APP-PLUS */
width: 70rpx;
height: 70rpx;
border-radius: 35rpx;
/* #endif */
text-align: center;
}
.social-icon {
/* #ifdef APP-PLUS */
font-size: 24rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 32rpx;
/* #endif */
color: #fff;
}
.google {
background-color: #db4437;
}
.facebook {
background-color: #4267B2;
}
.apple {
background-color: #000;
}
.wechat {
background-color: #1aad19;
}
.qq {
background-color: #498ad5;
}
.sms {
background-color: #ffb300;
}
/* Register option - compact */
.register-option {
display: flex;
justify-content: center;
/* #ifdef APP-PLUS */
margin-top: 15rpx;
margin-bottom: 5rpx;
/* #endif */
/* #ifndef APP-PLUS */
margin-top: 25rpx;
margin-bottom: 10rpx;
/* #endif */
}
.register-text {
/* #ifdef APP-PLUS */
font-size: 18rpx;
margin-right: 4rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
margin-right: 6rpx;
/* #endif */
color: #666;
}
.register-link {
/* #ifdef APP-PLUS */
font-size: 18rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 24rpx;
/* #endif */
color: #2196f3;
font-weight: normal;
}
</style>

View File

@@ -41,7 +41,7 @@ docker-compose logs auth | grep -i error
### 4. 验证配置是否生效
在 Supabase Dashboard (http://192.168.1.63:3000) 的 SQL Editor 中执行:
在 Supabase Dashboard (http://192.168.1.61:3000) 的 SQL Editor 中执行:
```sql
-- 检查当前配置(需要访问 GoTrue 配置)
@@ -59,7 +59,7 @@ docker-compose logs auth | grep -i error
确认 `ak/config.uts` 中的配置正确:
```typescript
export const SUPA_URL: string = 'http://192.168.1.63:8000'
export const SUPA_URL: string = 'http://192.168.1.61:8000'
export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
```

View File

@@ -58,7 +58,7 @@ grep ENABLE_EMAIL_AUTOCONFIRM supabase_pro/.env
## 🔍 验证用户是否创建
在 Supabase Dashboard (http://192.168.1.63:3000) 的 SQL Editor 中执行:
在 Supabase Dashboard (http://192.168.1.61:3000) 的 SQL Editor 中执行:
```sql
-- 检查最新注册的用户

View File

@@ -16,7 +16,7 @@
**执行步骤**
1. **在 Supabase Dashboard (http://192.168.1.63:3000) 中打开 SQL Editor**
1. **在 Supabase Dashboard (http://192.168.1.61:3000) 中打开 SQL Editor**
2. **执行 `USER_AUTH_SCHEMA.sql`**
- 创建 `ak_users` 表和 RLS 策略

View File

@@ -44,7 +44,7 @@ docker-compose restart auth
### 方法一:在 Supabase Dashboard 中手动确认
1. 打开 Supabase Dashboard: http://192.168.1.63:3000
1. 打开 Supabase Dashboard: http://192.168.1.61:3000
2. 进入 **Authentication****Users**
3. 找到对应的用户
4. 点击用户,在详情页中点击 **Confirm Email** 按钮

View File

@@ -1,105 +0,0 @@
// 测试 nav.uts 的 findActiveByCurrentPage 函数
// 模拟 menu.uts 中 maintain 菜单的结构
const maintainMenu = {
id: "maintain",
title: "维护",
groups: [
{
id: "dev-config",
title: "开发配置",
children: [
{
id: "dev-config-category",
title: "分类配置",
path: "/pages/mall/admin/maintain/dev-config/category",
},
],
},
{
id: "system-info",
title: "系统信息",
path: "/pages/mall/admin/maintain/system-info",
children: [],
},
],
};
function normalize(p) {
if (!p) return "";
const s = p.startsWith("/") ? p.slice(1) : p;
const q = s.indexOf("?");
return q >= 0 ? s.slice(0, q) : s;
}
function findActiveByCurrentPage(menuList, currentPage) {
const page = currentPage || "";
// 1) currentPage 直接是一级 menu id
const mById = menuList.find((m) => m.id === page);
if (mById) {
return { activeMenuId: mById.id, activeSubId: "", note: "命中一级菜单" };
}
// 2) currentPage 是 path/pages/xxx 或 pages/xxx
const pageNorm = normalize(page);
for (const m of menuList) {
const groups = m.groups || [];
// group / child / 四级 全部扫描
for (const g of groups) {
// group 叶子(可选)- 检查 id
if (g.id === page) {
return { activeMenuId: m.id, activeSubId: g.id, note: "命中 group.id" };
}
// group 叶子(可选)- 检查 path
if (g.path && normalize(g.path) === pageNorm) {
return {
activeMenuId: m.id,
activeSubId: g.id,
note: "命中 group.path",
};
}
const cs = g.children || [];
for (const c of cs) {
// 用 id 命中
if (c.id === page)
return {
activeMenuId: m.id,
activeSubId: c.id,
note: "命中 child.id",
};
// 用 path 命中
if (c.path && normalize(c.path) === pageNorm)
return {
activeMenuId: m.id,
activeSubId: c.id,
note: "命中 child.path",
};
}
}
}
// 3) 找不到:兜底 home
return { activeMenuId: "home", activeSubId: "", note: "未命中,兜底到 home" };
}
// 测试用例
const testCases = [
"system-info",
"/pages/mall/admin/maintain/system-info",
"pages/mall/admin/maintain/system-info",
"dev-config-category",
];
console.log("=== 测试 findActiveByCurrentPage ===\n");
testCases.forEach((testCase) => {
const result = findActiveByCurrentPage([maintainMenu], testCase);
console.log(`输入: "${testCase}"`);
console.log(
`结果: activeMenuId="${result.activeMenuId}", activeSubId="${result.activeSubId}"`,
);
console.log(`说明: ${result.note}\n`);
});

View File

@@ -1,142 +0,0 @@
// 测试 system-info 页面的 nav 匹配逻辑
// 模拟 menu 配置(简化版本)
const menuList = [
{
id: "home",
title: "首页",
groups: [],
},
{
id: "maintain",
title: "维护",
groups: [
{
id: "dev-config",
title: "开发配置",
children: [
{
id: "dev-config-category",
title: "配置分类",
path: "/pages/mall/admin/maintain/dev-config/category",
},
],
},
{
id: "system-info",
title: "系统信息",
path: "/pages/mall/admin/maintain/system-info",
children: [], // 注意:这是一个 group 叶子节点
},
],
},
];
// nav.uts 中的匹配逻辑
function findActiveByCurrentPage(menuList, currentPage) {
const page = currentPage || "";
// 1) currentPage 直接是一级 menu id
const mById = menuList.find((m) => m.id === page);
if (mById) {
return {
activeMenuId: mById.id,
activeSubId: "",
reason: "一级菜单ID匹配",
};
}
// 2) currentPage 是 path/pages/xxx 或 pages/xxx
const pageNorm = normalize(page);
for (const m of menuList) {
const groups = m.groups ?? [];
// group / child 扫描
for (const g of groups) {
// group 叶子 - 检查 id
if (g.id === page) {
console.log(`✓ Group ID 匹配: g.id="${g.id}" === page="${page}"`);
return {
activeMenuId: m.id,
activeSubId: g.id,
reason: "Group ID匹配",
};
}
// group 叶子 - 检查 path
if (g.path && normalize(g.path) === pageNorm) {
console.log(
`✓ Group Path 匹配: normalize("${g.path}") === normalize("${page}")`,
);
return {
activeMenuId: m.id,
activeSubId: g.id,
reason: "Group Path匹配",
};
}
const cs = g.children ?? [];
for (const c of cs) {
// 用 id 命中
if (c.id === page) {
console.log(`✓ Child ID 匹配: c.id="${c.id}" === page="${page}"`);
return {
activeMenuId: m.id,
activeSubId: c.id,
reason: "Child ID匹配",
};
}
// 用 path 命中
if (c.path && normalize(c.path) === pageNorm) {
console.log(
`✓ Child Path 匹配: normalize("${c.path}") === normalize("${page}")`,
);
return {
activeMenuId: m.id,
activeSubId: c.id,
reason: "Child Path匹配",
};
}
}
}
}
// 3) 找不到:兜底 home
return { activeMenuId: "home", activeSubId: "", reason: "未匹配默认home" };
}
function normalize(p) {
if (!p) return "";
const s = p.startsWith("/") ? p.slice(1) : p;
const q = s.indexOf("?");
return q >= 0 ? s.slice(0, q) : s;
}
// 测试用例
console.log("\n=== 测试 system-info 页面匹配 ===\n");
const testCases = [
"system-info",
"/pages/mall/admin/maintain/system-info",
"pages/mall/admin/maintain/system-info",
];
testCases.forEach((testPage, idx) => {
console.log(`\n测试 ${idx + 1}: currentPage = "${testPage}"`);
const result = findActiveByCurrentPage(menuList, testPage);
console.log(
`结果: activeMenuId="${result.activeMenuId}", activeSubId="${result.activeSubId}"`,
);
console.log(`原因: ${result.reason}`);
if (
result.activeMenuId === "maintain" &&
result.activeSubId === "system-info"
) {
console.log("✅ PASS - 正确匹配到 maintain/system-info");
} else {
console.log("❌ FAIL - 应该匹配到 maintain/system-info");
}
});
console.log("\n=== 测试完成 ===\n");

View File

@@ -85,6 +85,15 @@ export type ProductType = {
sales: number
status: number
created_at: string
// 药品相关字段
specification?: string | null // 规格说明
usage?: string | null // 用法用量
side_effects?: string | null // 副作用
precautions?: string | null // 注意事项
expiry_date?: string | null // 有效期
storage_conditions?: string | null // 储存条件
approval_number?: string | null // 批准文号
tags?: Array<string> | null // 商品标签
}
// 商品SKU类型

View File

@@ -1,4 +1,5 @@
import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts';
import { SUPA_URL } from '@/ak/config.uts';
// token 持久化 key
const ACCESS_TOKEN_KEY = 'akreq_access_token';
@@ -75,7 +76,7 @@ export class AkReq {
headers = Object.assign({}, headers, { 'apikey': apikey }) as UTSJSONObject;
} try {
const res = await this.request({
url: 'https://ak3.oulog.com/auth/v1/token?grant_type=refresh_token',
url: SUPA_URL + '/auth/v1/token?grant_type=refresh_token',
method: 'POST',
data: ({ refresh_token: refreshToken } as UTSJSONObject),
headers: headers,

1406
utils/supabaseService.uts Normal file

File diff suppressed because it is too large Load Diff