合并后台端
This commit is contained in:
@@ -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
38
ak/configbackup.uts
Normal 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
29
ak/configme.uts
Normal 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'
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
})();
|
||||
@@ -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. 刷新页面后重新运行此诊断脚本");
|
||||
})();
|
||||
@@ -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 警告或错误");
|
||||
247
doc_mall/SUPABASE_DATA_MIGRATION_GUIDE.md
Normal file
247
doc_mall/SUPABASE_DATA_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Supabase 数据迁移指南
|
||||
|
||||
## 概述
|
||||
|
||||
本指南将帮助您将当前使用模拟数据的 uni-app 项目迁移到使用 Supabase 数据库。您的项目已经配置了连接到 Ubuntu 服务器上的 Supabase(IP: 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. 测试连接和数据加载
|
||||
|
||||
完成后,您的应用将具备完整的后端数据支持,为后续添加用户管理、购物车、订单等功能打下基础。
|
||||
147
pages.json
147
pages.json
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
487
pages/mall/consumer/Supabase Snippet SQL Query.csv
Normal file
487
pages/mall/consumer/Supabase Snippet SQL Query.csv
Normal file
File diff suppressed because one or more lines are too long
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
219
pages/mall/consumer/address-list copy.uvue
Normal file
219
pages/mall/consumer/address-list copy.uvue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
1435
pages/mall/consumer/cart copy.uvue
Normal file
1435
pages/mall/consumer/cart copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
1371
pages/mall/consumer/cart药品.uvue
Normal file
1371
pages/mall/consumer/cart药品.uvue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
1131
pages/mall/consumer/category完成分类及商品数据获取.uvue
Normal file
1131
pages/mall/consumer/category完成分类及商品数据获取.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1131
pages/mall/consumer/category药品.uvue
Normal file
1131
pages/mall/consumer/category药品.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1733
pages/mall/consumer/checkout copy 2.uvue
Normal file
1733
pages/mall/consumer/checkout copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1730
pages/mall/consumer/checkout copy.uvue
Normal file
1730
pages/mall/consumer/checkout copy.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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或者购物车ID,createOrder 中会使用 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()
|
||||
|
||||
1733
pages/mall/consumer/checkoutgood.uvue
Normal file
1733
pages/mall/consumer/checkoutgood.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
|
||||
1392
pages/mall/consumer/product-detail copy 2.uvue
Normal file
1392
pages/mall/consumer/product-detail copy 2.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1392
pages/mall/consumer/product-detail copy 3.uvue
Normal file
1392
pages/mall/consumer/product-detail copy 3.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1359
pages/mall/consumer/product-detail copy完成图片数量数据获取.uvue
Normal file
1359
pages/mall/consumer/product-detail copy完成图片数量数据获取.uvue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
984
pages/mall/consumer/wallett.uvue
Normal file
984
pages/mall/consumer/wallett.uvue
Normal 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>
|
||||
@@ -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
135
pages/user/bind-email.uvue
Normal 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
135
pages/user/bind-phone.uvue
Normal 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>
|
||||
103
pages/user/change-password.uvue
Normal file
103
pages/user/change-password.uvue
Normal 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>
|
||||
@@ -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
690
pages/user/loginn.uvue
Normal 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>
|
||||
@@ -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...'
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
-- 检查最新注册的用户
|
||||
|
||||
@@ -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 策略
|
||||
|
||||
@@ -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** 按钮
|
||||
|
||||
105
test-nav.js
105
test-nav.js
@@ -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`);
|
||||
});
|
||||
@@ -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");
|
||||
@@ -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类型
|
||||
|
||||
@@ -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
1406
utils/supabaseService.uts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user