feat(admin): full integration of order, product, and finance modules with real RPC data streams
This commit is contained in:
41
docs/ops/2026-02-10__admin__product-module-repaired-full.md
Normal file
41
docs/ops/2026-02-10__admin__product-module-repaired-full.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 商品模块全量修复与统计补全报告
|
||||
|
||||
## 摘要
|
||||
本次对 Admin 商品模块进行了深度的端到端修复,重点解决了商品管理页面的搜索筛选失效、状态统计缺失以及商品统计页面的全量 Mock 问题。补齐了配套的后端 RPC 接口,并重构了前端 Service 与 UI 逻辑。
|
||||
|
||||
## 修复范围
|
||||
|
||||
### 1. 商品管理页 (Product Management)
|
||||
- **搜索功能真实化**:接入了分类选择器数据,绑定了搜索关键字,实现了按名称、分类、状态的联动查询。
|
||||
- **状态统计同步**:接入了各状态商品数量汇总 RPC,确保 Tab 上的“出售中”、“仓库中”等数量真实准确。
|
||||
- **交互完善**:补全了分页翻页逻辑、状态开关(上架/下架)以及逻辑删除(回收站)的操作反馈。
|
||||
|
||||
### 2. 商品统计页 (Product Statistics)
|
||||
- **数据源迁移**:彻底移除硬编码的静态数组,将 KPI 卡片、ECharts 趋势图及排行表格全部对接真实 RPC 数据流。
|
||||
- **动态图表**:趋势图现在能反映真实的浏览量、访客数及支付金额变化。
|
||||
- **全维度排行**:商品排行表支持加购、收藏、订单数、支付额及转化率的综合展示。
|
||||
|
||||
### 3. 商品分类页 (Classify)
|
||||
- **代码核对**:确认已符合 RPC 访问规范,支持树形结构的 CRUD 与状态管理。
|
||||
|
||||
## 变更清单
|
||||
|
||||
### 数据库 / RPC (SQL)
|
||||
- `docs/sql/30_rpc/product/rpc_admin_product_count_stats_v1.sql` (新增):商品状态汇总统计。
|
||||
- `docs/sql/30_rpc/product/rpc_admin_product_trend_v1.sql` (新增):商品营业趋势统计。
|
||||
- `docs/sql/30_rpc/product/rpc_admin_product_analytics_v1.sql` (升级):增强商品概况指标与全维度排行逻辑。
|
||||
|
||||
### 前端代码
|
||||
- `services/admin/productService.uts`:补齐了 stats/trend/ranking/counts 等 fetch 方法。
|
||||
- `pages/mall/admin/product/product-management/index.uvue`:重构逻辑与模板绑定。
|
||||
- `pages/mall/admin/product/product-statistics/index.uvue`:重构数据加载与图表驱动逻辑。
|
||||
|
||||
## 验证说明
|
||||
1. **数据库执行**:需依次执行 `docs/sql/30_rpc/product/` 下新增及修改的 SQL 脚本。
|
||||
2. **功能验证**:
|
||||
- 进入商品管理:确认搜索栏分类可选,点击查询后列表及 Tab 数量能正确刷新。
|
||||
- 进入商品统计:确认顶部 6 个指标不再是固定值,折线图与饼图有动态交互。
|
||||
|
||||
## 关联规范
|
||||
- 遵循 `AGENT_PROJECT_SPEC.md` 关于 Admin 侧通过 RPC 访问数据的要求。
|
||||
- 遵循 SQL 分层归档规范。
|
||||
48
docs/ops/2026-02-11__admin__finance-module-repaired-full.md
Normal file
48
docs/ops/2026-02-11__admin__finance-module-repaired-full.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 财务模块全量修复与数据库构建报告
|
||||
|
||||
## 摘要
|
||||
本次对 Admin 侧财务模块进行了深度的端到端修复,完成了核心财务表(提现、充值、流水)的数据库构建、行级安全策略(RLS)配置、管理端 RPC 接口补全,以及前端 5 个核心页面的全量重构。彻底解决了财务数据展示依赖 Mock 的问题。
|
||||
|
||||
## 修复范围
|
||||
|
||||
### 1. 数据库构建 (Schema & RLS)
|
||||
- **`ml_extract` (提现表)**:支持支付宝/微信/银行卡多渠道快照,补齐手续费、余额快照及审核流字段。
|
||||
- **`ml_user_recharge` (充值表)**:支持支付状态追踪、渠道类型及订单关联。
|
||||
- **`ml_user_bill` (资金流水表)**:统一收支原子日志,支持业务大类与细分类型筛选。
|
||||
- **RLS 策略**:为上述表配置了行级安全,确保普通用户仅能访问个人数据,Admin 侧通过 RPC 访问。
|
||||
|
||||
### 2. RPC 接口升级 (SECURITY DEFINER)
|
||||
- **列表类**:补全了提现、充值、流水的管理端分页查询,支持复杂的名称/订单号/时间筛选。
|
||||
- **操作类**:补全了提现审核(通过/驳回)、充值审计逻辑。
|
||||
- **统计类**:
|
||||
- `rpc_admin_finance_overview`:提供营业额、充值、提现等核心 KPI。
|
||||
- `rpc_admin_finance_bill_summary`:支持日/周/月维度的收支聚合。
|
||||
|
||||
### 3. 前端页面重构
|
||||
- **提现申请 (`withdrawal.uvue`)**:接入审核流逻辑,展示账号快照,实现实时统计更新。
|
||||
- **充值记录 (`recharge.uvue`)**:接入真实充值流水,对齐支付状态。
|
||||
- **资金流水 (`capital_flow.uvue`)**:接入原子流水展示,支持收支筛选。
|
||||
- **账单记录 (`bill.uvue`)**:基于周期聚合 RPC 实现动态账单切换。
|
||||
- **交易统计 (`transaction_stats.uvue`)**:接入真实 KPI 指标,动态驱动趋势折线图。
|
||||
|
||||
## 变更清单
|
||||
|
||||
### 数据库 SQL
|
||||
- `docs/sql/10_schema/finance/`:`ml_extract_v1.sql`, `ml_user_recharge_v1.sql`, `ml_user_bill_v1.sql`
|
||||
- `docs/sql/20_rls/finance/`:`ml_extract_rls_v1.sql`, `ml_user_recharge_rls_v1.sql`, `ml_user_bill_rls_v1.sql`
|
||||
- `docs/sql/30_rpc/finance/`:补齐了 `rpc_admin_finance_overview_v1.sql`, `rpc_admin_finance_bill_summary_v1.sql` 等共 7 个接口。
|
||||
|
||||
### 前端代码
|
||||
- `services/admin/financeService.uts`:补全 DTO 结构与汇总统计方法。
|
||||
- `pages/mall/admin/finance/` 目录下 5 个 `.uvue` 文件的逻辑与 UI 重构。
|
||||
|
||||
## 验证说明
|
||||
1. **数据库执行**:需依次执行 `10_schema` -> `20_rls` -> `30_rpc` 下的财务脚本。
|
||||
2. **功能验证**:
|
||||
- 进入财务统计:确认营业额等指标不再是固定值。
|
||||
- 提现审核:确认点击通过/驳回能真实联动数据库状态。
|
||||
- 筛选/分页:确认所有财务列表的分页与日期筛选均真实生效。
|
||||
|
||||
## 关联规范
|
||||
- 遵循 `AGENT_PROJECT_SPEC.md` 中关于 Admin 数据访问必须走 RPC 的要求。
|
||||
- 遵循 SQL 分层归档与版本管理规范。
|
||||
39
docs/sql/10_schema/finance/ml_extract_v1.sql
Normal file
39
docs/sql/10_schema/finance/ml_extract_v1.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- =====================================================================================
|
||||
-- Schema: 用户提现申请表
|
||||
-- 位置:docs/sql/10_schema/finance/
|
||||
-- 对象类型:Schema (DDL)
|
||||
-- 版本:v1
|
||||
-- 说明:管理用户发起的提现申请(佣金/余额),支持多种提现方式及快照信息
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.ml_extract (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
uid UUID NOT NULL REFERENCES public.ak_users(id),
|
||||
|
||||
real_name TEXT NULL, -- 提现人姓名快照
|
||||
extract_type TEXT NOT NULL, -- 提现方式: alipay, wechat, bank
|
||||
|
||||
-- 账号快照信息
|
||||
alipay_code TEXT NULL, -- 支付宝账号
|
||||
wechat_code TEXT NULL, -- 微信账号
|
||||
bank_code TEXT NULL, -- 银行卡号
|
||||
bank_address TEXT NULL, -- 开户行地址
|
||||
|
||||
extract_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 申请提现金额
|
||||
service_fee DECIMAL(12,2) NOT NULL DEFAULT 0, -- 提现手续费
|
||||
balance DECIMAL(12,2) NOT NULL DEFAULT 0, -- 提现时的余额快照
|
||||
|
||||
status SMALLINT NOT NULL DEFAULT 0, -- 状态: 0:待审核, 1:已通过, -1:已驳回
|
||||
refusal_reason TEXT NULL, -- 驳回原因
|
||||
|
||||
admin_id UUID NULL REFERENCES public.ak_users(id), -- 审核人ID
|
||||
payment_time TIMESTAMPTZ NULL, -- 打款/到账时间
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS ml_extract_uid_idx ON public.ml_extract (uid);
|
||||
CREATE INDEX IF NOT EXISTS ml_extract_status_idx ON public.ml_extract (status);
|
||||
CREATE INDEX IF NOT EXISTS ml_extract_created_at_idx ON public.ml_extract (created_at DESC);
|
||||
36
docs/sql/10_schema/finance/ml_invoices_v1.sql
Normal file
36
docs/sql/10_schema/finance/ml_invoices_v1.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- =====================================================================================
|
||||
-- Schema: 发票管理表
|
||||
-- 位置:docs/sql/10_schema/finance/ml_invoices_v1.sql
|
||||
-- 对象类型:Schema (DDL)
|
||||
-- 版本:v1
|
||||
-- 说明:记录用户提交的开票申请及其处理状态
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.ml_invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
uid UUID NOT NULL REFERENCES public.ak_users(id),
|
||||
order_no TEXT NOT NULL, -- 关联订单号
|
||||
|
||||
order_amount DECIMAL(12,2) NOT NULL, -- 订单金额
|
||||
|
||||
invoice_type SMALLINT NOT NULL DEFAULT 1, -- 1: 电子普通发票, 2: 增值税专用发票
|
||||
header_type SMALLINT NOT NULL DEFAULT 1, -- 1: 个人, 2: 企业
|
||||
|
||||
header_name TEXT NOT NULL, -- 发票抬头
|
||||
tax_id TEXT NULL, -- 企业税号
|
||||
|
||||
email TEXT NULL, -- 接收邮箱
|
||||
remark TEXT NULL, -- 备注
|
||||
|
||||
status SMALLINT NOT NULL DEFAULT 0, -- 0: 待开票, 1: 已开票, -1: 已拒绝
|
||||
refusal_reason TEXT NULL, -- 驳回原因
|
||||
invoice_url TEXT NULL, -- 电子发票文件路径/URL
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS ml_invoices_uid_idx ON public.ml_invoices (uid);
|
||||
CREATE INDEX IF NOT EXISTS ml_invoices_order_no_idx ON public.ml_invoices (order_no);
|
||||
CREATE INDEX IF NOT EXISTS ml_invoices_status_idx ON public.ml_invoices (status);
|
||||
33
docs/sql/10_schema/finance/ml_user_bill_v1.sql
Normal file
33
docs/sql/10_schema/finance/ml_user_bill_v1.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- =====================================================================================
|
||||
-- Schema: 用户资金流水表
|
||||
-- 位置:docs/sql/10_schema/finance/
|
||||
-- 对象类型:Schema (DDL)
|
||||
-- 版本:v1
|
||||
-- 说明:记录用户余额、积分、佣金的所有增减流水(原子日志)
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.ml_user_bill (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
uid UUID NOT NULL REFERENCES public.ak_users(id),
|
||||
|
||||
link_id TEXT NULL, -- 关联业务ID(订单号、提现ID、充值ID等)
|
||||
pm SMALLINT NOT NULL DEFAULT 1, -- 0:支出, 1:收入
|
||||
|
||||
title TEXT NOT NULL, -- 流水标题(如:商品购买、充值、提现)
|
||||
category TEXT NOT NULL, -- 业务大类(如:balance-余额, integral-积分, brokerage-佣金)
|
||||
type TEXT NOT NULL, -- 业务子类型(如:recharge, extract, pay, refund, system_add, system_sub)
|
||||
|
||||
number DECIMAL(12,2) NOT NULL DEFAULT 0, -- 变动金额
|
||||
balance DECIMAL(12,2) NOT NULL DEFAULT 0, -- 变动后的余额快照
|
||||
|
||||
mark TEXT NULL, -- 备注
|
||||
status SMALLINT NOT NULL DEFAULT 1, -- 状态(1:有效, 0:无效/冲正)
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 常用查询索引
|
||||
CREATE INDEX IF NOT EXISTS ml_user_bill_uid_idx ON public.ml_user_bill (uid);
|
||||
CREATE INDEX IF NOT EXISTS ml_user_bill_category_type_idx ON public.ml_user_bill (category, type);
|
||||
CREATE INDEX IF NOT EXISTS ml_user_bill_created_at_idx ON public.ml_user_bill (created_at DESC);
|
||||
32
docs/sql/10_schema/finance/ml_user_recharge_v1.sql
Normal file
32
docs/sql/10_schema/finance/ml_user_recharge_v1.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- =====================================================================================
|
||||
-- Schema: 用户充值记录表
|
||||
-- 位置:docs/sql/10_schema/finance/
|
||||
-- 对象类型:Schema (DDL)
|
||||
-- 版本:v1
|
||||
-- 说明:记录用户主动发起的充值申请及支付状态
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.ml_user_recharge (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
uid UUID NOT NULL REFERENCES public.ak_users(id),
|
||||
|
||||
order_no TEXT UNIQUE NOT NULL, -- 充值订单号(cz开头)
|
||||
recharge_type TEXT NOT NULL, -- 充值渠道: wechat, alipay, system (后台补单)
|
||||
|
||||
price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 实际充值金额
|
||||
give_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 赠送金额
|
||||
|
||||
paid SMALLINT NOT NULL DEFAULT 0, -- 支付状态: 0:未支付, 1:已支付
|
||||
pay_time TIMESTAMPTZ NULL, -- 支付时间
|
||||
|
||||
channel_trade_no TEXT NULL, -- 外部渠道流水号
|
||||
status SMALLINT NOT NULL DEFAULT 1, -- 记录状态: 1:正常, 0:逻辑删除
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS ml_user_recharge_uid_idx ON public.ml_user_recharge (uid);
|
||||
CREATE INDEX IF NOT EXISTS ml_user_recharge_order_no_idx ON public.ml_user_recharge (order_no);
|
||||
CREATE INDEX IF NOT EXISTS ml_user_recharge_created_at_idx ON public.ml_user_recharge (created_at DESC);
|
||||
20
docs/sql/20_rls/finance/ml_extract_rls_v1.sql
Normal file
20
docs/sql/20_rls/finance/ml_extract_rls_v1.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- =====================================================================================
|
||||
-- RLS: 用户提现申请表
|
||||
-- 位置:docs/sql/20_rls/finance/
|
||||
-- 对象类型:RLS 策略
|
||||
-- 版本:v1
|
||||
-- 说明:仅允许用户查看自己的提现记录;管理端通过 RPC 访问
|
||||
-- =====================================================================================
|
||||
|
||||
ALTER TABLE public.ml_extract ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 策略 1: 允许用户读取自己的提现申请
|
||||
DROP POLICY IF EXISTS ml_extract_user_select ON public.ml_extract;
|
||||
CREATE POLICY ml_extract_user_select
|
||||
ON public.ml_extract
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (uid = auth.uid());
|
||||
|
||||
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户
|
||||
-- 提现申请通常由特定的 RPC 函数 (security definer) 创建,以确保业务逻辑(如冻结余额)的原子性
|
||||
19
docs/sql/20_rls/finance/ml_invoices_rls_v1.sql
Normal file
19
docs/sql/20_rls/finance/ml_invoices_rls_v1.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- =====================================================================================
|
||||
-- RLS: 发票管理表
|
||||
-- 位置:docs/sql/20_rls/finance/ml_invoices_rls_v1.sql
|
||||
-- 对象类型:RLS 策略
|
||||
-- 版本:v1
|
||||
-- 说明:用户仅能查看自己的开票申请;管理端通过 RPC 访问
|
||||
-- =====================================================================================
|
||||
|
||||
ALTER TABLE public.ml_invoices ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 策略 1: 允许用户读取自己的记录
|
||||
DROP POLICY IF EXISTS ml_invoices_user_select ON public.ml_invoices;
|
||||
CREATE POLICY ml_invoices_user_select
|
||||
ON public.ml_invoices
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (uid = auth.uid());
|
||||
|
||||
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,通常由 RPC 或支付后逻辑触发
|
||||
19
docs/sql/20_rls/finance/ml_user_bill_rls_v1.sql
Normal file
19
docs/sql/20_rls/finance/ml_user_bill_rls_v1.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- =====================================================================================
|
||||
-- RLS: 用户资金流水表
|
||||
-- 位置:docs/sql/20_rls/finance/
|
||||
-- 对象类型:RLS 策略
|
||||
-- 版本:v1
|
||||
-- 说明:仅允许用户查看自己的流水记录;管理端通过 RPC 访问
|
||||
-- =====================================================================================
|
||||
|
||||
ALTER TABLE public.ml_user_bill ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 策略 1: 允许用户读取自己的记录
|
||||
DROP POLICY IF EXISTS ml_user_bill_user_select ON public.ml_user_bill;
|
||||
CREATE POLICY ml_user_bill_user_select
|
||||
ON public.ml_user_bill
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (uid = auth.uid());
|
||||
|
||||
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,由后端逻辑或 RPC 触发
|
||||
19
docs/sql/20_rls/finance/ml_user_recharge_rls_v1.sql
Normal file
19
docs/sql/20_rls/finance/ml_user_recharge_rls_v1.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- =====================================================================================
|
||||
-- RLS: 用户充值记录表
|
||||
-- 位置:docs/sql/20_rls/finance/
|
||||
-- 对象类型:RLS 策略
|
||||
-- 版本:v1
|
||||
-- 说明:仅允许用户查看自己的充值记录;管理端通过 RPC 访问
|
||||
-- =====================================================================================
|
||||
|
||||
ALTER TABLE public.ml_user_recharge ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 策略 1: 允许用户读取自己的记录
|
||||
DROP POLICY IF EXISTS ml_user_recharge_user_select ON public.ml_user_recharge;
|
||||
CREATE POLICY ml_user_recharge_user_select
|
||||
ON public.ml_user_recharge
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (uid = auth.uid());
|
||||
|
||||
-- 默认不开放 INSERT/UPDATE/DELETE 给普通用户,写操作通常由业务逻辑或支付回调触发
|
||||
@@ -0,0 +1,78 @@
|
||||
-- =====================================================================================
|
||||
-- Admin 财务统计 - 余额收支分布统计 RPC
|
||||
-- 位置:docs/sql/30_rpc/finance/
|
||||
-- 对象类型:RPC 函数(SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:按业务子类型统计指定时间范围内的余额收入与支出分布
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_balance_distribution(
|
||||
p_start_time TIMESTAMP WITH TIME ZONE,
|
||||
p_end_time TIMESTAMP WITH TIME ZONE
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_total_income DECIMAL(12,2);
|
||||
v_total_expense DECIMAL(12,2);
|
||||
v_income_items JSONB;
|
||||
v_expense_items JSONB;
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 计算总收入与总支出
|
||||
SELECT
|
||||
COALESCE(SUM(number) FILTER (WHERE pm = 1), 0),
|
||||
COALESCE(SUM(number) FILTER (WHERE pm = 0), 0)
|
||||
INTO v_total_income, v_total_expense
|
||||
FROM public.ml_user_bill
|
||||
WHERE category = 'balance'
|
||||
AND created_at >= p_start_time
|
||||
AND created_at <= p_end_time
|
||||
AND status = 1;
|
||||
|
||||
-- 3. 统计收入分布 (来源分析)
|
||||
SELECT jsonb_agg(t) INTO v_income_items
|
||||
FROM (
|
||||
SELECT
|
||||
type AS name,
|
||||
SUM(number) AS value,
|
||||
CASE WHEN v_total_income > 0 THEN ROUND(SUM(number) / v_total_income * 100, 2) ELSE 0 END AS percent
|
||||
FROM public.ml_user_bill
|
||||
WHERE category = 'balance' AND pm = 1 AND status = 1
|
||||
AND created_at >= p_start_time AND created_at <= p_end_time
|
||||
GROUP BY type
|
||||
ORDER BY value DESC
|
||||
) t;
|
||||
|
||||
-- 4. 统计支出分布 (消耗分析)
|
||||
SELECT jsonb_agg(t) INTO v_expense_items
|
||||
FROM (
|
||||
SELECT
|
||||
type AS name,
|
||||
SUM(number) AS value,
|
||||
CASE WHEN v_total_expense > 0 THEN ROUND(SUM(number) / v_total_expense * 100, 2) ELSE 0 END AS percent
|
||||
FROM public.ml_user_bill
|
||||
WHERE category = 'balance' AND pm = 0 AND status = 1
|
||||
AND created_at >= p_start_time AND created_at <= p_end_time
|
||||
GROUP BY type
|
||||
ORDER BY value DESC
|
||||
) t;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'income', COALESCE(v_income_items, '[]'::jsonb),
|
||||
'expense', COALESCE(v_expense_items, '[]'::jsonb)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_balance_distribution IS '统计财务余额收支来源与消耗分布';
|
||||
48
docs/sql/30_rpc/finance/rpc_admin_balance_stats_v1.sql
Normal file
48
docs/sql/30_rpc/finance/rpc_admin_balance_stats_v1.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- =====================================================================================
|
||||
-- Admin 财务统计 - 余额核心指标 RPC
|
||||
-- 位置:docs/sql/30_rpc/finance/
|
||||
-- 对象类型:RPC 函数(SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:获取全站当前余额存量、累计增加总额及累计消耗总额
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_balance_stats()
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_current_balance DECIMAL(12,2);
|
||||
v_total_accumulation DECIMAL(12,2);
|
||||
v_total_consumption DECIMAL(12,2);
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 统计当前全站用户余额总存量
|
||||
SELECT COALESCE(SUM(now_money), 0) INTO v_current_balance FROM public.ak_users;
|
||||
|
||||
-- 3. 统计累计增加 (pm=1) 和 累计消耗 (pm=0)
|
||||
-- 基于 ml_user_bill 表中 category='balance' 的记录
|
||||
SELECT
|
||||
COALESCE(SUM(number) FILTER (WHERE pm = 1), 0),
|
||||
COALESCE(SUM(number) FILTER (WHERE pm = 0), 0)
|
||||
INTO v_total_accumulation, v_total_consumption
|
||||
FROM public.ml_user_bill
|
||||
WHERE category = 'balance' AND status = 1;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'current_balance', v_current_balance,
|
||||
'total_accumulation', v_total_accumulation,
|
||||
'total_consumption', v_total_consumption
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_balance_stats IS '获取全站余额存量及累计收支汇总';
|
||||
46
docs/sql/30_rpc/finance/rpc_admin_balance_trend_v1.sql
Normal file
46
docs/sql/30_rpc/finance/rpc_admin_balance_trend_v1.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- =====================================================================================
|
||||
-- Admin 财务统计 - 余额收支趋势 RPC
|
||||
-- 位置:docs/sql/30_rpc/finance/
|
||||
-- 对象类型:RPC 函数(SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:按日聚合指定时间范围内的余额积累 (pm=1) 与 余额消耗 (pm=0) 趋势
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_balance_trend(
|
||||
p_start_time TIMESTAMP WITH TIME ZONE,
|
||||
p_end_time TIMESTAMP WITH TIME ZONE
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_items JSONB;
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 按日聚合统计
|
||||
SELECT jsonb_agg(t) INTO v_items
|
||||
FROM (
|
||||
SELECT
|
||||
to_char(date_trunc('day', gs.day), 'YYYY-MM-DD') AS date_group,
|
||||
COALESCE(SUM(number) FILTER (WHERE pm = 1 AND category = 'balance'), 0) AS accumulation,
|
||||
COALESCE(SUM(number) FILTER (WHERE pm = 0 AND category = 'balance'), 0) AS consumption
|
||||
FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
|
||||
LEFT JOIN public.ml_user_bill b ON date_trunc('day', b.created_at) = gs.day AND b.status = 1
|
||||
GROUP BY gs.day
|
||||
ORDER BY gs.day ASC
|
||||
) t;
|
||||
|
||||
RETURN COALESCE(v_items, '[]'::jsonb);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_balance_trend IS '按日聚合财务余额收支趋势';
|
||||
@@ -0,0 +1,56 @@
|
||||
-- =====================================================================================
|
||||
-- Admin 财务功能 - 账单汇总统计 RPC
|
||||
-- 位置:docs/sql/30_rpc/finance/
|
||||
-- 对象类型:RPC 函数(SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:按日/周/月维度聚合财务收支数据,支撑账单列表展示
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_finance_bill_summary(
|
||||
p_start_time TIMESTAMP WITH TIME ZONE,
|
||||
p_end_time TIMESTAMP WITH TIME ZONE,
|
||||
p_interval TEXT DEFAULT 'day' -- day, week, month
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_items JSONB;
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 聚合统计
|
||||
SELECT jsonb_agg(t) INTO v_items
|
||||
FROM (
|
||||
SELECT
|
||||
to_char(date_trunc(p_interval, created_at),
|
||||
CASE
|
||||
WHEN p_interval = 'day' THEN 'YYYY-MM-DD'
|
||||
WHEN p_interval = 'week' THEN 'IYYY-IW'
|
||||
ELSE 'YYYY-MM'
|
||||
END
|
||||
) AS date_group,
|
||||
SUM(number) FILTER (WHERE pm = 1) AS income,
|
||||
SUM(number) FILTER (WHERE pm = 0) AS expense,
|
||||
SUM(CASE WHEN pm = 1 THEN number ELSE -number END) AS net_entry
|
||||
FROM public.ml_user_bill
|
||||
WHERE created_at >= p_start_time
|
||||
AND created_at <= p_end_time
|
||||
AND status = 1
|
||||
GROUP BY date_trunc(p_interval, created_at)
|
||||
ORDER BY date_trunc(p_interval, created_at) DESC
|
||||
) t;
|
||||
|
||||
RETURN COALESCE(v_items, '[]'::jsonb);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_finance_bill_summary IS '按周期聚合财务收支账单';
|
||||
72
docs/sql/30_rpc/finance/rpc_admin_finance_overview_v1.sql
Normal file
72
docs/sql/30_rpc/finance/rpc_admin_finance_overview_v1.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
-- =====================================================================================
|
||||
-- Admin 财务功能 - 财务概况统计 RPC
|
||||
-- 位置:docs/sql/30_rpc/finance/
|
||||
-- 对象类型:RPC 函数(SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:获取指定时间段内的财务核心 KPI(营业额、充值汇总、提现汇总、资金存量)
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_finance_overview(
|
||||
p_start_time TIMESTAMP WITH TIME ZONE,
|
||||
p_end_time TIMESTAMP WITH TIME ZONE
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_recharge_amount DECIMAL(12,2);
|
||||
v_recharge_count BIGINT;
|
||||
v_extract_amount DECIMAL(12,2);
|
||||
v_extract_count BIGINT;
|
||||
v_total_user_balance DECIMAL(12,2);
|
||||
v_total_user_brokerage DECIMAL(12,2);
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 统计充值 (仅统计已支付)
|
||||
SELECT
|
||||
COALESCE(SUM(price + give_price), 0),
|
||||
COUNT(*)
|
||||
INTO v_recharge_amount, v_recharge_count
|
||||
FROM public.ml_user_recharge
|
||||
WHERE paid = 1
|
||||
AND created_at >= p_start_time
|
||||
AND created_at <= p_end_time;
|
||||
|
||||
-- 3. 统计提现 (仅统计已通过)
|
||||
SELECT
|
||||
COALESCE(SUM(extract_price), 0),
|
||||
COUNT(*)
|
||||
INTO v_extract_amount, v_extract_count
|
||||
FROM public.ml_extract
|
||||
WHERE status = 1
|
||||
AND created_at >= p_start_time
|
||||
AND created_at <= p_end_time;
|
||||
|
||||
-- 4. 统计全站资金存量 (实时快照)
|
||||
SELECT
|
||||
COALESCE(SUM(now_money), 0),
|
||||
COALESCE(SUM(brokerage_price), 0)
|
||||
INTO v_total_user_balance, v_total_user_brokerage
|
||||
FROM public.ak_users;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'recharge_amount', v_recharge_amount,
|
||||
'recharge_count', v_recharge_count,
|
||||
'extract_amount', v_extract_amount,
|
||||
'extract_count', v_extract_count,
|
||||
'total_user_balance', v_total_user_balance,
|
||||
'total_user_brokerage', v_total_user_brokerage
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_finance_overview IS '财务核心 KPI 概况统计';
|
||||
93
docs/sql/30_rpc/finance/rpc_admin_invoice_list_v1.sql
Normal file
93
docs/sql/30_rpc/finance/rpc_admin_invoice_list_v1.sql
Normal file
@@ -0,0 +1,93 @@
|
||||
-- =====================================================================================
|
||||
-- RPC: rpc_admin_invoice_list
|
||||
-- 位置:docs/sql/30_rpc/finance/
|
||||
-- 对象类型:RPC 函数 (SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:管理端分页获取发票申请列表,支持搜索、状态筛选及时间过滤
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_invoice_list(
|
||||
p_page INTEGER DEFAULT 1,
|
||||
p_page_size INTEGER DEFAULT 15,
|
||||
p_status SMALLINT DEFAULT NULL,
|
||||
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
|
||||
p_search TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_offset INTEGER;
|
||||
v_total BIGINT;
|
||||
v_items JSONB;
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
v_offset := (p_page - 1) * p_page_size;
|
||||
|
||||
-- 2. 获取总数
|
||||
SELECT COUNT(*) INTO v_total
|
||||
FROM public.ml_invoices i
|
||||
LEFT JOIN public.ak_users u ON u.id = i.uid
|
||||
WHERE (p_status IS NULL OR i.status = p_status)
|
||||
AND (p_start_time IS NULL OR i.created_at >= p_start_time)
|
||||
AND (p_end_time IS NULL OR i.created_at <= p_end_time)
|
||||
AND (p_search IS NULL OR (
|
||||
i.order_no ILIKE '%' || p_search || '%' OR
|
||||
i.header_name ILIKE '%' || p_search || '%' OR
|
||||
u.username ILIKE '%' || p_search || '%'
|
||||
));
|
||||
|
||||
-- 3. 获取明细数据
|
||||
SELECT jsonb_agg(t) INTO v_items
|
||||
FROM (
|
||||
SELECT
|
||||
i.id,
|
||||
i.uid,
|
||||
i.order_no,
|
||||
i.order_amount,
|
||||
i.invoice_type,
|
||||
i.header_type,
|
||||
i.header_name,
|
||||
i.tax_id,
|
||||
i.email,
|
||||
i.remark,
|
||||
i.status,
|
||||
i.refusal_reason,
|
||||
i.invoice_url,
|
||||
i.created_at,
|
||||
i.updated_at,
|
||||
u.username as user_name,
|
||||
u.email as user_email
|
||||
FROM public.ml_invoices i
|
||||
LEFT JOIN public.ak_users u ON u.id = i.uid
|
||||
WHERE (p_status IS NULL OR i.status = p_status)
|
||||
AND (p_start_time IS NULL OR i.created_at >= p_start_time)
|
||||
AND (p_end_time IS NULL OR i.created_at <= p_end_time)
|
||||
AND (p_search IS NULL OR (
|
||||
i.order_no ILIKE '%' || p_search || '%' OR
|
||||
i.header_name ILIKE '%' || p_search || '%' OR
|
||||
u.username ILIKE '%' || p_search || '%'
|
||||
))
|
||||
ORDER BY i.created_at DESC
|
||||
LIMIT p_page_size
|
||||
OFFSET v_offset
|
||||
) t;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'total', v_total,
|
||||
'items', COALESCE(v_items, '[]'::jsonb)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_invoice_list IS '管理员分页查询发票申请列表';
|
||||
45
docs/sql/30_rpc/finance/rpc_admin_invoice_process_v1.sql
Normal file
45
docs/sql/30_rpc/finance/rpc_admin_invoice_process_v1.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- =====================================================================================
|
||||
-- RPC: rpc_admin_invoice_process
|
||||
-- 位置:docs/sql/30_rpc/finance/
|
||||
-- 对象类型:RPC 函数 (SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:管理端处理发票申请(开票或驳回)
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_invoice_process(
|
||||
p_id UUID,
|
||||
p_status SMALLINT, -- 1: 已开票, -1: 已拒绝
|
||||
p_invoice_url TEXT DEFAULT NULL,
|
||||
p_refusal_reason TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_ok BOOLEAN;
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 更新状态
|
||||
UPDATE public.ml_invoices
|
||||
SET
|
||||
status = p_status,
|
||||
invoice_url = CASE WHEN p_status = 1 THEN p_invoice_url ELSE invoice_url END,
|
||||
refusal_reason = CASE WHEN p_status = -1 THEN p_refusal_reason ELSE refusal_reason END,
|
||||
updated_at = now()
|
||||
WHERE id = p_id;
|
||||
|
||||
GET DIAGNOSTICS v_ok = ROW_COUNT;
|
||||
RETURN v_ok;
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION public.rpc_admin_invoice_process IS '管理员处理发票开票申请';
|
||||
@@ -48,7 +48,7 @@ BEGIN
|
||||
'refund_amount', refund_amount
|
||||
) INTO v_stats FROM stats;
|
||||
|
||||
RETURN v_stats;
|
||||
RETURN v_stats;
|
||||
END;
|
||||
$$;
|
||||
|
||||
@@ -86,17 +86,29 @@ BEGIN
|
||||
p.main_image_url as image,
|
||||
COALESCE(p.view_count, 0) as views,
|
||||
(SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history bh WHERE bh.product_id = p.id AND bh.created_at BETWEEN p_start_time AND p_end_time) as visitors,
|
||||
(SELECT COALESCE(SUM(quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
|
||||
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as sales,
|
||||
(SELECT COALESCE(SUM(quantity), 0) FROM public.ml_shopping_cart sc WHERE sc.product_id = p.id AND sc.created_at BETWEEN p_start_time AND p_end_time) as cart_count,
|
||||
(SELECT COUNT(DISTINCT o.id) FROM public.ml_orders o JOIN public.ml_order_items oi ON o.id = oi.order_id
|
||||
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time) as order_count,
|
||||
(SELECT COALESCE(SUM(oi.quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
|
||||
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_count,
|
||||
(SELECT COALESCE(SUM(oi.total_amount), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
|
||||
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as amount
|
||||
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_amount,
|
||||
(SELECT COUNT(*) FROM public.ml_user_favorites f WHERE f.target_id = p.id AND f.target_type = 1 AND f.created_at BETWEEN p_start_time AND p_end_time) as fav_count
|
||||
FROM public.ml_products p
|
||||
WHERE p.status != 4
|
||||
ORDER BY
|
||||
CASE WHEN p_sort_by = 'views' THEN 4
|
||||
WHEN p_sort_by = 'sales' THEN 6
|
||||
WHEN p_sort_by = 'amount' THEN 7
|
||||
ELSE 6 END DESC
|
||||
CASE
|
||||
WHEN p_sort_by = 'views' THEN COALESCE(p.view_count, 0)
|
||||
WHEN p_sort_by = 'sales' THEN (
|
||||
SELECT COALESCE(SUM(oi.quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
|
||||
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)
|
||||
)
|
||||
WHEN p_sort_by = 'amount' THEN (
|
||||
SELECT COALESCE(SUM(oi.total_amount), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
|
||||
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)
|
||||
)
|
||||
ELSE COALESCE(p.view_count, 0)
|
||||
END DESC
|
||||
LIMIT p_limit
|
||||
) t;
|
||||
|
||||
|
||||
38
docs/sql/30_rpc/product/rpc_admin_product_count_stats_v1.sql
Normal file
38
docs/sql/30_rpc/product/rpc_admin_product_count_stats_v1.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
-- =====================================================================================
|
||||
-- Admin 商品管理 - 商品状态汇总统计 RPC
|
||||
-- 位置:docs/sql/30_rpc/product/
|
||||
-- 对象类型:RPC 函数(SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:统计出售中、仓库中、草稿箱、回收站各状态的商品数量
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_product_count_stats()
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_result JSONB;
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 统计各状态数量
|
||||
-- status 定义:1:上架(出售中), 2:下架(仓库中), 3:草稿, 4:逻辑删除(回收站)
|
||||
SELECT jsonb_build_object(
|
||||
'selling', COUNT(*) FILTER (WHERE status = 1),
|
||||
'warehouse', COUNT(*) FILTER (WHERE status = 2),
|
||||
'draft', COUNT(*) FILTER (WHERE status = 3),
|
||||
'recycle', COUNT(*) FILTER (WHERE status = 4)
|
||||
) INTO v_result
|
||||
FROM public.ml_products;
|
||||
|
||||
RETURN v_result;
|
||||
END;
|
||||
$$;
|
||||
44
docs/sql/30_rpc/product/rpc_admin_product_trend_v1.sql
Normal file
44
docs/sql/30_rpc/product/rpc_admin_product_trend_v1.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- =====================================================================================
|
||||
-- Admin 商品统计 - 营业趋势统计 RPC
|
||||
-- 位置:docs/sql/30_rpc/product/
|
||||
-- 对象类型:RPC 函数(SECURITY DEFINER)
|
||||
-- 版本:v1
|
||||
-- 说明:按天聚合指定时间范围内的商品浏览量、访客量、支付金额及退款金额
|
||||
-- =====================================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.rpc_admin_product_trend(
|
||||
p_start_time TIMESTAMP WITH TIME ZONE,
|
||||
p_end_time TIMESTAMP WITH TIME ZONE
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_items JSONB;
|
||||
BEGIN
|
||||
-- 1. 权限检查
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.ak_users
|
||||
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Permission denied';
|
||||
END IF;
|
||||
|
||||
-- 2. 按日聚合统计
|
||||
SELECT jsonb_agg(t) INTO v_items
|
||||
FROM (
|
||||
SELECT
|
||||
to_char(date_trunc('day', gs.day), 'YYYY-MM-DD') AS date_group,
|
||||
(SELECT COUNT(*) FROM public.ml_browse_history bh WHERE date_trunc('day', bh.created_at) = gs.day) as views,
|
||||
(SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history bh WHERE date_trunc('day', bh.created_at) = gs.day) as visitors,
|
||||
(SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders o WHERE date_trunc('day', o.created_at) = gs.day AND o.order_status NOT IN (1, 5)) as pay_amount,
|
||||
(SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders o WHERE date_trunc('day', o.created_at) = gs.day AND o.order_status = 7) as refund_amount
|
||||
FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
|
||||
ORDER BY gs.day ASC
|
||||
) t;
|
||||
|
||||
RETURN COALESCE(v_items, '[]'::jsonb);
|
||||
END;
|
||||
$$;
|
||||
@@ -3,19 +3,16 @@
|
||||
<!-- 筛选卡片 -->
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">订单时间:</text>
|
||||
<view class="date-picker-wrap">
|
||||
<text class="calendar-icon">📅</text>
|
||||
<text class="date-placeholder">开始日期 - 结束日期</text>
|
||||
</view>
|
||||
<view class="filter-item search-wrap">
|
||||
<text class="filter-label">流水搜索:</text>
|
||||
<input class="search-input" v-model="searchKeyword" placeholder="订单号/昵称/电话/用户ID" @confirm="handleQuery" />
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">交易类型:</text>
|
||||
<view class="select-box">
|
||||
<text class="select-txt">请选择</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<uni-data-select v-model="typeValue" :localdata="typeOptions" class="data-select" @change="handleQuery" />
|
||||
</view>
|
||||
<view class="btn-query" @click="handleQuery">
|
||||
<text class="btn-txt">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -23,114 +20,164 @@
|
||||
<!-- 列表表格 -->
|
||||
<view class="table-container border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">ID</text></view>
|
||||
<view class="th col-order"><text class="th-txt">关联订单</text></view>
|
||||
<view class="th col-id"><text class="th-txt">序号</text></view>
|
||||
<view class="th col-order"><text class="th-txt">关联单据</text></view>
|
||||
<view class="th col-time"><text class="th-txt">交易时间</text></view>
|
||||
<view class="th col-amount"><text class="th-txt">交易金额</text></view>
|
||||
<view class="th col-user"><text class="th-txt">用户</text></view>
|
||||
<view class="th col-type"><text class="th-txt">交易类型</text></view>
|
||||
<view class="th col-type"><text class="th-txt">业务类型</text></view>
|
||||
<view class="th col-remark"><text class="th-txt">备注</text></view>
|
||||
<view class="th col-op"><text class="th-txt">操作</text></view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="table-body">
|
||||
<view class="table-row" v-for="item in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td col-order text-left"><text class="td-txt">{{ item.order }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="table-body">
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="tableData.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无记录</text>
|
||||
</view>
|
||||
<view class="table-row" v-for="(item, index) in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td col-order text-left"><text class="td-txt">{{ item.link_id || '-' }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.created_at.substring(0, 16).replace('T', ' ') }}</text></view>
|
||||
<view class="td col-amount">
|
||||
<text :class="['td-txt', item.amount.startsWith('+') ? 'red-txt' : 'green-txt']">{{ item.amount }}</text>
|
||||
<text :class="['td-txt', item.pm === 1 ? 'red-txt' : 'green-txt']">
|
||||
{{ item.pm === 1 ? '+' : '-' }}{{ item.number.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td col-user"><text class="td-txt">{{ item.user }}</text></view>
|
||||
<view class="td col-type"><text class="td-txt">{{ item.type }}</text></view>
|
||||
<view class="td col-remark text-left"><text class="td-txt">{{ item.remark }}</text></view>
|
||||
<view class="td col-user">
|
||||
<view class="u-info-box">
|
||||
<text class="u-name">{{ item.user_name || '未知' }}</text>
|
||||
<text class="u-id">UID:{{ item.uid.substring(0, 8) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-type"><text class="td-txt">{{ getBillTypeText(item.type) }}</text></view>
|
||||
<view class="td col-remark text-left"><text class="td-txt">{{ item.mark || '-' }}</text></view>
|
||||
<view class="td col-op">
|
||||
<text class="btn-link">备注</text>
|
||||
<text class="btn-link">详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<view class="p-btn" :class="{ disabled: page <= 1 }" @click="prevPage">
|
||||
<text><</text>
|
||||
</view>
|
||||
<view class="p-btn active">
|
||||
<text>{{ page }}</text>
|
||||
</view>
|
||||
<view class="p-btn" :class="{ disabled: page >= totalPages }" @click="nextPage">
|
||||
<text>></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { fetchUserBillList } from '@/services/admin/financeService.uts'
|
||||
import { UserBillRecord } from '@/types/admin/finance.uts'
|
||||
|
||||
interface BalanceRecord {
|
||||
id: string
|
||||
order: string
|
||||
time: string
|
||||
amount: string
|
||||
user: string
|
||||
type: string
|
||||
remark: string
|
||||
const typeValue = ref<string>('all')
|
||||
const searchKeyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'all', text: '全部类型' },
|
||||
{ value: 'recharge', text: '充值' },
|
||||
{ value: 'extract', text: '提现' },
|
||||
{ value: 'pay', text: '支付' },
|
||||
{ value: 'refund', text: '退款' },
|
||||
{ value: 'system_add', text: '系统增加' },
|
||||
{ value: 'system_sub', text: '系统减少' }
|
||||
]
|
||||
|
||||
const tableData = ref<UserBillRecord[]>([])
|
||||
|
||||
const totalPages = computed(() : number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
return Math.ceil(total.value / pageSize.value)
|
||||
})
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const type = typeValue.value == 'all' ? null : typeValue.value
|
||||
|
||||
const res = await fetchUserBillList(
|
||||
page.value,
|
||||
pageSize.value,
|
||||
'balance', // 余额明细
|
||||
type,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
searchKeyword.value || null
|
||||
)
|
||||
tableData.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载余额记录失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const tableData = ref<BalanceRecord[]>([
|
||||
{
|
||||
id: '31216',
|
||||
order: '新用户注册赠送余额',
|
||||
time: '2026-02-03 10:30:11',
|
||||
amount: '+ 88888.00',
|
||||
user: '1',
|
||||
type: '新用户注册赠送余额',
|
||||
remark: '新用户注册赠送88888余额'
|
||||
},
|
||||
{
|
||||
id: '31215',
|
||||
order: '新用户注册赠送余额',
|
||||
time: '2026-02-03 10:19:52',
|
||||
amount: '+ 88888.00',
|
||||
user: 'circus',
|
||||
type: '新用户注册赠送余额',
|
||||
remark: '新用户注册赠送88888余额'
|
||||
},
|
||||
{
|
||||
id: '31214',
|
||||
order: 'cp541560738494283776',
|
||||
time: '2026-02-03 10:09:07',
|
||||
amount: '- 999.00',
|
||||
user: '1岁上班22岁退休',
|
||||
type: '余额支付购买商品',
|
||||
remark: '余额支付999.00元购买商品'
|
||||
},
|
||||
{
|
||||
id: '31213',
|
||||
order: '新用户注册赠送余额',
|
||||
time: '2026-02-03 10:07:59',
|
||||
amount: '+ 88888.00',
|
||||
user: '1岁上班22岁退休',
|
||||
type: '新用户注册赠送余额',
|
||||
remark: '新用户注册赠送88888余额'
|
||||
},
|
||||
{
|
||||
id: '31212',
|
||||
order: '新用户注册赠送余额',
|
||||
time: '2026-02-03 02:17:24',
|
||||
amount: '+ 88888.00',
|
||||
user: '136****0434',
|
||||
type: '新用户注册赠送余额',
|
||||
remark: '新用户注册赠送88888余额'
|
||||
},
|
||||
{
|
||||
id: '31211',
|
||||
order: '新用户注册赠送余额',
|
||||
time: '2026-02-03 02:04:17',
|
||||
amount: '+ 88888.00',
|
||||
user: '灵境',
|
||||
type: '新用户注册赠送余额',
|
||||
remark: '新用户注册赠送88888余额'
|
||||
},
|
||||
{
|
||||
id: '31210',
|
||||
order: '新用户注册赠送余额',
|
||||
time: '2026-02-03 00:58:21',
|
||||
amount: '+ 88888.00',
|
||||
user: 'J.',
|
||||
type: '新用户注册赠送余额',
|
||||
remark: '新用户注册赠送88888余额'
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
})
|
||||
|
||||
const handleQuery = () => {
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadList()
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
const goToJumpPage = () => {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadList()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '页码无效', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function getBillTypeText(type : string) : string {
|
||||
const found = typeOptions.find(opt => opt.value === type)
|
||||
return found != null ? found.text : type
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -148,40 +195,31 @@ const tableData = ref<BalanceRecord[]>([
|
||||
|
||||
.filter-card { padding: 24px; margin-bottom: 20px; }
|
||||
.filter-row { display: flex; flex-direction: row; align-items: center; }
|
||||
.filter-item { display: flex; flex-direction: row; align-items: center; margin-right: 40px; }
|
||||
.filter-label { font-size: 14px; color: #333; margin-right: 15px; }
|
||||
.filter-item { display: flex; flex-direction: row; align-items: center; margin-right: 28px; }
|
||||
.filter-label { font-size: 14px; color: #333; margin-right: 12px; white-space: nowrap; }
|
||||
|
||||
.date-picker-wrap {
|
||||
width: 260px;
|
||||
height: 36px;
|
||||
border: 1px solid #dcdfe6;
|
||||
.search-wrap { flex: 1; }
|
||||
.search-input { flex: 1; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 13px; }
|
||||
|
||||
.data-select { width: 160px; }
|
||||
|
||||
.btn-query {
|
||||
background-color: #1890ff;
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select-box {
|
||||
width: 200px;
|
||||
height: 36px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.calendar-icon { font-size: 14px; margin-right: 10px; color: #c0c4cc; }
|
||||
.date-placeholder, .select-txt { font-size: 14px; color: #c0c4cc; }
|
||||
.arrow-down { margin-left: auto; font-size: 10px; color: #c0c4cc; }
|
||||
.btn-txt { color: #fff; font-size: 14px; }
|
||||
|
||||
/* 表格样式 */
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 600px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -189,7 +227,6 @@ const tableData = ref<BalanceRecord[]>([
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.th {
|
||||
@@ -201,15 +238,20 @@ const tableData = ref<BalanceRecord[]>([
|
||||
|
||||
.th-txt {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background-color: #fff;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
@@ -217,7 +259,7 @@ const tableData = ref<BalanceRecord[]>([
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 16px 10px;
|
||||
padding: 12px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -228,20 +270,57 @@ const tableData = ref<BalanceRecord[]>([
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 列宽分配 (参考截图比例) */
|
||||
.u-info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.u-name { font-size: 13px; color: #303133; font-weight: 500; }
|
||||
.u-id { font-size: 11px; color: #909399; }
|
||||
}
|
||||
|
||||
/* 列宽分配 */
|
||||
.col-id { width: 70px; }
|
||||
.col-order { flex: 1.5; min-width: 180px; justify-content: flex-start; }
|
||||
.col-order { width: 200px; justify-content: flex-start; }
|
||||
.col-time { width: 160px; }
|
||||
.col-amount { width: 120px; }
|
||||
.col-user { width: 120px; }
|
||||
.col-type { width: 150px; }
|
||||
.col-remark { flex: 1.8; min-width: 200px; justify-content: flex-start; }
|
||||
.col-user { width: 160px; }
|
||||
.col-type { width: 140px; }
|
||||
.col-remark { flex: 1; min-width: 180px; justify-content: flex-start; }
|
||||
.col-op { width: 80px; }
|
||||
|
||||
.text-left { justify-content: flex-start; text-align: left; }
|
||||
|
||||
/* 颜色 */
|
||||
.red-txt { color: #f56c6c; font-weight: 500; }
|
||||
.green-txt { color: #67c23a; font-weight: 500; }
|
||||
.red-txt { color: #f5222d; font-weight: bold; }
|
||||
.green-txt { color: #52c41a; font-weight: bold; }
|
||||
.btn-link { color: #1890ff; font-size: 13px; cursor: pointer; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<text class="icon-white">💰</text>
|
||||
</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-value">1447117274.55</text>
|
||||
<text class="stat-value">{{ statsData.current_balance.toFixed(2) }}</text>
|
||||
<text class="stat-label">当前余额</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -16,7 +16,7 @@
|
||||
<text class="icon-white">🏦</text>
|
||||
</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-value">1602611838.49</text>
|
||||
<text class="stat-value">{{ statsData.total_accumulation.toFixed(2) }}</text>
|
||||
<text class="stat-label">累计余额</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -25,7 +25,7 @@
|
||||
<text class="icon-white">💳</text>
|
||||
</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-value">155494563.94</text>
|
||||
<text class="stat-value">{{ statsData.total_consumption.toFixed(2) }}</text>
|
||||
<text class="stat-label">累计消耗余额</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -144,30 +144,31 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import {
|
||||
fetchAdminBalanceStats,
|
||||
fetchAdminBalanceTrend,
|
||||
fetchAdminBalanceDistribution
|
||||
} from '@/services/admin/financeService.uts'
|
||||
|
||||
const trendOption = ref<any>(null)
|
||||
const sourceOption = ref<any>(null)
|
||||
const consumptionOption = ref<any>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// 顶部汇总指标
|
||||
const statsData = ref({
|
||||
current_balance: 0,
|
||||
total_accumulation: 0,
|
||||
total_consumption: 0
|
||||
})
|
||||
|
||||
// 样式切换状态: 0=图表, 1=列表
|
||||
const sourceStyleMode = ref(0)
|
||||
const consumptionStyleMode = ref(0)
|
||||
|
||||
// 统计数据 (使用 ref 保证响应式)
|
||||
const sourceData = ref([
|
||||
{ value: 125000.00, name: '系统增加', percent: 40.00 },
|
||||
{ value: 93750.00, name: '用户充值', percent: 30.00 },
|
||||
{ value: 78125.00, name: '佣金提现', percent: 25.00 },
|
||||
{ value: 62500.00, name: '抽奖赠送', percent: 20.00 },
|
||||
{ value: 46875.00, name: '商品退款', percent: 15.00 }
|
||||
])
|
||||
|
||||
const consumptionDataList = ref([
|
||||
{ value: 435692.51, name: '购买商品', percent: 50.00 },
|
||||
{ value: 8060.18, name: '购买会员', percent: 20.00 },
|
||||
{ value: 0.00, name: '充值退款', percent: 15.00 },
|
||||
{ value: 0.00, name: '系统减少', percent: 15.00 }
|
||||
])
|
||||
// 统计数据列表 (用于展示样式1)
|
||||
const sourceData = ref<any[]>([])
|
||||
const consumptionDataList = ref<any[]>([])
|
||||
|
||||
/**
|
||||
* 转换 Plain Object 工具
|
||||
@@ -195,17 +196,46 @@ function toPlainObject(obj : any) : any {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initTrendChart()
|
||||
initSourceChart()
|
||||
initConsumptionChart()
|
||||
}, 300)
|
||||
loadAllData()
|
||||
})
|
||||
|
||||
function initTrendChart() {
|
||||
const dates = ['01-05', '01-06', '01-07', '01-08', '01-09', '01-10', '01-11', '01-12', '01-13', '01-14', '01-15', '01-16', '01-17', '01-18', '01-19', '01-20', '01-21', '01-22', '01-23', '01-24', '01-25', '01-26', '01-27', '01-28', '01-29', '01-30', '01-31', '02-01', '02-02', '02-03']
|
||||
const accumulationData = [2500000, 2900000, 1500000, 2400000, 1800000, 1300000, 500000, 2100000, 3000000, 2800000, 2300000, 2200000, 1500000, 1100000, 2300000, 2800000, 2600000, 2700000, 1800000, 1950000, 650000, 1600000, 1750000, 2400000, 2600000, 2000000, 1400000, 550000, 2100000, 550000]
|
||||
const consumptionData = [10000, 20000, 15000, 120000, 50000, 20000, 10000, 30000, 40000, 35000, 60000, 25000, 30000, 45000, 55000, 110000, 60000, 50000, 40000, 35000, 85000, 45000, 120000, 50000, 45000, 40000, 35000, 55000, 65000, 45000]
|
||||
async function loadAllData() {
|
||||
loading.value = true
|
||||
const endTime = new Date().toISOString()
|
||||
const startTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() // 默认最近30天
|
||||
|
||||
try {
|
||||
// 1. 加载汇总指标
|
||||
const stats = await fetchAdminBalanceStats()
|
||||
if (stats != null) {
|
||||
statsData.value.current_balance = (stats as any).current_balance ?? 0
|
||||
statsData.value.total_accumulation = (stats as any).total_accumulation ?? 0
|
||||
statsData.value.total_consumption = (stats as any).total_consumption ?? 0
|
||||
}
|
||||
|
||||
// 2. 加载趋势数据
|
||||
const trendData = await fetchAdminBalanceTrend(startTime, endTime)
|
||||
initTrendChart(trendData)
|
||||
|
||||
// 3. 加载分布数据 (来源与消耗)
|
||||
const distRes = await fetchAdminBalanceDistribution(startTime, endTime)
|
||||
if (distRes != null) {
|
||||
sourceData.value = (distRes as any).income ?? []
|
||||
consumptionDataList.value = (distRes as any).expense ?? []
|
||||
initSourceChart()
|
||||
initConsumptionChart()
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载统计数据失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function initTrendChart(data : any[]) {
|
||||
const dates = data.map(item => item.date_group.substring(5))
|
||||
const accumulationData = data.map(item => item.accumulation)
|
||||
const consumptionData = data.map(item => item.consumption)
|
||||
|
||||
const option = {
|
||||
grid: { left: '3%', right: '4%', bottom: '5%', top: '5%', containLabel: true },
|
||||
@@ -255,7 +285,6 @@ function initSourceChart() {
|
||||
radius: '70%',
|
||||
center: ['40%', '50%'],
|
||||
label: { show: true, fontSize: 11, formatter: '{b}\n{c}%' },
|
||||
// 关键点:将图表数据映射到 percent 字段
|
||||
data: sourceData.value.map(item => ({ value: item.percent, name: item.name }))
|
||||
}
|
||||
]
|
||||
@@ -275,7 +304,6 @@ function initConsumptionChart() {
|
||||
radius: '70%',
|
||||
center: ['40%', '50%'],
|
||||
label: { show: true, fontSize: 11, formatter: '{b}\n{c}%' },
|
||||
// 关键点:将图表数据映射到 percent 字段
|
||||
data: consumptionDataList.value.map(item => ({ value: item.percent, name: item.name }))
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">创建时间:</text>
|
||||
<text class="filter-label">时间筛选:</text>
|
||||
<view class="date-picker-wrap">
|
||||
<text class="calendar-icon">📅</text>
|
||||
<text class="date-placeholder">开始日期 - 结束日期</text>
|
||||
<text class="date-placeholder">最近30天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -17,36 +17,45 @@
|
||||
<view class="content-card border-shadow">
|
||||
<!-- 汇总选项卡 -->
|
||||
<view class="tab-header">
|
||||
<view class="tab-item active"><text class="tab-txt active-txt">日账单</text></view>
|
||||
<view class="tab-item"><text class="tab-txt">周账单</text></view>
|
||||
<view class="tab-item"><text class="tab-txt">月账单</text></view>
|
||||
<view
|
||||
v-for="(item, index) in intervalOptions"
|
||||
:key="index"
|
||||
:class="['tab-item', currentTab === index ? 'active' : '']"
|
||||
@click="handleTabChange(index)"
|
||||
>
|
||||
<text :class="['tab-txt', currentTab === index ? 'active-txt' : '']">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<view class="table-container">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">ID</text></view>
|
||||
<view class="th col-title"><text class="th-txt">标题</text></view>
|
||||
<view class="th col-date"><text class="th-txt">日期</text></view>
|
||||
<view class="th col-id"><text class="th-txt">序号</text></view>
|
||||
<view class="th col-title"><text class="th-txt">账单周期</text></view>
|
||||
<view class="th col-date"><text class="th-txt">具体日期/周期</text></view>
|
||||
<view class="th col-income"><text class="th-txt">收入金额</text></view>
|
||||
<view class="th col-expense"><text class="th-txt">支出金额</text></view>
|
||||
<view class="th col-entry"><text class="th-txt">入账金额</text></view>
|
||||
<view class="th col-entry"><text class="th-txt">净入账</text></view>
|
||||
<view class="th col-ops"><text class="th-txt">操作</text></view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td col-title text-left"><text class="td-txt">{{ item.title }}</text></view>
|
||||
<view class="td col-date"><text class="td-txt">{{ item.date }}</text></view>
|
||||
<view class="td col-income"><text class="td-txt red-txt">¥{{ item.income }}</text></view>
|
||||
<view class="td col-expense"><text class="td-txt green-txt">¥{{ item.expense }}</text></view>
|
||||
<view class="td col-entry"><text class="td-txt">¥{{ item.entry }}</text></view>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="tableData.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无账单数据</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in tableData" :key="index">
|
||||
<view class="td col-id"><text class="td-txt">{{ index + 1 }}</text></view>
|
||||
<view class="td col-title text-left"><text class="td-txt">{{ intervalOptions[currentTab].label }}</text></view>
|
||||
<view class="td col-date"><text class="td-txt">{{ item.date_group }}</text></view>
|
||||
<view class="td col-income"><text class="td-txt red-txt">¥{{ item.income.toFixed(2) }}</text></view>
|
||||
<view class="td col-expense"><text class="td-txt green-txt">¥{{ item.expense.toFixed(2) }}</text></view>
|
||||
<view class="td col-entry"><text class="td-txt">¥{{ item.net_entry.toFixed(2) }}</text></view>
|
||||
<view class="td col-ops">
|
||||
<view class="ops-wrap">
|
||||
<text class="op-link">账单详情</text>
|
||||
<text class="op-divider">|</text>
|
||||
<text class="op-link">下载</text>
|
||||
<text class="op-link">查看流水</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -57,29 +66,46 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { fetchFinanceBillSummary } from '@/services/admin/financeService.uts'
|
||||
|
||||
interface BillSumRecord {
|
||||
id: number
|
||||
title: string
|
||||
date: string
|
||||
income: string
|
||||
expense: string
|
||||
entry: string
|
||||
const currentTab = ref(0)
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const intervalOptions = [
|
||||
{ label: '日账单', value: 'day' },
|
||||
{ label: '周账单', value: 'week' },
|
||||
{ label: '月账单', value: 'month' }
|
||||
]
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
const endTime = new Date().toISOString()
|
||||
const startTime = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString() // 默认查最近90天
|
||||
|
||||
try {
|
||||
const res = await fetchFinanceBillSummary(
|
||||
startTime,
|
||||
endTime,
|
||||
intervalOptions[currentTab.value].value
|
||||
)
|
||||
tableData.value = res
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载账单失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const tableData = ref<BillSumRecord[]>([
|
||||
{ id: 1, title: '日账单', date: '2026-02-03', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 2, title: '日账单', date: '2026-02-01', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 3, title: '日账单', date: '2026-01-30', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 4, title: '日账单', date: '2026-01-29', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 5, title: '日账单', date: '2026-01-28', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 6, title: '日账单', date: '2026-01-27', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 7, title: '日账单', date: '2026-01-26', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 8, title: '日账单', date: '2026-01-25', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 9, title: '日账单', date: '2026-01-23', income: '0.00', expense: '0.00', entry: '0.00' },
|
||||
{ id: 10, title: '日账单', date: '2026-01-22', income: '0.00', expense: '0.00', entry: '0.00' }
|
||||
])
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
const handleTabChange = (index : number) => {
|
||||
currentTab.value = index
|
||||
loadData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -187,7 +213,7 @@ const tableData = ref<BillSumRecord[]>([
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #e6f0ff; /* 对应截图的淡蓝色背景 */
|
||||
background-color: #e6f0ff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -227,7 +253,6 @@ const tableData = ref<BillSumRecord[]>([
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 列宽分配 */
|
||||
.col-id { width: 80px; }
|
||||
.col-title { width: 150px; }
|
||||
.col-date { width: 220px; }
|
||||
@@ -240,16 +265,14 @@ const tableData = ref<BillSumRecord[]>([
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* 颜色 */
|
||||
.red-txt {
|
||||
color: #f5222d; /* 收入红色 */
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.green-txt {
|
||||
color: #52c41a; /* 支出绿色 */
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
/* 操作项 */
|
||||
.ops-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -261,162 +284,4 @@ const tableData = ref<BillSumRecord[]>([
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.op-divider {
|
||||
margin: 0 8px;
|
||||
color: #e8e8e8;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.finance-bill {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.date-picker-wrap, .select-box {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.date-placeholder, .select-txt {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
margin-left: auto;
|
||||
font-size: 8px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.btn-query, .btn-export {
|
||||
height: 32px;
|
||||
padding: 0 20px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-query {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.btn-txt { color: #fff; font-size: 14px; }
|
||||
.export-txt { color: #606266; font-size: 14px; }
|
||||
|
||||
/* 表格样式 */
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #f8f9fb;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.th-txt {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.td-txt {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.col-id { width: 100px; }
|
||||
.col-title { width: 150px; }
|
||||
.col-type { width: 120px; }
|
||||
.col-amount { width: 120px; }
|
||||
.col-time { width: 180px; }
|
||||
.col-remark { flex: 1; min-width: 200px; }
|
||||
|
||||
.text-left { justify-content: flex-start; }
|
||||
|
||||
.type-tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tag-green { background-color: #f0f9eb; color: #67c23a; border: 1px solid #e1f3d8; }
|
||||
.tag-orange { background-color: #fdf6ec; color: #e6a23c; border: 1px solid #faecd8; }
|
||||
|
||||
.green-txt { color: #67c23a; }
|
||||
.red-txt { color: #f56c6c; }
|
||||
</style>
|
||||
|
||||
@@ -4,24 +4,18 @@
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">订单时间:</text>
|
||||
<view class="date-picker-wrap">
|
||||
<text class="calendar-icon">📅</text>
|
||||
<text class="date-placeholder">开始日期 - 结束日期</text>
|
||||
</view>
|
||||
<text class="filter-label">交易类型:</text>
|
||||
<uni-data-select v-model="typeValue" :localdata="typeOptions" class="data-select" @change="handleQuery" />
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">交易类型:</text>
|
||||
<view class="select-box">
|
||||
<text class="select-txt">请选择</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<text class="filter-label">收支类型:</text>
|
||||
<uni-data-select v-model="pmValue" :localdata="pmOptions" class="data-select" @change="handleQuery" />
|
||||
</view>
|
||||
<view class="filter-item search-wrap">
|
||||
<text class="filter-label">流水搜索:</text>
|
||||
<input class="search-input" placeholder="订单号/昵称/电话/用户ID" />
|
||||
<input class="search-input" v-model="searchKeyword" placeholder="订单号/昵称/电话/用户ID" @confirm="handleQuery" />
|
||||
</view>
|
||||
<view class="btn-query">
|
||||
<view class="btn-query" @click="handleQuery">
|
||||
<text class="btn-txt">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -30,112 +24,170 @@
|
||||
<!-- 数据表格 (Flex 模拟) -->
|
||||
<view class="table-container border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th col-flow"><text class="th-txt">交易单号</text></view>
|
||||
<view class="th col-order"><text class="th-txt">关联订单</text></view>
|
||||
<view class="th col-flow"><text class="th-txt">交易 ID</text></view>
|
||||
<view class="th col-order"><text class="th-txt">关联单据</text></view>
|
||||
<view class="th col-time"><text class="th-txt">交易时间</text></view>
|
||||
<view class="th col-amount"><text class="th-txt">交易金额</text></view>
|
||||
<view class="th col-user"><text class="th-txt">交易用户</text></view>
|
||||
<view class="th col-method"><text class="th-txt">支付方式</text></view>
|
||||
<view class="th col-method"><text class="th-txt">业务类型</text></view>
|
||||
<view class="th col-remark"><text class="th-txt">备注</text></view>
|
||||
<view class="th col-ops"><text class="th-txt">操作</text></view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in tableData" :key="item.flowNo">
|
||||
<view class="td col-flow"><text class="td-txt">{{ item.flowNo }}</text></view>
|
||||
<view class="td col-order text-left"><text class="td-txt">{{ item.orderNo }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="td col-amount"><text class="td-txt red-txt">{{ item.amount }}</text></view>
|
||||
<view class="td col-user"><text class="td-txt">{{ item.user }}</text></view>
|
||||
<view class="td col-method"><text class="td-txt">{{ item.method }}</text></view>
|
||||
<view class="td col-remark"><text class="td-txt">{{ item.remark }}</text></view>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="tableData.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无流水记录</text>
|
||||
</view>
|
||||
<view class="table-row" v-for="item in tableData" :key="item.id">
|
||||
<view class="td col-flow"><text class="td-txt">{{ item.id.substring(0, 8) }}...</text></view>
|
||||
<view class="td col-order text-left"><text class="td-txt">{{ item.link_id || '-' }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.created_at.substring(0, 16).replace('T', ' ') }}</text></view>
|
||||
<view class="td col-amount">
|
||||
<text class="td-txt" :class="item.pm === 1 ? 'red-txt' : 'green-txt'">
|
||||
{{ item.pm === 1 ? '+' : '-' }}{{ item.number.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td col-user">
|
||||
<view class="u-info-box">
|
||||
<text class="u-name">{{ item.user_name || '未知' }}</text>
|
||||
<text class="u-id">UID:{{ item.uid.substring(0, 8) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-method"><text class="td-txt">{{ getBillTypeText(item.type) }}</text></view>
|
||||
<view class="td col-remark"><text class="td-txt">{{ item.mark || '-' }}</text></view>
|
||||
<view class="td col-ops">
|
||||
<text class="op-link">备注</text>
|
||||
<text class="op-link">详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<view class="p-btn" :class="{ disabled: page <= 1 }" @click="prevPage">
|
||||
<text><</text>
|
||||
</view>
|
||||
<view class="p-btn active">
|
||||
<text>{{ page }}</text>
|
||||
</view>
|
||||
<view class="p-btn" :class="{ disabled: page >= totalPages }" @click="nextPage">
|
||||
<text>></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { fetchUserBillList } from '@/services/admin/financeService.uts'
|
||||
import { UserBillRecord } from '@/types/admin/finance.uts'
|
||||
|
||||
interface FlowRecord {
|
||||
flowNo: string
|
||||
orderNo: string
|
||||
time: string
|
||||
amount: string
|
||||
user: string
|
||||
method: string
|
||||
remark: string
|
||||
const typeValue = ref<string>('all')
|
||||
const pmValue = ref<string>('all')
|
||||
const searchKeyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const typeOptions = [
|
||||
{ value: 'all', text: '全部类型' },
|
||||
{ value: 'recharge', text: '充值' },
|
||||
{ value: 'extract', text: '提现' },
|
||||
{ value: 'pay', text: '支付' },
|
||||
{ value: 'refund', text: '退款' }
|
||||
]
|
||||
|
||||
const pmOptions = [
|
||||
{ value: 'all', text: '全部' },
|
||||
{ value: '1', text: '收入' },
|
||||
{ value: '0', text: '支出' }
|
||||
]
|
||||
|
||||
const tableData = ref<UserBillRecord[]>([])
|
||||
|
||||
const totalPages = computed(() : number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
return Math.ceil(total.value / pageSize.value)
|
||||
})
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const pm = pmValue.value == 'all' ? null : parseInt(pmValue.value)
|
||||
const type = typeValue.value == 'all' ? null : typeValue.value
|
||||
|
||||
const res = await fetchUserBillList(
|
||||
page.value,
|
||||
pageSize.value,
|
||||
null, // category 先不传
|
||||
type,
|
||||
pm,
|
||||
null,
|
||||
null,
|
||||
searchKeyword.value || null
|
||||
)
|
||||
tableData.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载流水失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const tableData = ref<FlowRecord[]>([
|
||||
{
|
||||
flowNo: 'ZJ202602031258428744',
|
||||
orderNo: 'hy541422245264752640',
|
||||
time: '2026-02-03 00:58:42',
|
||||
amount: '+0.00',
|
||||
user: 'J.',
|
||||
method: '微信',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
flowNo: 'ZJ202602011056592117',
|
||||
orderNo: 'hy541029224517992448',
|
||||
time: '2026-02-01 22:56:59',
|
||||
amount: '+0.00',
|
||||
user: 'dev 王鑫',
|
||||
method: '微信',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
flowNo: 'ZJ202602010513443817',
|
||||
orderNo: 'hy540942844546777088',
|
||||
time: '2026-02-01 17:13:44',
|
||||
amount: '+0.00',
|
||||
user: 'Sunshine',
|
||||
method: '微信',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
flowNo: 'ZJ202602010127248683',
|
||||
orderNo: 'hy540885887420989440',
|
||||
time: '2026-02-01 13:27:24',
|
||||
amount: '+0.00',
|
||||
user: '132****8769',
|
||||
method: '微信',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
flowNo: 'ZJ202602011215407366',
|
||||
orderNo: 'hy540686639874179072',
|
||||
time: '2026-02-01 00:15:40',
|
||||
amount: '+0.00',
|
||||
user: '177****9187',
|
||||
method: '微信',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
flowNo: 'ZJ202601301026512881',
|
||||
orderNo: 'hy540296867267739648',
|
||||
time: '2026-01-30 22:26:51',
|
||||
amount: '+0.00',
|
||||
user: '暂未成功人士',
|
||||
method: '微信',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
flowNo: 'ZJ202601300534267557',
|
||||
orderNo: 'hy540223279126806528',
|
||||
time: '2026-01-30 17:34:26',
|
||||
amount: '+0.00',
|
||||
user: 'b342865d2077',
|
||||
method: '微信',
|
||||
remark: ''
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
})
|
||||
|
||||
const handleQuery = () => {
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadList()
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
const goToJumpPage = () => {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadList()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '页码无效', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function getBillTypeText(type : string) : string {
|
||||
const found = typeOptions.find(opt => opt.value === type)
|
||||
return found != null ? found.text : type
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -177,47 +229,8 @@ const tableData = ref<FlowRecord[]>([
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-picker-wrap {
|
||||
width: 220px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.date-placeholder {
|
||||
font-size: 13px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.select-box {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.select-txt {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
font-size: 8px;
|
||||
color: #c0c4cc;
|
||||
.data-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
@@ -242,6 +255,7 @@ const tableData = ref<FlowRecord[]>([
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-txt {
|
||||
@@ -263,7 +277,7 @@ const tableData = ref<FlowRecord[]>([
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 15px 10px;
|
||||
padding: 12px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -279,10 +293,11 @@ const tableData = ref<FlowRecord[]>([
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 15px 10px;
|
||||
padding: 12px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -293,25 +308,60 @@ const tableData = ref<FlowRecord[]>([
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.col-flow { width: 200px; }
|
||||
.col-order { width: 220px; }
|
||||
.col-time { width: 180px; }
|
||||
.u-info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.u-name { font-size: 13px; color: #303133; font-weight: 500; }
|
||||
.u-id { font-size: 11px; color: #909399; }
|
||||
}
|
||||
|
||||
.red-txt { color: #f5222d; font-weight: bold; }
|
||||
.green-txt { color: #52c41a; font-weight: bold; }
|
||||
|
||||
.col-flow { width: 140px; }
|
||||
.col-order { width: 200px; }
|
||||
.col-time { width: 160px; }
|
||||
.col-amount { width: 120px; }
|
||||
.col-user { width: 150px; }
|
||||
.col-user { width: 160px; }
|
||||
.col-method { width: 120px; }
|
||||
.col-remark { flex: 1; min-width: 100px; }
|
||||
.col-remark { flex: 1; min-width: 150px; }
|
||||
.col-ops { width: 100px; }
|
||||
|
||||
.text-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.red-txt {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.text-left { justify-content: flex-start; }
|
||||
|
||||
.op-link {
|
||||
font-size: 13px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
</style>
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
<!-- 筛选卡片 -->
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">昵称/手机号/分销商ID:</text>
|
||||
<input class="search-input" placeholder="请输入昵称搜索" />
|
||||
<view class="filter-item search-wrap">
|
||||
<text class="filter-label">佣金搜索:</text>
|
||||
<input class="search-input" v-model="searchKeyword" placeholder="关联单据/用户昵称/UID" @confirm="handleQuery" />
|
||||
</view>
|
||||
<view class="btn-query">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">收支类型:</text>
|
||||
<uni-data-select v-model="pmValue" :localdata="pmOptions" class="data-select" @change="handleQuery" />
|
||||
</view>
|
||||
<view class="btn-query" @click="handleQuery">
|
||||
<text class="btn-txt">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -23,102 +27,159 @@
|
||||
|
||||
<view class="table-container">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">序号</text></view>
|
||||
<view class="th col-order"><text class="th-txt">关联单据</text></view>
|
||||
<view class="th col-time"><text class="th-txt">时间</text></view>
|
||||
<view class="th col-amount"><text class="th-txt">变动金额</text></view>
|
||||
<view class="th col-user"><text class="th-txt">用户信息</text></view>
|
||||
<view class="th col-total"><text class="th-txt">总佣金金额</text></view>
|
||||
<view class="th col-account"><text class="th-txt">账户佣金</text></view>
|
||||
<view class="th col-withdraw"><text class="th-txt">提现佣金</text></view>
|
||||
<view class="th col-type"><text class="th-txt">业务类型</text></view>
|
||||
<view class="th col-remark"><text class="th-txt">备注</text></view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in tableData" :key="item.uid">
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="tableData.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无佣金记录</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td col-order"><text class="td-txt">{{ item.link_id || '-' }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.created_at.substring(0, 16).replace('T', ' ') }}</text></view>
|
||||
<view class="td col-amount">
|
||||
<text :class="['td-txt', item.pm === 1 ? 'red-txt' : 'green-txt']">
|
||||
{{ item.pm === 1 ? '+' : '-' }}{{ item.number.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td col-user">
|
||||
<text class="td-txt">{{ item.userInfo }}</text>
|
||||
</view>
|
||||
<view class="td col-total">
|
||||
<text class="td-txt">{{ item.totalAmount }}</text>
|
||||
</view>
|
||||
<view class="td col-account">
|
||||
<text class="td-txt">{{ item.accountAmount }}</text>
|
||||
</view>
|
||||
<view class="td col-withdraw">
|
||||
<text class="td-txt">{{ item.withdrawAmount }}</text>
|
||||
<view class="u-info-box">
|
||||
<text class="u-name">{{ item.user_name || '未知' }}</text>
|
||||
<text class="u-id">UID:{{ item.uid.substring(0, 8) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-type"><text class="td-txt">{{ getBillTypeText(item.type) }}</text></view>
|
||||
<view class="td col-remark"><text class="td-txt">{{ item.mark || '-' }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<view class="p-btn" :class="{ disabled: page <= 1 }" @click="prevPage">
|
||||
<text><</text>
|
||||
</view>
|
||||
<view class="p-btn active">
|
||||
<text>{{ page }}</text>
|
||||
</view>
|
||||
<view class="p-btn" :class="{ disabled: page >= totalPages }" @click="nextPage">
|
||||
<text>></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { fetchUserBillList } from '@/services/admin/financeService.uts'
|
||||
import { UserBillRecord } from '@/types/admin/finance.uts'
|
||||
|
||||
interface CommissionSummary {
|
||||
uid: string
|
||||
userInfo: string
|
||||
totalAmount: string
|
||||
accountAmount: string
|
||||
withdrawAmount: string
|
||||
const pmValue = ref<string>('all')
|
||||
const searchKeyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const pmOptions = [
|
||||
{ value: 'all', text: '全部收支' },
|
||||
{ value: '1', text: '获得佣金' },
|
||||
{ value: '0', text: '佣金支出' }
|
||||
]
|
||||
|
||||
const tableData = ref<UserBillRecord[]>([])
|
||||
|
||||
const totalPages = computed(() : number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
return Math.ceil(total.value / pageSize.value)
|
||||
})
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const pm = pmValue.value == 'all' ? null : parseInt(pmValue.value)
|
||||
|
||||
const res = await fetchUserBillList(
|
||||
page.value,
|
||||
pageSize.value,
|
||||
'brokerage', // 佣金明细
|
||||
null,
|
||||
pm,
|
||||
null,
|
||||
null,
|
||||
searchKeyword.value || null
|
||||
)
|
||||
tableData.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载佣金记录失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const tableData = ref<CommissionSummary[]>([
|
||||
{
|
||||
uid: '77418',
|
||||
userInfo: '张迪 | 155****5525 | 77418',
|
||||
totalAmount: '11.00',
|
||||
accountAmount: '11.00',
|
||||
withdrawAmount: '0'
|
||||
},
|
||||
{
|
||||
uid: '69696',
|
||||
userInfo: "I'm yours - | 69696",
|
||||
totalAmount: '0.40',
|
||||
accountAmount: '0.40',
|
||||
withdrawAmount: '0'
|
||||
},
|
||||
{
|
||||
uid: '68582',
|
||||
userInfo: 'guan | 68582',
|
||||
totalAmount: '140.80',
|
||||
accountAmount: '140.80',
|
||||
withdrawAmount: '0'
|
||||
},
|
||||
{
|
||||
uid: '65258',
|
||||
userInfo: '纯爱战神别名王富贵儿 | 158****4881 | 65258',
|
||||
totalAmount: '11.00',
|
||||
accountAmount: '11.00',
|
||||
withdrawAmount: '0'
|
||||
},
|
||||
{
|
||||
uid: '66265',
|
||||
userInfo: '王伟兴 | 66265',
|
||||
totalAmount: '11.99',
|
||||
accountAmount: '11.99',
|
||||
withdrawAmount: '0'
|
||||
},
|
||||
{
|
||||
uid: '65270',
|
||||
userInfo: '199****1781 | 199****1781 | 65270',
|
||||
totalAmount: '31.60',
|
||||
accountAmount: '31.60',
|
||||
withdrawAmount: '0'
|
||||
},
|
||||
{
|
||||
uid: '64572',
|
||||
userInfo: '洒笾菂艸 | 188****2434 | 64572',
|
||||
totalAmount: '0.20',
|
||||
accountAmount: '0.20',
|
||||
withdrawAmount: '0'
|
||||
},
|
||||
{
|
||||
uid: '47952',
|
||||
userInfo: '那小子 | 134****3573 | 47952',
|
||||
totalAmount: '19.80',
|
||||
accountAmount: '19.80',
|
||||
withdrawAmount: '0'
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
})
|
||||
|
||||
const handleQuery = () => {
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadList()
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
const goToJumpPage = () => {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadList()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '页码无效', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function getBillTypeText(type : string) : string {
|
||||
if (type == 'extract') return '提现扣除'
|
||||
if (type == 'brokerage') return '分销返佣'
|
||||
if (type == 'refund') return '退款扣除'
|
||||
return type
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -134,225 +195,92 @@ const tableData = ref<CommissionSummary[]>([
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 筛选 */
|
||||
.filter-card {
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
height: 36px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-query {
|
||||
margin-left: 12px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 4px;
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-txt {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 表格区域 */
|
||||
.table-card {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
width: 60px;
|
||||
height: 32px;
|
||||
background-color: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.export-txt {
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #e6f0ff; /* 淡蓝色表头 */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.th-txt {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.td-txt {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* 列宽分配 */
|
||||
.col-user { flex: 2; min-width: 300px; justify-content: flex-start; }
|
||||
.col-total { flex: 1; justify-content: flex-start; }
|
||||
.col-account { flex: 1; justify-content: flex-start; }
|
||||
.col-withdraw { flex: 1; justify-content: flex-start; }
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.finance-commission {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.border-shadow {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.date-picker-wrap {
|
||||
width: 220px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.calendar-icon { font-size: 12px; margin-right: 8px; }
|
||||
.date-placeholder { font-size: 13px; color: #c0c4cc; }
|
||||
.filter-card { padding: 24px; margin-bottom: 20px; }
|
||||
.filter-row { display: flex; flex-direction: row; align-items: center; }
|
||||
.filter-item { display: flex; flex-direction: row; align-items: center; margin-right: 28px; }
|
||||
.filter-label { font-size: 14px; color: #333; margin-right: 12px; white-space: nowrap; }
|
||||
|
||||
.search-wrap { flex: 1; }
|
||||
.search-input {
|
||||
width: 200px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.search-input { flex: 1; height: 36px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 14px; }
|
||||
|
||||
.data-select { width: 160px; }
|
||||
|
||||
.btn-query {
|
||||
height: 32px;
|
||||
padding: 0 20px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 4px;
|
||||
height: 36px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-txt { color: #fff; font-size: 14px; }
|
||||
|
||||
/* 表格 */
|
||||
.table-container { display: flex; flex-direction: column; }
|
||||
.table-header { background-color: #f8f9fb; display: flex; flex-direction: row; border-bottom: 1px solid #ebeef5; }
|
||||
.th { padding: 15px 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.th-txt { font-size: 14px; font-weight: 600; color: #909399; }
|
||||
/* 表格区域 */
|
||||
.table-card { padding: 0; }
|
||||
.action-bar { padding: 15px 20px; }
|
||||
.btn-export { width: 60px; height: 32px; background-color: #e6f7ff; border: 1px solid #91d5ff; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.export-txt { color: #1890ff; font-size: 14px; }
|
||||
|
||||
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #ebeef5; }
|
||||
.td { padding: 15px 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.table-container { display: flex; flex-direction: column; }
|
||||
.table-header { background-color: #e6f0ff; display: flex; flex-direction: row; }
|
||||
.th { padding: 14px 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.th-txt { font-size: 13px; font-weight: 600; color: #303133; }
|
||||
|
||||
.table-body { display: flex; flex-direction: column; }
|
||||
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 60px; }
|
||||
.table-row:hover { background-color: #f9f9f9; }
|
||||
.td { padding: 12px 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.td-txt { font-size: 13px; color: #606266; }
|
||||
|
||||
.col-id { width: 80px; }
|
||||
.col-user { width: 180px; justify-content: flex-start; }
|
||||
.col-order { width: 180px; }
|
||||
.col-amount { width: 120px; }
|
||||
.col-type { width: 120px; }
|
||||
.col-time { width: 180px; }
|
||||
.col-status { width: 100px; }
|
||||
|
||||
.avatar { width: 32px; height: 32px; border-radius: 16px; margin-right: 10px; }
|
||||
.user-name { font-size: 13px; color: #606266; }
|
||||
.green-txt { color: #67c23a; font-weight: bold; }
|
||||
|
||||
.status-dot-active {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #67c23a;
|
||||
margin-right: 6px;
|
||||
.u-info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.u-name { font-size: 13px; color: #303133; font-weight: 500; }
|
||||
.u-id { font-size: 11px; color: #909399; }
|
||||
}
|
||||
|
||||
.red-txt { color: #f5222d; font-weight: bold; }
|
||||
.green-txt { color: #52c41a; font-weight: bold; }
|
||||
|
||||
/* 列宽分配 */
|
||||
.col-id { width: 70px; }
|
||||
.col-order { width: 180px; }
|
||||
.col-time { width: 160px; }
|
||||
.col-amount { width: 120px; }
|
||||
.col-user { width: 180px; justify-content: flex-start; }
|
||||
.col-type { width: 120px; }
|
||||
.col-remark { flex: 1; min-width: 150px; justify-content: flex-start; }
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
</style>
|
||||
|
||||
@@ -7,20 +7,14 @@
|
||||
<text class="filter-label">创建时间:</text>
|
||||
<view class="date-picker-wrap">
|
||||
<text class="calendar-icon">📅</text>
|
||||
<text class="date-placeholder">开始日期 - 结束日期</text>
|
||||
<text class="date-placeholder">最近30天</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<view class="filter-item search-wrap">
|
||||
<text class="filter-label">搜索:</text>
|
||||
<view class="search-group">
|
||||
<view class="select-trigger">
|
||||
<text class="select-txt">请选择</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<input class="search-input" placeholder="请输入" />
|
||||
</view>
|
||||
<input class="search-input" v-model="searchKeyword" placeholder="订单号/抬头/用户名" @confirm="handleQuery" />
|
||||
</view>
|
||||
<view class="btn-query">
|
||||
<view class="btn-query" @click="handleQuery">
|
||||
<text class="btn-txt">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -33,134 +27,206 @@
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === index }"
|
||||
@click="activeTab = index"
|
||||
@click="handleTabChange(index)"
|
||||
>
|
||||
<text class="tab-title">{{ tab.name }} ({{ tab.count }})</text>
|
||||
<text class="tab-title">{{ tab.name }}</text>
|
||||
<view class="active-line" v-if="activeTab === index"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据表格 (Flex 模拟) -->
|
||||
<!-- 数据表格 -->
|
||||
<view class="table-container border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th col-order"><text class="th-txt">订单号</text></view>
|
||||
<view class="th col-amount"><text class="th-txt">订单金额</text></view>
|
||||
<view class="th col-type"><text class="th-txt">发票类型</text></view>
|
||||
<view class="th col-header"><text class="th-txt">发票抬头类型</text></view>
|
||||
<view class="th col-time"><text class="th-txt">下单时间</text></view>
|
||||
<view class="th col-header"><text class="th-txt">抬头信息</text></view>
|
||||
<view class="th col-time"><text class="th-txt">申请时间</text></view>
|
||||
<view class="th col-status-inv"><text class="th-txt">开票状态</text></view>
|
||||
<view class="th col-status-order"><text class="th-txt">订单状态</text></view>
|
||||
<view class="th col-ops"><text class="th-txt">操作</text></view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in tableData" :key="item.orderNo">
|
||||
<view class="td col-order text-left"><text class="td-txt">{{ item.orderNo }}</text></view>
|
||||
<view class="td col-amount"><text class="td-txt">¥ {{ item.amount }}</text></view>
|
||||
<view class="td col-type"><text class="td-txt">{{ item.invoiceType }}</text></view>
|
||||
<view class="td col-header"><text class="td-txt">{{ item.headerType }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="td col-status-inv"><text class="td-txt red-cell">{{ item.invStatus }}</text></view>
|
||||
<view class="td col-status-order"><text class="td-txt">{{ item.orderStatus }}</text></view>
|
||||
<view class="td col-ops">
|
||||
<view class="ops-box">
|
||||
<text class="op-link">操作</text>
|
||||
<text class="sep">|</text>
|
||||
<text class="op-link">订单信息</text>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="tableData.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无发票申请</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in tableData" :key="item.id">
|
||||
<view class="td col-order text-left"><text class="td-txt">{{ item.order_no }}</text></view>
|
||||
<view class="td col-amount"><text class="td-txt">¥ {{ item.order_amount.toFixed(2) }}</text></view>
|
||||
<view class="td col-type"><text class="td-txt">{{ item.invoice_type == 1 ? '普通发票' : '专用发票' }}</text></view>
|
||||
<view class="td col-header">
|
||||
<view class="header-box">
|
||||
<text class="td-txt">{{ item.header_name }}</text>
|
||||
<text class="sub-txt" v-if="item.tax_id">税号: {{ item.tax_id }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.created_at.substring(0, 16).replace('T', ' ') }}</text></view>
|
||||
<view class="td col-status-inv">
|
||||
<text class="status-tag" :class="getStatusClass(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td col-ops">
|
||||
<view class="ops-box" v-if="item.status === 0">
|
||||
<text class="op-link" @click="handleProcess(item.id, 1)">开票</text>
|
||||
<text class="sep">|</text>
|
||||
<text class="op-link red-txt" @click="handleProcess(item.id, -1)">驳回</text>
|
||||
</view>
|
||||
<text v-else class="td-txt">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<view class="p-btn" :class="{ disabled: page <= 1 }" @click="prevPage">
|
||||
<text><</text>
|
||||
</view>
|
||||
<view class="p-btn active">
|
||||
<text>{{ page }}</text>
|
||||
</view>
|
||||
<view class="p-btn" :class="{ disabled: page >= totalPages }" @click="nextPage">
|
||||
<text>></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { fetchInvoiceList, processInvoice } from '@/services/admin/financeService.uts'
|
||||
|
||||
const activeTab = ref(0)
|
||||
const searchKeyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const tabs = ref([
|
||||
{ name: '全部发票', count: 163 },
|
||||
{ name: '待开发票', count: 105 },
|
||||
{ name: '已开发票', count: 34 },
|
||||
{ name: '退款发票', count: 33 }
|
||||
])
|
||||
const tabs = [
|
||||
{ name: '全部发票', value: null as number | null },
|
||||
{ name: '待开发票', value: 0 as number | null },
|
||||
{ name: '已开发票', value: 1 as number | null },
|
||||
{ name: '已拒绝', value: -1 as number | null }
|
||||
]
|
||||
|
||||
const tableData = ref([
|
||||
{
|
||||
orderNo: 'cp538981381384962048',
|
||||
amount: '999.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2026-01-27 07:19:35',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '未发货'
|
||||
},
|
||||
{
|
||||
orderNo: 'cp523490058603331584',
|
||||
amount: '9.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2025-12-15 13:22:35',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '未发货'
|
||||
},
|
||||
{
|
||||
orderNo: 'cp524967077409193984',
|
||||
amount: '9.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2025-12-19 15:11:44',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '未发货'
|
||||
},
|
||||
{
|
||||
orderNo: 'cp521126678106210304',
|
||||
amount: '11890.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2025-12-09 00:51:22',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '未发货'
|
||||
},
|
||||
{
|
||||
orderNo: 'cp521126166883467264',
|
||||
amount: '11800.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2025-12-09 00:49:20',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '未发货'
|
||||
},
|
||||
{
|
||||
orderNo: 'cp517015093888679936',
|
||||
amount: '142.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2025-11-27 16:33:24',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '未发货'
|
||||
},
|
||||
{
|
||||
orderNo: 'cp313601579989073920',
|
||||
amount: '0.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2024-05-15 08:59:56',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '待评价'
|
||||
},
|
||||
{
|
||||
orderNo: 'cp503871643047690240',
|
||||
amount: '999.00',
|
||||
invoiceType: '电子普通发票',
|
||||
headerType: '个人',
|
||||
time: '2025-10-22 10:06:01',
|
||||
invStatus: '未开票',
|
||||
orderStatus: '未发货'
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const totalPages = computed(() : number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
return Math.ceil(total.value / pageSize.value)
|
||||
})
|
||||
|
||||
async function loadInvoices() {
|
||||
loading.value = true
|
||||
try {
|
||||
const status = tabs[activeTab.value].value
|
||||
const res = await fetchInvoiceList(
|
||||
page.value,
|
||||
pageSize.value,
|
||||
status,
|
||||
null,
|
||||
null,
|
||||
searchKeyword.value || null
|
||||
)
|
||||
tableData.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载发票列表失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadInvoices()
|
||||
})
|
||||
|
||||
function handleTabChange(index : number) {
|
||||
activeTab.value = index
|
||||
page.value = 1
|
||||
loadInvoices()
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
page.value = 1
|
||||
loadInvoices()
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadInvoices()
|
||||
}
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadInvoices()
|
||||
}
|
||||
}
|
||||
|
||||
function goToJumpPage() {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadInvoices()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '页码无效', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleProcess(id : string, status : number) {
|
||||
const action = status === 1 ? '确认已完成开票' : '确认驳回该申请'
|
||||
uni.showModal({
|
||||
title: '开票处理',
|
||||
content: action,
|
||||
editable: status === -1,
|
||||
placeholderText: '请输入驳回原因',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await processInvoice(id, status, null, res.content || null)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '处理成功' })
|
||||
loadInvoices()
|
||||
} else {
|
||||
uni.showToast({ title: '处理失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getStatusText(status : number) : string {
|
||||
if (status === 0) return '待开票'
|
||||
if (status === 1) return '已开票'
|
||||
if (status === -1) return '已拒绝'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
function getStatusClass(status : number) : string {
|
||||
if (status === 0) return 'status-pending'
|
||||
if (status === 1) return 'status-success'
|
||||
if (status === -1) return 'status-error'
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -176,90 +242,13 @@ const tableData = ref([
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-card {
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-card { padding: 20px 24px; margin-bottom: 20px; }
|
||||
.filter-row { display: flex; flex-direction: row; align-items: center; }
|
||||
.filter-item { display: flex; flex-direction: row; align-items: center; margin-right: 30px; }
|
||||
.filter-label { font-size: 14px; color: #333; margin-right: 8px; }
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.date-picker-wrap {
|
||||
width: 240px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.date-placeholder {
|
||||
font-size: 13px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.select-trigger {
|
||||
width: 90px;
|
||||
height: 100%;
|
||||
border-right: 1px solid #dcdfe6;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.select-txt {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
font-size: 8px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 160px;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.search-wrap { flex: 1; }
|
||||
.search-input { flex: 1; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 13px; }
|
||||
|
||||
.btn-query {
|
||||
background-color: #1890ff;
|
||||
@@ -269,126 +258,69 @@ const tableData = ref([
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-txt {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
.btn-txt { color: #fff; font-size: 14px; }
|
||||
|
||||
/* 选项卡 */
|
||||
.tab-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 12px 20px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-title {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.active-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 2px;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
.tab-section { display: flex; flex-direction: row; margin-bottom: 20px; border-bottom: 1px solid #e4e7ed; padding: 0 10px; }
|
||||
.tab-item { padding: 12px 20px; position: relative; cursor: pointer; }
|
||||
.tab-title { font-size: 14px; color: #606266; transition: all 0.3s; }
|
||||
.tab-item.active .tab-title { color: #1890ff; font-weight: 600; }
|
||||
.active-line { position: absolute; bottom: 0; left: 20px; right: 20px; height: 2px; background-color: #1890ff; }
|
||||
|
||||
/* 表格 */
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.table-container { display: flex; flex-direction: column; }
|
||||
.table-header { background-color: #f8f9fb; display: flex; flex-direction: row; border-bottom: 1px solid #ebeef5; }
|
||||
.th { padding: 15px 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.th-txt { font-size: 14px; font-weight: 600; color: #909399; }
|
||||
|
||||
.table-header {
|
||||
background-color: #f8f9fb;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.table-row { display: flex; flex-direction: row; border-bottom: 1px solid #ebeef5; }
|
||||
.td { padding: 15px 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.td-txt { font-size: 13px; color: #606266; }
|
||||
|
||||
.th {
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.header-box { display: flex; flex-direction: column; align-items: flex-start; .sub-txt { font-size: 11px; color: #999; margin-top: 2px; } }
|
||||
|
||||
.th-txt {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: 15px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.td-txt {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.status-tag { font-size: 12px; padding: 2px 8px; border-radius: 2px; }
|
||||
.status-pending { color: #fa8c16; background: #fff7e6; border: 1px solid #ffd591; }
|
||||
.status-success { color: #52c41a; background: #f6ffed; border: 1px solid #b7eb8f; }
|
||||
.status-error { color: #f5222d; background: #fff1f0; border: 1px solid #ffa39e; }
|
||||
|
||||
/* 列宽分配 */
|
||||
.col-order { width: 220px; justify-content: flex-start; padding-left: 20px; }
|
||||
.col-amount { width: 120px; }
|
||||
.col-type { width: 150px; }
|
||||
.col-header { width: 120px; }
|
||||
.col-time { width: 180px; }
|
||||
.col-status-inv { width: 120px; }
|
||||
.col-status-order { width: 120px; }
|
||||
.col-ops { flex: 1; min-width: 140px; }
|
||||
.col-order { width: 200px; justify-content: flex-start; padding-left: 20px; }
|
||||
.col-amount { width: 100px; }
|
||||
.col-type { width: 120px; }
|
||||
.col-header { flex: 1; min-width: 180px; justify-content: flex-start; }
|
||||
.col-time { width: 160px; }
|
||||
.col-status-inv { width: 100px; }
|
||||
.col-ops { width: 140px; }
|
||||
|
||||
.text-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.text-left { justify-content: flex-start; }
|
||||
.ops-box { display: flex; flex-direction: row; align-items: center; }
|
||||
.op-link { font-size: 13px; color: #1890ff; cursor: pointer; }
|
||||
.red-txt { color: #f5222d; }
|
||||
.sep { font-size: 12px; color: #ebeef5; margin: 0 8px; }
|
||||
|
||||
.red-cell {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.ops-box {
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 14px; cursor: pointer;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
|
||||
|
||||
.op-link {
|
||||
font-size: 13px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sep {
|
||||
font-size: 12px;
|
||||
color: #ebeef5;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
</style>
|
||||
|
||||
@@ -4,24 +4,14 @@
|
||||
<view class="filter-card border-shadow">
|
||||
<view class="filter-row">
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">时间选择:</text>
|
||||
<view class="date-picker-wrap">
|
||||
<text class="calendar-icon">📅</text>
|
||||
<text class="date-placeholder">开始日期 - 结束日期</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="filter-item">
|
||||
<text class="filter-label">支付类型:</text>
|
||||
<view class="select-box">
|
||||
<text class="select-txt">全部</text>
|
||||
<text class="arrow-down">▼</text>
|
||||
</view>
|
||||
<text class="filter-label">支付状态:</text>
|
||||
<uni-data-select v-model="paidValue" :localdata="paidOptions" class="data-select" @change="handleQuery" />
|
||||
</view>
|
||||
<view class="filter-item search-wrap">
|
||||
<text class="filter-label">搜索:</text>
|
||||
<input class="search-input" placeholder="请输入用户昵称、订单号" />
|
||||
<input class="search-input" v-model="searchKeyword" placeholder="请输入用户昵称、订单号" @confirm="handleQuery" />
|
||||
</view>
|
||||
<view class="btn-query">
|
||||
<view class="btn-query" @click="handleQuery">
|
||||
<text class="btn-txt">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -29,7 +19,7 @@
|
||||
|
||||
<!-- 统计网格 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card border-shadow" v-for="(item, index) in stats" :key="index">
|
||||
<view class="stat-card border-shadow" v-for="(item, index) in statsItems" :key="index">
|
||||
<view class="icon-circle" :class="item.colorClass">
|
||||
<text class="stat-icon">{{ item.icon }}</text>
|
||||
</view>
|
||||
@@ -51,8 +41,7 @@
|
||||
<view class="table-container border-shadow">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">ID</text></view>
|
||||
<view class="th col-avatar"><text class="th-txt">头像</text></view>
|
||||
<view class="th col-nickname"><text class="th-txt">用户昵称</text></view>
|
||||
<view class="th col-nickname"><text class="th-txt">用户信息</text></view>
|
||||
<view class="th col-orderno"><text class="th-txt">订单号</text></view>
|
||||
<view class="th col-amount"><text class="th-txt">支付金额</text></view>
|
||||
<view class="th col-paid"><text class="th-txt">是否支付</text></view>
|
||||
@@ -62,102 +51,169 @@
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="item in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view class="td col-avatar">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-placeholder" v-if="!item.hasAvatar">🖼️</text>
|
||||
<view class="avatar-img-mock" v-else></view>
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="tableData.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无充值记录</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td col-nickname">
|
||||
<view class="u-info-box">
|
||||
<text class="u-name">{{ item.user_name || '未知' }}</text>
|
||||
<text class="u-email">{{ item.user_email || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-nickname"><text class="td-txt">{{ item.nickname }}</text></view>
|
||||
<view class="td col-orderno"><text class="td-txt">{{ item.orderNo }}</text></view>
|
||||
<view class="td col-amount"><text class="td-txt">{{ item.amount }}</text></view>
|
||||
<view class="td col-paid"><text class="td-txt">{{ item.isPaid }}</text></view>
|
||||
<view class="td col-type"><text class="td-txt">{{ item.type }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="td col-orderno"><text class="td-txt">{{ item.order_no }}</text></view>
|
||||
<view class="td col-amount">
|
||||
<view class="amount-box">
|
||||
<text class="price">¥{{ item.price.toFixed(2) }}</text>
|
||||
<text class="give" v-if="item.give_price > 0">赠:¥{{ item.give_price.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-paid">
|
||||
<text class="status-tag" :class="item.paid == 1 ? 'paid' : 'unpaid'">
|
||||
{{ item.paid == 1 ? '已支付' : '未支付' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td col-type"><text class="td-txt">{{ getRechargeTypeText(item.recharge_type) }}</text></view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.pay_time ? item.pay_time.substring(0, 16).replace('T', ' ') : '-' }}</text></view>
|
||||
<view class="td col-ops">
|
||||
<text class="op-link">删除</text>
|
||||
<text class="op-link">详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-footer">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns">
|
||||
<view class="p-btn" :class="{ disabled: page <= 1 }" @click="prevPage">
|
||||
<text><</text>
|
||||
</view>
|
||||
<view class="p-btn active">
|
||||
<text>{{ page }}</text>
|
||||
</view>
|
||||
<view class="p-btn" :class="{ disabled: page >= totalPages }" @click="nextPage">
|
||||
<text>></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-jump">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { fetchRechargeList, fetchFinanceOverview } from '@/services/admin/financeService.uts'
|
||||
|
||||
const stats = ref([
|
||||
{ label: '充值总金额', value: '446747490.72', icon: '¥', colorClass: 'blue' },
|
||||
{ label: '充值退款金额', value: '1.19', icon: '¥', colorClass: 'orange' },
|
||||
{ label: '支付宝充值金额', value: '78812.24', icon: '支', colorClass: 'green' },
|
||||
{ label: '微信充值金额', value: '8518075.11', icon: '✔', colorClass: 'pink' }
|
||||
const paidValue = ref<string>('all')
|
||||
const searchKeyword = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const jumpPage = ref('')
|
||||
|
||||
const paidOptions = [
|
||||
{ value: 'all', text: '全部状态' },
|
||||
{ value: '1', text: '已支付' },
|
||||
{ value: '0', text: '未支付' }
|
||||
]
|
||||
|
||||
const statsItems = ref([
|
||||
{ label: '充值总金额', value: '0.00', icon: '¥', colorClass: 'blue' },
|
||||
{ label: '充值退款金额', value: '0.00', icon: '¥', colorClass: 'orange' },
|
||||
{ label: '本月充值金额', value: '0.00', icon: '月', colorClass: 'green' },
|
||||
{ label: '今日充值金额', value: '0.00', icon: '今', colorClass: 'pink' }
|
||||
])
|
||||
|
||||
interface RechargeRecord {
|
||||
id: number
|
||||
hasAvatar: boolean
|
||||
nickname: string
|
||||
orderNo: string
|
||||
amount: string
|
||||
isPaid: string
|
||||
type: string
|
||||
time: string
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const totalPages = computed(() : number => {
|
||||
if (pageSize.value <= 0) return 1
|
||||
return Math.ceil(total.value / pageSize.value)
|
||||
})
|
||||
|
||||
async function loadStats() {
|
||||
const endTime = new Date().toISOString()
|
||||
const startTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||
const res = await fetchFinanceOverview(startTime, endTime)
|
||||
if (res != null) {
|
||||
statsItems.value[0].value = res.recharge_amount.toFixed(2)
|
||||
}
|
||||
}
|
||||
|
||||
const tableData = ref<RechargeRecord[]>([
|
||||
{
|
||||
id: 4522,
|
||||
hasAvatar: true,
|
||||
nickname: 'TTA NW',
|
||||
orderNo: 'cz540603403592531968',
|
||||
amount: '10.00',
|
||||
isPaid: '未支付',
|
||||
type: '微信充值',
|
||||
time: '暂无'
|
||||
},
|
||||
{
|
||||
id: 4521,
|
||||
hasAvatar: true,
|
||||
nickname: 'TTA NW',
|
||||
orderNo: 'cz540592008763277312',
|
||||
amount: '3343.00',
|
||||
isPaid: '未支付',
|
||||
type: '微信充值',
|
||||
time: '暂无'
|
||||
},
|
||||
{
|
||||
id: 4520,
|
||||
hasAvatar: true,
|
||||
nickname: '绯',
|
||||
orderNo: 'cz538561368400330752',
|
||||
amount: '500.00',
|
||||
isPaid: '未支付',
|
||||
type: '支付宝充值',
|
||||
time: '暂无'
|
||||
},
|
||||
{
|
||||
id: 4519,
|
||||
hasAvatar: false,
|
||||
nickname: '六六狗',
|
||||
orderNo: 'cz538368229429477376',
|
||||
amount: '50.00',
|
||||
isPaid: '未支付',
|
||||
type: '微信充值',
|
||||
time: '暂无'
|
||||
},
|
||||
{
|
||||
id: 4518,
|
||||
hasAvatar: false,
|
||||
nickname: 'aabbc',
|
||||
orderNo: 'cz538165303901683712',
|
||||
amount: '10.00',
|
||||
isPaid: '未支付',
|
||||
type: '其他充值',
|
||||
time: '暂无'
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const paid = paidValue.value == 'all' ? null : parseInt(paidValue.value)
|
||||
const res = await fetchRechargeList(
|
||||
page.value,
|
||||
pageSize.value,
|
||||
paid,
|
||||
null,
|
||||
null,
|
||||
searchKeyword.value || null
|
||||
)
|
||||
tableData.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载充值记录失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
loadList()
|
||||
})
|
||||
|
||||
const handleQuery = () => {
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (page.value > 1) {
|
||||
page.value--
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (page.value < totalPages.value) {
|
||||
page.value++
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
const goToJumpPage = () => {
|
||||
const targetPage = parseInt(jumpPage.value)
|
||||
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
|
||||
page.value = targetPage
|
||||
loadList()
|
||||
jumpPage.value = ''
|
||||
} else {
|
||||
uni.showToast({ title: '页码无效', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function getRechargeTypeText(type : string | null) : string {
|
||||
if (type == 'wechat') return '微信充值'
|
||||
if (type == 'alipay') return '支付宝充值'
|
||||
if (type == 'system') return '后台补单'
|
||||
return type || '其他充值'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -199,47 +255,8 @@ const tableData = ref<RechargeRecord[]>([
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.date-picker-wrap {
|
||||
width: 220px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.date-placeholder {
|
||||
font-size: 13px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.select-box {
|
||||
width: 160px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.select-txt {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
font-size: 8px;
|
||||
color: #c0c4cc;
|
||||
.data-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.search-wrap {
|
||||
@@ -264,6 +281,7 @@ const tableData = ref<RechargeRecord[]>([
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-txt {
|
||||
@@ -336,6 +354,7 @@ const tableData = ref<RechargeRecord[]>([
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-export-txt {
|
||||
@@ -373,6 +392,7 @@ const tableData = ref<RechargeRecord[]>([
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.td {
|
||||
@@ -387,39 +407,71 @@ const tableData = ref<RechargeRecord[]>([
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.col-id { width: 80px; }
|
||||
.col-avatar { width: 80px; }
|
||||
.col-nickname { width: 150px; }
|
||||
.u-info-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.u-name { font-size: 13px; color: #303133; font-weight: 500; }
|
||||
.u-email { font-size: 11px; color: #909399; }
|
||||
}
|
||||
|
||||
.amount-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.price { font-size: 14px; color: #303133; font-weight: bold; }
|
||||
.give { font-size: 11px; color: #f56c6c; }
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
&.paid { background-color: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
|
||||
&.unpaid { background-color: #fff1f0; color: #ff4d4f; border: 1px solid #ffa39e; }
|
||||
}
|
||||
|
||||
.col-id { width: 60px; }
|
||||
.col-nickname { width: 180px; justify-content: flex-start; }
|
||||
.col-orderno { width: 220px; }
|
||||
.col-amount { width: 120px; }
|
||||
.col-paid { width: 120px; }
|
||||
.col-type { width: 150px; }
|
||||
.col-time { width: 150px; }
|
||||
.col-paid { width: 100px; }
|
||||
.col-type { width: 120px; }
|
||||
.col-time { width: 160px; }
|
||||
.col-ops { flex: 1; min-width: 100px; }
|
||||
|
||||
.avatar-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-img-mock {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.op-link {
|
||||
font-size: 13px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination-footer {
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
.total-txt { font-size: 14px; color: #606266; }
|
||||
.page-btns { display: flex; flex-direction: row; gap: 8px; }
|
||||
.p-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; }
|
||||
.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; }
|
||||
|
||||
.page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; }
|
||||
.jump-txt { font-size: 14px; color: #606266; }
|
||||
.jump-input { width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center; border-radius: 4px; font-size: 14px; }
|
||||
</style>
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
:key="index"
|
||||
class="date-tab-item"
|
||||
:class="{ active: activeDateTab === index }"
|
||||
@click="activeDateTab = index"
|
||||
@click="handleDateTabChange(index)"
|
||||
>{{ item }}</text>
|
||||
</view>
|
||||
<view class="date-picker-wrap">
|
||||
<text class="calendar-icon">D</text>
|
||||
<text class="date-range-text">2026-02-03 - 2026-02-03</text>
|
||||
<text class="date-range-text">{{ displayDateRange }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 交易概况区块 (CRMEB 1:1 复刻) -->
|
||||
<!-- 交易概况区块 -->
|
||||
<view class="overview-card">
|
||||
<view class="overview-header">
|
||||
<view class="header-left">
|
||||
@@ -25,11 +25,8 @@
|
||||
<text class="info-tag">?</text>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="date-picker-inline">
|
||||
<text class="date-text">2026/01/05 - 2026/02/03</text>
|
||||
</view>
|
||||
<button class="btn-query">查询</button>
|
||||
<button class="btn-export">导出</button>
|
||||
<button class="btn-query" @click="loadData">刷新数据</button>
|
||||
<button class="btn-export">导出报表</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -41,10 +38,10 @@
|
||||
<view class="icon-box blue"><text class="icon">🕒</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">营业额</text>
|
||||
<text class="item-value">{{ stats.revenue }}</text>
|
||||
<text class="item-value">¥{{ stats.revenue }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value up">44275370% ▲</text>
|
||||
<text class="trend-label">昨日对比:</text>
|
||||
<text class="trend-value">{{ stats.revenueTrend }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -52,10 +49,10 @@
|
||||
<view class="icon-box green"><text class="icon">¥</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">商品支付金额</text>
|
||||
<text class="item-value">{{ stats.payAmount }}</text>
|
||||
<text class="item-value">¥{{ stats.payAmount }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value up">43469352% ▲</text>
|
||||
<text class="trend-label">交易笔数:</text>
|
||||
<text class="trend-value">{{ stats.orderCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -63,10 +60,10 @@
|
||||
<view class="icon-box orange"><text class="icon">🔒</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">购买会员金额</text>
|
||||
<text class="item-value">{{ stats.memberAmount }}</text>
|
||||
<text class="item-value">¥{{ stats.memberAmount }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value up">805918% ▲</text>
|
||||
<text class="trend-label">占比:</text>
|
||||
<text class="trend-value">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -74,10 +71,10 @@
|
||||
<view class="icon-box purple"><text class="icon">💰</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">充值金额</text>
|
||||
<text class="item-value">{{ stats.rechargeAmount }}</text>
|
||||
<text class="item-value">¥{{ stats.rechargeAmount }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value">0% -</text>
|
||||
<text class="trend-label">笔数:</text>
|
||||
<text class="trend-value">{{ stats.rechargeCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -85,10 +82,10 @@
|
||||
<view class="icon-box cyan"><text class="icon">🛒</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">线下收银金额</text>
|
||||
<text class="item-value">{{ stats.offlineAmount }}</text>
|
||||
<text class="item-value">¥{{ stats.offlineAmount }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value up">100% ▲</text>
|
||||
<text class="trend-label">占比:</text>
|
||||
<text class="trend-value">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -99,33 +96,33 @@
|
||||
<view class="overview-item">
|
||||
<view class="icon-box light-green"><text class="icon">↘</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">支出金额</text>
|
||||
<text class="item-value">{{ stats.expenditure }}</text>
|
||||
<text class="item-label">支出金额 (提现)</text>
|
||||
<text class="item-value">¥{{ stats.expenditure }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value up">44275269% ▲</text>
|
||||
<text class="trend-label">笔数:</text>
|
||||
<text class="trend-value">{{ stats.extractCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<view class="icon-box gold"><text class="icon">💳</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">余额支付金额</text>
|
||||
<text class="item-value">{{ stats.balancePay }}</text>
|
||||
<text class="item-label">全站余额存量</text>
|
||||
<text class="item-value">¥{{ stats.balancePay }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value up">5293.00% ▲</text>
|
||||
<text class="trend-label">用户总数:</text>
|
||||
<text class="trend-value">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="overview-item">
|
||||
<view class="icon-box red-purple"><text class="icon">%</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">支付佣金金额</text>
|
||||
<text class="item-value">{{ stats.commissionPay }}</text>
|
||||
<text class="item-label">佣金总存量</text>
|
||||
<text class="item-value">¥{{ stats.commissionPay }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value">0% -</text>
|
||||
<text class="trend-label">待结算:</text>
|
||||
<text class="trend-value">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -133,147 +130,22 @@
|
||||
<view class="icon-box blue-gray"><text class="icon">📦</text></view>
|
||||
<view class="item-info">
|
||||
<text class="item-label">商品退款金额</text>
|
||||
<text class="item-value">{{ stats.refundAmount }}</text>
|
||||
<text class="item-value">¥{{ stats.refundAmount }}</text>
|
||||
<view class="trend-row">
|
||||
<text class="trend-label">环比增长:</text>
|
||||
<text class="trend-value">0% -</text>
|
||||
<text class="trend-label">退款率:</text>
|
||||
<text class="trend-value">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 占位使其对齐 -->
|
||||
<view class="overview-item transparent"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 概况图表区 -->
|
||||
<view class="overview-chart-section">
|
||||
<view class="overview-chart-legend">
|
||||
<view class="legend-dot blue"><text class="legend-text">营业额</text></view>
|
||||
<view class="legend-dot green"><text class="legend-text">商品支付金额</text></view>
|
||||
<view class="legend-dot gray-blue"><text class="legend-text">购买会员金额</text></view>
|
||||
<view class="legend-dot red"><text class="legend-text">充值金额</text></view>
|
||||
<view class="legend-dot orange"><text class="legend-text">支出金额</text></view>
|
||||
</view>
|
||||
<view class="overview-chart-box">
|
||||
<EChartsView v-if="overviewTrendOption != null" :option="overviewTrendOption" class="main-trend-chart" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 之前的统计卡片区域 -->
|
||||
<view class="stats-section">
|
||||
<!-- 左侧:今日订单金额 -->
|
||||
<view class="stats-card-main">
|
||||
<view class="card-header">
|
||||
<text class="card-title">今日订单金额</text>
|
||||
</view>
|
||||
<view class="card-content">
|
||||
<view class="amount-wrap">
|
||||
<text class="currency">¥</text>
|
||||
<text class="amount-value">0</text>
|
||||
</view>
|
||||
<view class="chart-legend">
|
||||
<view class="legend-item">
|
||||
<view class="dot blue"></view>
|
||||
<text>今天</text>
|
||||
</view>
|
||||
<view class="legend-item">
|
||||
<view class="dot gray"></view>
|
||||
<text>昨天</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chart-box">
|
||||
<EChartsView v-if="orderAmountOption != null" :option="orderAmountOption" class="stats-chart" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 侧边统计网格 -->
|
||||
<view class="stats-side-grid">
|
||||
<view class="side-column">
|
||||
<!-- 今日订单数 -->
|
||||
<view class="side-stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">今日订单数</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="main-val">0</text>
|
||||
<view class="compare-row">
|
||||
<text class="label">昨日:</text>
|
||||
<text class="val">4</text>
|
||||
</view>
|
||||
<view class="compare-row">
|
||||
<text class="label">日环比:</text>
|
||||
<text class="val down">-100% ▼</text>
|
||||
</view>
|
||||
<view class="mini-chart-placeholder">
|
||||
<view class="blue-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 本月订单数 -->
|
||||
<view class="side-stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">本月订单数</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="main-val">12</text>
|
||||
<view class="compare-row">
|
||||
<text class="label">上月:</text>
|
||||
<text class="val">206</text>
|
||||
</view>
|
||||
<view class="compare-row">
|
||||
<text class="label">月环比:</text>
|
||||
<text class="val down">-94% ▼</text>
|
||||
</view>
|
||||
<view class="mini-chart-placeholder">
|
||||
<view class="blue-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="side-column">
|
||||
<!-- 今日支付人数 -->
|
||||
<view class="side-stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">今日支付人数</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="main-val">0</text>
|
||||
<view class="compare-row">
|
||||
<text class="label">昨日:</text>
|
||||
<text class="val">4</text>
|
||||
</view>
|
||||
<view class="compare-row">
|
||||
<text class="label">日环比:</text>
|
||||
<text class="val down">-100% ▼</text>
|
||||
</view>
|
||||
<view class="mini-chart-placeholder">
|
||||
<view class="blue-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 本月支付人数 -->
|
||||
<view class="side-stat-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">本月支付人数</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="main-val">7</text>
|
||||
<view class="compare-row">
|
||||
<text class="label">上月:</text>
|
||||
<text class="val">134</text>
|
||||
</view>
|
||||
<view class="compare-row">
|
||||
<text class="label">月环比:</text>
|
||||
<text class="val down">-94% ▼</text>
|
||||
</view>
|
||||
<view class="mini-chart-placeholder">
|
||||
<view class="blue-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="chart-loading"><text>统计加载中...</text></view>
|
||||
<EChartsView v-else-if="overviewTrendOption != null" :option="overviewTrendOption" class="main-trend-chart" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -281,142 +153,129 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { fetchFinanceOverview } from '@/services/admin/financeService.uts'
|
||||
import { rpcOrNull } from '@/services/analytics/rpc.uts'
|
||||
import { fetchFinanceOverview, fetchFinanceBillSummary } from '@/services/admin/financeService.uts'
|
||||
import { fetchOrderStats } from '@/services/orderService.uts'
|
||||
|
||||
const dateOptions = ['今天', '昨天', '最近7天', '最近30天', '本月', '本年']
|
||||
const dateOptions = ['最近30天', '最近7天', '本月', '本年']
|
||||
const activeDateTab = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
// 响应式统计数据
|
||||
const stats = reactive({
|
||||
revenue: '0.00', // 营业额
|
||||
payAmount: '0.00', // 商品支付金额
|
||||
memberAmount: '0.00', // 购买会员金额
|
||||
rechargeAmount: '0.00', // 充值金额
|
||||
offlineAmount: '0.00', // 线下收银金额
|
||||
expenditure: '0.00', // 支出金额
|
||||
balancePay: '0.00', // 余额支付金额
|
||||
commissionPay: '0.00', // 支付佣金金额
|
||||
refundAmount: '0.00', // 商品退款金额
|
||||
|
||||
// 环比数据 (示例暂留)
|
||||
revenueTrend: '0%',
|
||||
rechargeTrend: '0%'
|
||||
revenue: '0.00',
|
||||
payAmount: '0.00',
|
||||
orderCount: '0',
|
||||
memberAmount: '0.00',
|
||||
rechargeAmount: '0.00',
|
||||
rechargeCount: '0',
|
||||
offlineAmount: '0.00',
|
||||
expenditure: '0.00',
|
||||
extractCount: '0',
|
||||
balancePay: '0.00',
|
||||
commissionPay: '0.00',
|
||||
refundAmount: '0.00',
|
||||
revenueTrend: '-'
|
||||
})
|
||||
|
||||
const orderAmountOption = ref<any>(null)
|
||||
const overviewTrendOption = ref<any>(null)
|
||||
|
||||
/**
|
||||
* 加载统计数据
|
||||
*/
|
||||
const displayDateRange = computed(() : string => {
|
||||
const now = new Date()
|
||||
const start = getStartTime()
|
||||
return `${start.substring(0, 10)} - ${now.toISOString().substring(0, 10)}`
|
||||
})
|
||||
|
||||
function getStartTime() : string {
|
||||
const now = Date.now()
|
||||
let days = 30
|
||||
if (activeDateTab.value == 1) days = 7
|
||||
if (activeDateTab.value == 2) days = 30 // 简化处理
|
||||
if (activeDateTab.value == 3) days = 365
|
||||
return new Date(now - days * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
// TODO: 根据 activeDateTab 计算具体的 startTime 和 endTime
|
||||
const startTime = '2026-01-01T00:00:00Z'
|
||||
const endTime = '2026-12-31T23:59:59Z'
|
||||
loading.value = true
|
||||
const endTime = new Date().toISOString()
|
||||
const startTime = getStartTime()
|
||||
|
||||
try {
|
||||
// 1. 获取财务概况 (充值、提现等)
|
||||
const financeRes = await fetchFinanceOverview(startTime, endTime)
|
||||
if (financeRes != null) {
|
||||
stats.rechargeAmount = financeRes.recharge_amount.toFixed(2)
|
||||
// 支出金额暂以提现成功金额为例
|
||||
stats.expenditure = financeRes.extract_amount.toFixed(2)
|
||||
// 1. 获取财务概况
|
||||
const finRes = await fetchFinanceOverview(startTime, endTime)
|
||||
if (finRes != null) {
|
||||
stats.rechargeAmount = finRes.recharge_amount.toFixed(2)
|
||||
stats.rechargeCount = String(finRes.recharge_count)
|
||||
stats.expenditure = finRes.extract_amount.toFixed(2)
|
||||
stats.extractCount = String(finRes.extract_count)
|
||||
stats.balancePay = finRes.total_user_balance.toFixed(2)
|
||||
stats.commissionPay = finRes.total_user_brokerage.toFixed(2)
|
||||
}
|
||||
|
||||
// 2. 获取订单统计 (营业额、退款等)
|
||||
const orderRes = await rpcOrNull('rpc_admin_order_stats', {
|
||||
p_start_time: startTime,
|
||||
p_end_time: endTime
|
||||
} as UTSJSONObject)
|
||||
|
||||
// 2. 获取订单统计
|
||||
const orderRes = await fetchOrderStats(startTime, endTime)
|
||||
if (orderRes != null) {
|
||||
stats.revenue = ((orderRes as any).total_amount ?? 0).toFixed(2)
|
||||
stats.payAmount = stats.revenue // 简单处理
|
||||
stats.refundAmount = ((orderRes as any).refund_amount ?? 0).toFixed(2)
|
||||
stats.revenue = orderRes.total_amount.toFixed(2)
|
||||
stats.payAmount = orderRes.total_amount.toFixed(2)
|
||||
stats.orderCount = String(orderRes.order_count)
|
||||
stats.refundAmount = orderRes.refund_amount.toFixed(2)
|
||||
}
|
||||
|
||||
// 3. 获取趋势数据驱动图表
|
||||
const trendRes = await fetchFinanceBillSummary(startTime, endTime, 'day')
|
||||
updateChart(trendRes)
|
||||
} catch (e) {
|
||||
console.error('Failed to load transaction stats:', e)
|
||||
uni.showToast({ title: '加载统计失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDateTabChange(index : number) {
|
||||
activeDateTab.value = index
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
// 延迟初始化图表确保容器就位
|
||||
setTimeout(() => {
|
||||
initCharts()
|
||||
}, 300)
|
||||
})
|
||||
|
||||
function initCharts() {
|
||||
// 模拟趋势数据
|
||||
const todayData = [120, 132, 101, 134, 90, 230, 210]
|
||||
const yesterdayData = [220, 182, 191, 234, 290, 330, 310]
|
||||
function toPlainObject(obj: any): any {
|
||||
if (obj == null) return null
|
||||
if (typeof obj !== 'object') return obj
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item: any) : any => toPlainObject(item))
|
||||
}
|
||||
const plain: any = {}
|
||||
const keys = Object.keys(obj)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
const value = obj[key]
|
||||
if (typeof value === 'function' || key.startsWith('_') || key === 'toJSON') {
|
||||
continue
|
||||
}
|
||||
if (value != null && typeof value === 'object') {
|
||||
plain[key] = toPlainObject(value)
|
||||
} else {
|
||||
plain[key] = value
|
||||
}
|
||||
}
|
||||
return plain
|
||||
}
|
||||
|
||||
function updateChart(data : any[]) {
|
||||
const dates = data.map(item => item.date_group.substring(5))
|
||||
const incomes = data.map(item => item.income)
|
||||
const expenses = data.map(item => item.expense)
|
||||
|
||||
const option = {
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', top: '5%', containLabel: true },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
lineStyle: { color: '#1890ff', type: 'dashed' }
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
|
||||
axisLine: { lineStyle: { color: '#f0f0f0' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '今天',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: todayData,
|
||||
lineStyle: { color: '#1890ff', width: 2 },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(24, 144, 255, 0.2)' },
|
||||
{ offset: 1, color: 'rgba(24, 144, 255, 0)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
itemStyle: { color: '#1890ff' }
|
||||
},
|
||||
{
|
||||
name: '昨天',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: yesterdayData,
|
||||
lineStyle: { color: '#d9d9d9', width: 2, type: 'dashed' },
|
||||
itemStyle: { color: '#d9d9d9' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 核心修复:传递给 ECharts 的 Option 必须是 Plain Object
|
||||
orderAmountOption.value = toPlainObject(option)
|
||||
|
||||
// 初始化交易概况趋势图 (多曲线)
|
||||
const overviewOption = {
|
||||
grid: { left: '3%', right: '4%', bottom: '10%', top: '5%', containLabel: true },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['收入', '支出'], bottom: 0 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['2026-01-05', '01-10', '01-15', '01-20', '01-25', '01-31', '02-03'],
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: '#f0f0f0' } },
|
||||
axisLabel: { color: '#999', fontSize: 10 }
|
||||
},
|
||||
@@ -428,29 +287,24 @@ function initCharts() {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '营业额',
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1000, 5000, 20000, 15000, 80000, 20000, 5000],
|
||||
itemStyle: { color: '#1890ff' }
|
||||
data: incomes,
|
||||
itemStyle: { color: '#1890ff' },
|
||||
areaStyle: { color: 'rgba(24, 144, 255, 0.1)' }
|
||||
},
|
||||
{
|
||||
name: '商品支付金额',
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [800, 4000, 18000, 12000, 75000, 18000, 4000],
|
||||
itemStyle: { color: '#52c41a' }
|
||||
},
|
||||
{
|
||||
name: '支出金额',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [500, 3000, 15000, 10000, 90000, 15000, 3000],
|
||||
itemStyle: { color: '#fa8c16' }
|
||||
data: expenses,
|
||||
itemStyle: { color: '#fa8c16' },
|
||||
areaStyle: { color: 'rgba(250, 140, 22, 0.1)' }
|
||||
}
|
||||
]
|
||||
}
|
||||
overviewTrendOption.value = toPlainObject(overviewOption)
|
||||
overviewTrendOption.value = toPlainObject(option)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -461,7 +315,6 @@ function initCharts() {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 头部筛选 */
|
||||
.header-filters {
|
||||
background: #fff;
|
||||
padding: 12px 20px;
|
||||
@@ -487,15 +340,8 @@ function initCharts() {
|
||||
border-right: 1px solid #d9d9d9;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
&:last-child { border-right: none; }
|
||||
&.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
|
||||
}
|
||||
|
||||
.date-picker-wrap {
|
||||
@@ -503,167 +349,17 @@ function initCharts() {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.calendar-icon { margin-right: 8px; font-size: 14px; }
|
||||
.date-range-text { font-size: 14px; color: #333; }
|
||||
|
||||
.date-range-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 统计区域布局 */
|
||||
.stats-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stats-card-main {
|
||||
flex: 3;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.amount-wrap {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.currency {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 40px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
|
||||
&.blue { background-color: #1890ff; }
|
||||
&.gray { background-color: #d9d9d9; }
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.stats-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 侧边网格 */
|
||||
.stats-side-grid {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.side-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.side-stat-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-val {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.compare-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.compare-row .label {
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.compare-row .val {
|
||||
color: #333;
|
||||
|
||||
&.down { color: #52c41a; }
|
||||
&.up { color: #f5222d; }
|
||||
}
|
||||
|
||||
.mini-chart-placeholder {
|
||||
margin-top: 20px;
|
||||
height: 2px;
|
||||
background-color: #f0f0f0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blue-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 交易概况复刻样式 */
|
||||
.overview-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.overview-header {
|
||||
@@ -674,72 +370,22 @@ function initCharts() {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.overview-header .header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-left { display: flex; flex-direction: row; align-items: center; }
|
||||
.section-title { font-size: 16px; font-weight: bold; color: #333; }
|
||||
.info-tag {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #eee;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.overview-header .header-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.date-picker-inline {
|
||||
border: 1px solid #dcdfe6;
|
||||
padding: 5px 15px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
background: #eee; color: #999; font-size: 11px;
|
||||
display: flex; align-items: center; justify-content: center; margin-left: 8px;
|
||||
}
|
||||
|
||||
.header-right { display: flex; flex-direction: row; gap: 12px; }
|
||||
.btn-query, .btn-export {
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 0 16px; height: 32px; line-height: 32px;
|
||||
font-size: 13px; border-radius: 4px; cursor: pointer;
|
||||
}
|
||||
.btn-query { background-color: #1890ff; color: #fff; border: none; }
|
||||
.btn-export { background: #fff; color: #1890ff; border: 1px solid #1890ff; }
|
||||
|
||||
.btn-query {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
background: #fff;
|
||||
color: #1890ff;
|
||||
border: 1px solid #1890ff;
|
||||
}
|
||||
|
||||
/* 指标网格 */
|
||||
.overview-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -748,36 +394,14 @@ function initCharts() {
|
||||
border-bottom: 1px dashed #f0f0f0;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.overview-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.overview-item.transparent {
|
||||
visibility: hidden;
|
||||
}
|
||||
.grid-row { display: flex; flex-direction: row; justify-content: space-between; }
|
||||
.overview-item { flex: 1; display: flex; flex-direction: row; align-items: center; }
|
||||
.overview-item.transparent { visibility: hidden; }
|
||||
|
||||
.icon-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.icon-box .icon {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
width: 44px; height: 44px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center; margin-right: 12px;
|
||||
text { color: #fff; font-size: 20px; }
|
||||
}
|
||||
|
||||
.icon-box.blue { background-color: #2f54eb; }
|
||||
@@ -790,74 +414,14 @@ function initCharts() {
|
||||
.icon-box.red-purple { background-color: #eb2f96; }
|
||||
.icon-box.blue-gray { background-color: #4096ff; }
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.item-info { display: flex; flex-direction: column; }
|
||||
.item-label { font-size: 13px; color: #999; margin-bottom: 4px; }
|
||||
.item-value { font-size: 22px; font-weight: bold; color: #333; margin-bottom: 4px; }
|
||||
.trend-row { display: flex; flex-direction: row; font-size: 12px; color: #999; }
|
||||
.trend-value { margin-left: 4px; }
|
||||
|
||||
.item-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trend-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.trend-value.up {
|
||||
color: #f5222d;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 图表区 */
|
||||
.overview-chart-section {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.overview-chart-legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-dot.blue { background: #1890ff; }
|
||||
.legend-dot.green { background: #52c41a; }
|
||||
.legend-dot.gray-blue { background: #607d8b; }
|
||||
.legend-dot.red { background: #f44336; }
|
||||
.legend-dot.orange { background: #fa8c16; }
|
||||
|
||||
.legend-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.overview-chart-box {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.main-trend-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.overview-chart-section { padding-top: 24px; }
|
||||
.overview-chart-box { width: 100%; height: 350px; }
|
||||
.main-trend-chart { width: 100%; height: 100%; }
|
||||
.chart-loading { height: 100%; display: flex; align-items: center; justify-content: center; color: #999; }
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- 统计指标网格 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card" v-for="(item, index) in stats" :key="index">
|
||||
<view class="stat-card" v-for="(item, index) in statsItems" :key="index">
|
||||
<view class="icon-circle" :class="item.colorClass">
|
||||
<text class="stat-icon">{{ item.icon }}</text>
|
||||
</view>
|
||||
@@ -43,73 +43,98 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据表格 (使用 Flex 模拟以确保兼容性和精度) -->
|
||||
<!-- 数据表格 -->
|
||||
<view class="table-container">
|
||||
<view class="table-header">
|
||||
<view class="th col-id"><text class="th-txt">ID</text></view>
|
||||
<view class="th col-user"><text class="th-txt">用户信息</text></view>
|
||||
<view class="th col-amount"><text class="th-txt">提现金额</text></view>
|
||||
<view class="th col-fee"><text class="th-txt">提现手续费</text></view>
|
||||
<view class="th col-fee"><text class="th-txt">手续费</text></view>
|
||||
<view class="th col-net"><text class="th-txt">到账金额</text></view>
|
||||
<view class="th col-method"><text class="th-txt">提现方式</text></view>
|
||||
<view class="th col-qr"><text class="th-txt">收款码</text></view>
|
||||
<view class="th col-time"><text class="th-txt">申请时间</text></view>
|
||||
<view class="th col-remark"><text class="th-txt">备注</text></view>
|
||||
<view class="th col-status"><text class="th-txt">审核状态</text></view>
|
||||
<view class="th col-ops"><text class="th-txt">操作</text></view>
|
||||
</view>
|
||||
|
||||
<view class="table-body">
|
||||
<view class="table-row" v-for="(item, index) in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ item.id }}</text></view>
|
||||
<view v-if="loading" class="loading-state" style="padding: 40px; text-align: center;">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="tableData.length === 0" class="empty-state" style="padding: 40px; text-align: center;">
|
||||
<text>暂无提现申请</text>
|
||||
</view>
|
||||
<view v-else class="table-row" v-for="(item, index) in tableData" :key="item.id">
|
||||
<view class="td col-id"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
|
||||
<view class="td col-user">
|
||||
<view class="user-info-box">
|
||||
<view class="avatar-box">
|
||||
<text class="avatar-placeholder">U</text>
|
||||
</view>
|
||||
<view class="user-detail">
|
||||
<text class="user-nickname">{{ item.nickname }}</text>
|
||||
<text class="user-id">用户id:{{ item.userId }}</text>
|
||||
<text class="user-nickname">{{ item.user_name || '未知' }}</text>
|
||||
<text class="user-id">UID:{{ item.uid.substring(0, 8) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-amount"><text class="td-txt">{{ item.amount }}</text></view>
|
||||
<view class="td col-fee"><text class="td-txt">{{ item.fee }}</text></view>
|
||||
<view class="td col-net"><text class="td-txt green-txt">{{ item.netAmount }}</text></view>
|
||||
<view class="td col-amount"><text class="td-txt">¥{{ item.extract_price.toFixed(2) }}</text></view>
|
||||
<view class="td col-fee"><text class="td-txt">¥{{ item.service_fee.toFixed(2) }}</text></view>
|
||||
<view class="td col-net"><text class="td-txt green-txt">¥{{ (item.extract_price - item.service_fee).toFixed(2) }}</text></view>
|
||||
<view class="td col-method">
|
||||
<view class="method-box">
|
||||
<text class="m-line">姓名:{{ item.name }}</text>
|
||||
<text class="m-line">{{ item.type }}:{{ item.account }}</text>
|
||||
<text v-if="item.bank" class="m-line">银行开户地址:{{ item.bank }}</text>
|
||||
<text class="m-line">{{ getMethodText(item.extract_type) }}</text>
|
||||
<text class="m-line">账号:{{ item.alipay_code || item.wechat_code || item.bank_code || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="td col-qr">
|
||||
<view class="qr-box" v-if="item.id === 57">
|
||||
<text class="qr-icon-txt">■</text>
|
||||
</view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.created_at.substring(0, 16).replace('T', ' ') }}</text></view>
|
||||
<view class="td col-status">
|
||||
<text class="td-txt" :class="{ 'red-txt': item.status == -1, 'green-txt': item.status == 1 }">
|
||||
{{ getStatusText(item.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="td col-time"><text class="td-txt">{{ item.time }}</text></view>
|
||||
<view class="td col-remark"><text class="td-txt">{{ item.remark || '' }}</text></view>
|
||||
<view class="td col-status"><text class="td-txt">申请中</text></view>
|
||||
<view class="td col-ops">
|
||||
<view class="ops-box">
|
||||
<text class="op-btn blue">编辑</text>
|
||||
<view class="ops-box" v-if="item.status == 0">
|
||||
<text class="op-btn blue" @click="handleReview(item.id, 1)">通过</text>
|
||||
<text class="op-sep">|</text>
|
||||
<text class="op-btn blue">通过</text>
|
||||
<text class="op-sep">|</text>
|
||||
<text class="op-btn blue">驳回</text>
|
||||
<text class="op-btn blue" @click="handleReview(item.id, -1)">驳回</text>
|
||||
</view>
|
||||
<text v-else class="td-txt">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<view class="pagination-footer" style="padding: 24px 0; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px;">
|
||||
<view class="page-total">
|
||||
<text class="total-txt">共 {{ total }} 条</text>
|
||||
</view>
|
||||
<view class="page-btns" style="display: flex; flex-direction: row; gap: 8px;">
|
||||
<view class="p-btn" :class="{ disabled: page <= 1 }" @click="prevPage" style="width: 32px; height: 32px; border: 1px solid #dcdfe6; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||
<text><</text>
|
||||
</view>
|
||||
<view class="p-btn active" style="width: 32px; height: 32px; background-color: #2d8cf0; color: #fff; display: flex; align-items: center; justify-content: center;">
|
||||
<text>{{ page }}</text>
|
||||
</view>
|
||||
<view class="p-btn" :class="{ disabled: page >= totalPages }" @click="nextPage" style="width: 32px; height: 32px; border: 1px solid #dcdfe6; display: flex; align-items: center; justify-content: center; cursor: pointer;">
|
||||
<text>></text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-jump" style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
|
||||
<text class="jump-txt">前往</text>
|
||||
<input class="jump-input" v-model="jumpPage" type="number" @confirm="goToJumpPage" style="width: 40px; height: 32px; border: 1px solid #dcdfe6; text-align: center;" />
|
||||
<text class="jump-txt">页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { fetchExtractList, fetchFinanceOverview, reviewExtract } from '@/services/admin/financeService.uts'
|
||||
import { ExtractRecord } from '@/types/admin/finance.uts'
|
||||
|
||||
const timeRange = ref([])
|
||||
const timeRange = ref<string[]>([])
|
||||
const statusValue = ref('all')
|
||||
const methodValue = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
@@ -128,45 +153,101 @@ const methodOptions = ref([
|
||||
{ value: 'weixin', text: '微信' }
|
||||
])
|
||||
|
||||
const stats = ref([
|
||||
{ label: '佣金总金额', value: '676809.25', icon: '$', colorClass: 'blue' },
|
||||
{ label: '待提现金额', value: '71', icon: '¥', colorClass: 'orange' },
|
||||
{ label: '已提现金额', value: '68879.25', icon: '$', colorClass: 'green' },
|
||||
{ label: '未提现金额', value: '607930.00', icon: '¥', colorClass: 'pink' }
|
||||
const statsItems = ref([
|
||||
{ label: '佣金总金额', value: '0.00', icon: '¥', colorClass: 'blue' },
|
||||
{ label: '待提现金额', value: '0.00', icon: '¥', colorClass: 'orange' },
|
||||
{ label: '已提现金额', value: '0.00', icon: '¥', colorClass: 'green' },
|
||||
{ label: '未提现金额', value: '0.00', icon: '¥', colorClass: 'pink' }
|
||||
])
|
||||
|
||||
const tableData = ref([
|
||||
{
|
||||
id: 57,
|
||||
nickname: '用户昵称: 177****766',
|
||||
userId: '58837',
|
||||
amount: '20.00',
|
||||
fee: '0.00',
|
||||
netAmount: '20.00',
|
||||
name: '接口',
|
||||
type: '支付宝',
|
||||
account: '1195953899',
|
||||
time: '2025-10-24 16:04',
|
||||
remark: ''
|
||||
},
|
||||
{
|
||||
id: 56,
|
||||
nickname: '用户昵称: 测试员的',
|
||||
userId: '20695',
|
||||
amount: '1.00',
|
||||
fee: '0.00',
|
||||
netAmount: '1.00',
|
||||
name: '123',
|
||||
type: '银行卡',
|
||||
account: '4001231221',
|
||||
bank: '中国银行',
|
||||
time: '2025-07-04 15:11',
|
||||
remark: ''
|
||||
const tableData = ref<ExtractRecord[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadStats() {
|
||||
const endTime = new Date().toISOString()
|
||||
const startTime = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString()
|
||||
const res = await fetchFinanceOverview(startTime, endTime)
|
||||
if (res != null) {
|
||||
statsItems.value[0].value = res.total_user_brokerage.toFixed(2)
|
||||
statsItems.value[1].value = res.extract_amount.toFixed(2) // 示例逻辑,根据实际汇总调整
|
||||
statsItems.value[2].value = res.extract_amount.toFixed(2)
|
||||
statsItems.value[3].value = (res.total_user_brokerage - res.extract_amount).toFixed(2)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const status = statusValue.value == 'all' ? null : parseInt(statusValue.value)
|
||||
let startTime : string | null = null
|
||||
let endTime : string | null = null
|
||||
if (timeRange.value.length == 2) {
|
||||
startTime = timeRange.value[0]
|
||||
endTime = timeRange.value[1]
|
||||
}
|
||||
|
||||
const res = await fetchExtractList(
|
||||
page.value,
|
||||
pageSize.value,
|
||||
status,
|
||||
startTime,
|
||||
endTime,
|
||||
searchKeyword.value || null
|
||||
)
|
||||
tableData.value = res.items
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
loadList()
|
||||
})
|
||||
|
||||
const handleQuery = () => {
|
||||
console.log('Query with:', statusValue.value, methodValue.value, searchKeyword.value)
|
||||
page.value = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
async function handleReview(id : string, status : number) {
|
||||
const action = status == 1 ? '通过' : '驳回'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确认要${action}该提现申请吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await reviewExtract(id, status)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '操作成功' })
|
||||
loadList()
|
||||
loadStats()
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getStatusText(status : number) : string {
|
||||
if (status == 0) return '待审核'
|
||||
if (status == 1) return '已通过'
|
||||
if (status == -1) return '已驳回'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
function getMethodText(type : string) : string {
|
||||
if (type == 'alipay') return '支付宝'
|
||||
if (type == 'bank') return '银行卡'
|
||||
if (type == 'wechat') return '微信'
|
||||
return type
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<view class="search-row">
|
||||
<view class="search-item">
|
||||
<text class="label">商品搜索:</text>
|
||||
<input class="mock-input" placeholder="请输入商品名称/关键字/ID" />
|
||||
<input class="mock-input" placeholder="请输入商品名称/关键字/ID" v-model="searchName" @confirm="handleSearch" />
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="label">商品类型:</text>
|
||||
@@ -16,14 +16,19 @@
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="label">商品分类:</text>
|
||||
<view class="mock-select">
|
||||
<text>请选择</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
<picker :value="categoryIndex" :range="categoryOptions" range-key="label" @change="e => {
|
||||
categoryIndex = e.detail.value;
|
||||
selectedCategoryId = categoryOptions[categoryIndex].value;
|
||||
}">
|
||||
<view class="mock-select">
|
||||
<text>{{ categoryOptions[categoryIndex].label }}</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="search-btns">
|
||||
<button class="btn-primary">查询</button>
|
||||
<button class="btn-reset">重置</button>
|
||||
<button class="btn-primary" @click="handleSearch">查询</button>
|
||||
<button class="btn-reset" @click="handleReset">重置</button>
|
||||
<view class="expand-control">
|
||||
<text class="expand-txt">展开</text>
|
||||
<text class="expand-arrow">▼</text>
|
||||
@@ -49,7 +54,7 @@
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: activeStatus === tab.key }"
|
||||
@click="activeStatus = tab.key"
|
||||
@click="changeStatus(tab.key)"
|
||||
>
|
||||
<text>{{ tab.label }}({{ tab.count }})</text>
|
||||
</view>
|
||||
@@ -115,7 +120,7 @@
|
||||
<view class="td col-stock"><text>{{ item.stock }}</text></view>
|
||||
<view class="td col-sort"><text>{{ item.sort }}</text></view>
|
||||
<view class="td col-status">
|
||||
<view class="mock-switch" :class="{ on: item.status === 1 }">
|
||||
<view class="mock-switch" :class="{ on: item.status === 1 }" @click="toggleStatus(item)">
|
||||
<text class="switch-txt">{{ item.status === 1 ? '上架' : '下架' }}</text>
|
||||
<view class="switch-dot"></view>
|
||||
</view>
|
||||
@@ -137,7 +142,7 @@
|
||||
<text class="menu-item" @click.stop="goReviews(item.id)">查看评论</text>
|
||||
<text class="menu-item" @click.stop="goMemberPrice(item.id)">会员价管理</text>
|
||||
<text class="menu-item" @click.stop="uni.showToast({title:'佣金管理开发中', icon:'none'})">佣金管理</text>
|
||||
<text class="menu-item danger-item" @click.stop="moveToRecycle(item.id)">{{ activeStatus === 'recycle' ? '恢复商品' : '移到回收站' }}</text>
|
||||
<text class="menu-item danger-item" @click.stop="moveToRecycle(item)">{{ activeStatus === 4 ? '恢复商品' : '移到回收站' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -149,10 +154,9 @@
|
||||
<view class="pagination-row">
|
||||
<text class="total">共 {{ total }} 条</text>
|
||||
<view class="page-ctrl">
|
||||
<text class="page-btn disabled">{"<"}</text>
|
||||
<text class="page-num active">1</text>
|
||||
<text class="page-num">2</text>
|
||||
<text class="page-btn">{">"}</text>
|
||||
<text class="page-btn" :class="{ disabled: page <= 1 }" @click="page > 1 && (page--, loadData())">{"<"}</text>
|
||||
<text class="page-num active">{{ page }}</text>
|
||||
<text class="page-btn" :class="{ disabled: productList.length < pageSize }" @click="productList.length == pageSize && (page++, loadData())">{">"}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -162,15 +166,21 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
import { fetchAdminProductPage, updateAdminProductStatus, type AdminProduct } from '@/services/admin/productService.uts'
|
||||
import { fetchAdminProductPage, updateAdminProductStatus, fetchAdminProductCountStats, type AdminProduct } from '@/services/admin/productService.uts'
|
||||
import { fetchAdminCategoryList, type AdminCategory } from '@/services/admin/productCategoryService.uts'
|
||||
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const activeStatus = ref<number | null>(1) // 1:出售中
|
||||
const activeStatus = ref<number>(1) // 1:出售中
|
||||
const activeDropdownId = ref<string | null>(null)
|
||||
const productList = ref<Array<AdminProduct>>([])
|
||||
const searchName = ref('')
|
||||
const selectedCategoryId = ref<string | null>(null)
|
||||
const categoryOptions = ref<Array<{label: string, value: string | null}>>([
|
||||
{ label: '全部', value: null }
|
||||
])
|
||||
const categoryIndex = ref(0)
|
||||
|
||||
const statusTabs = ref([
|
||||
{ key: 1, label: '出售中的商品', count: 0 },
|
||||
@@ -180,13 +190,40 @@ const statusTabs = ref([
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
loadCounts()
|
||||
loadCategories()
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const categories = await fetchAdminCategoryList({ isActive: true })
|
||||
categories.forEach(item => {
|
||||
categoryOptions.value.push({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('加载分类失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCounts() {
|
||||
const stats = await fetchAdminProductCountStats()
|
||||
if (stats != null) {
|
||||
statusTabs.value[0].count = parseInt(String(stats['selling'] ?? '0'))
|
||||
statusTabs.value[1].count = parseInt(String(stats['warehouse'] ?? '0'))
|
||||
statusTabs.value[2].count = parseInt(String(stats['draft'] ?? '0'))
|
||||
statusTabs.value[3].count = parseInt(String(stats['recycle'] ?? '0'))
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const res = await fetchAdminProductPage(page.value, pageSize.value, {
|
||||
name: searchName.value,
|
||||
status: activeStatus.value ?? undefined
|
||||
status: activeStatus.value,
|
||||
categoryId: selectedCategoryId.value ?? undefined
|
||||
})
|
||||
productList.value = res.items
|
||||
total.value = res.total
|
||||
@@ -195,43 +232,15 @@ async function loadData() {
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
loadData()
|
||||
loadCounts()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchName.value = ''
|
||||
selectedCategoryId.value = null
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function toggleStatus(item: AdminProduct) {
|
||||
const newStatus = item.status === 1 ? 2 : 1
|
||||
const ok = await updateAdminProductStatus(item.id, newStatus)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
loadData()
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToRecycle(item: AdminProduct) {
|
||||
const isDelete = activeStatus.value !== 4
|
||||
const targetStatus = isDelete ? 4 : 2 // 移到回收站或恢复到下架
|
||||
|
||||
const action = isDelete ? '移到回收站' : '恢复'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确认要将该商品${action}吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
const ok = await updateAdminProductStatus(item.id, targetStatus)
|
||||
if (ok) {
|
||||
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
loadCounts()
|
||||
}
|
||||
|
||||
function changeStatus(key: number) {
|
||||
|
||||
@@ -89,7 +89,13 @@
|
||||
<text class="th col-num">收藏数</text>
|
||||
<text class="th col-num wide">访客-支付转化率(%)</text>
|
||||
</view>
|
||||
<view v-for="(item, index) in rankingList" :key="index" class="table-row">
|
||||
<view v-if="loading" class="table-loading" style="padding: 40px; text-align: center;">
|
||||
<text>加载排行中...</text>
|
||||
</view>
|
||||
<view v-else-if="rankingList.length === 0" class="table-empty" style="padding: 40px; text-align: center;">
|
||||
<text>暂无排行数据</text>
|
||||
</view>
|
||||
<view v-else v-for="(item, index) in rankingList" :key="index" class="table-row">
|
||||
<text class="td col-id">{{ item.id }}</text>
|
||||
<view class="td col-img">
|
||||
<image class="product-img" :src="item.image" mode="aspectFill" />
|
||||
@@ -99,12 +105,12 @@
|
||||
</view>
|
||||
<text class="td col-num">{{ item.views }}</text>
|
||||
<text class="td col-num">{{ item.visitors }}</text>
|
||||
<text class="td col-num">{{ item.cartCount }}</text>
|
||||
<text class="td col-num">{{ item.orderCount }}</text>
|
||||
<text class="td col-num">{{ item.payCount }}</text>
|
||||
<text class="td col-num">{{ item.payAmount }}</text>
|
||||
<text class="td col-num">{{ item.favCount }}</text>
|
||||
<text class="td col-num wide">{{ item.conversion }}%</text>
|
||||
<text class="td col-num">{{ item.cart_count }}</text>
|
||||
<text class="td col-num">{{ item.order_count }}</text>
|
||||
<text class="td col-num">{{ item.pay_count }}</text>
|
||||
<text class="td col-num">{{ item.pay_amount }}</text>
|
||||
<text class="td col-num">{{ item.fav_count }}</text>
|
||||
<text class="td col-num wide">{{ item.visitors > 0 ? (item.pay_count / item.visitors * 100).toFixed(2) : '0.00' }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -114,79 +120,54 @@
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { fetchAdminProductStats, fetchAdminProductTrend, fetchAdminProductRanking } from '@/services/admin/productService.uts'
|
||||
|
||||
const statItems = ref([
|
||||
{ label: '商品浏览量', value: '7576', compare: '0.93%', trend: 'up', trendClass: 'up-red', bgColor: '#e6f7ff', emoji: '👁️' },
|
||||
{ label: '商品访客量', value: '765', compare: '0.79%', trend: 'up', trendClass: 'up-red', bgColor: '#f6ffed', emoji: '👤' },
|
||||
{ label: '支付件数', value: '322', compare: '-49.52%', trend: 'down', trendClass: 'down-green', bgColor: '#fff7e6', emoji: '🛍️' },
|
||||
{ label: '支付金额', value: '443254.62', compare: '-63.62%', trend: 'down', trendClass: 'down-green', bgColor: '#f9f0ff', emoji: '💰' },
|
||||
{ label: '退款件数', value: '0', compare: '0.00%', trend: 'none', trendClass: 'none-gray', bgColor: '#e6f7ff', emoji: '🔄' },
|
||||
{ label: '退款金额', value: '0', compare: '0.00%', trend: 'none', trendClass: 'none-gray', bgColor: '#f6ffed', emoji: '💴' }
|
||||
])
|
||||
|
||||
const rankingList = ref([
|
||||
{
|
||||
id: 963,
|
||||
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
|
||||
views: 1200,
|
||||
visitors: 246,
|
||||
cartCount: 74,
|
||||
orderCount: 214,
|
||||
payCount: 180,
|
||||
payAmount: '11877.49',
|
||||
favCount: 13,
|
||||
conversion: 18
|
||||
},
|
||||
{
|
||||
id: 116,
|
||||
image: 'https://img2.baidu.com/it/u=3775079632,546700868&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: '爱奇艺智能 奇遇LT01 投影仪 家用卧室超高清手机便携投影机 (4K超清 支持侧投 手机同屏 华为一碰即投)',
|
||||
views: 959,
|
||||
visitors: 376,
|
||||
cartCount: 1,
|
||||
orderCount: 60,
|
||||
payCount: 29,
|
||||
payAmount: '26.00',
|
||||
favCount: 6,
|
||||
conversion: 7
|
||||
},
|
||||
{
|
||||
id: 48,
|
||||
image: 'https://img0.baidu.com/it/u=1762118431,3101886131&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
||||
name: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇墨水蓝/传奇墨水蓝/白 XL',
|
||||
views: 758,
|
||||
visitors: 207,
|
||||
cartCount: 63,
|
||||
orderCount: 67,
|
||||
payCount: 17,
|
||||
payAmount: '1409.30',
|
||||
favCount: 4,
|
||||
conversion: 7
|
||||
},
|
||||
{
|
||||
id: 108,
|
||||
image: 'https://img2.baidu.com/it/u=3033501986,2204481084&fm=253&fmt=auto&app=138&f=JPEG?w=569&h=500',
|
||||
name: 'FOMIX 蛋壳椅 进口头层牛皮橙色单人沙发椅Egg chair设计师师单椅单沙头层牛皮/单椅',
|
||||
views: 730,
|
||||
visitors: 216,
|
||||
cartCount: 26999,
|
||||
orderCount: 327,
|
||||
payCount: 14,
|
||||
payAmount: '66197.00',
|
||||
favCount: 4,
|
||||
conversion: 6
|
||||
}
|
||||
{ label: '商品浏览量', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#e6f7ff', emoji: '👁️', key: 'views' },
|
||||
{ label: '商品访客量', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#f6ffed', emoji: '👤', key: 'visitors' },
|
||||
{ label: '支付件数', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#fff7e6', emoji: '🛍️', key: 'pay_count' },
|
||||
{ label: '支付金额', value: '0.00', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#f9f0ff', emoji: '💰', key: 'pay_amount' },
|
||||
{ label: '退款件数', value: '0', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#e6f7ff', emoji: '🔄', key: 'refund_count' },
|
||||
{ label: '退款金额', value: '0.00', compare: '0%', trend: 'none', trendClass: 'none-gray', bgColor: '#f6ffed', emoji: '💴', key: 'refund_amount' }
|
||||
])
|
||||
|
||||
const rankingList = ref<Array<any>>([])
|
||||
const chartOption = ref<any>({})
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initChart()
|
||||
}, 300)
|
||||
loadAllData()
|
||||
})
|
||||
|
||||
async function loadAllData() {
|
||||
loading.value = true
|
||||
const endTime = new Date().toISOString()
|
||||
const startTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
try {
|
||||
// 1. 加载核心指标
|
||||
const stats = await fetchAdminProductStats(startTime, endTime)
|
||||
if (stats != null) {
|
||||
statItems.value.forEach(item => {
|
||||
const val = stats[item.key as string]
|
||||
item.value = typeof val === 'number' ? (item.key.includes('amount') ? val.toFixed(2) : String(val)) : String(val ?? '0')
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 加载趋势图
|
||||
const trendData = await fetchAdminProductTrend(startTime, endTime)
|
||||
initChart(trendData)
|
||||
|
||||
// 3. 加载排行
|
||||
const rankingData = await fetchAdminProductRanking(startTime, endTime, 'sales', 10)
|
||||
rankingList.value = rankingData
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载统计失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toPlainObject(obj: any): any {
|
||||
if (obj == null) return null
|
||||
if (typeof obj !== 'object') return obj
|
||||
@@ -210,29 +191,19 @@ function toPlainObject(obj: any): any {
|
||||
return plain
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
const dates = [
|
||||
'01-04', '01-05', '01-06', '01-07', '01-08', '01-09', '01-10', '01-11', '01-12', '01-13',
|
||||
'01-14', '01-15', '01-16', '01-17', '01-18', '01-19', '01-20', '01-21', '01-22', '01-23',
|
||||
'01-24', '01-25', '01-26', '01-27', '01-28', '01-29', '01-30', '01-31', '02-01', '02-02'
|
||||
]
|
||||
function initChart(data: any[]) {
|
||||
const dates = data.map(item => item.date_group.substring(5))
|
||||
const views = data.map(item => item.views)
|
||||
const visitors = data.map(item => item.visitors)
|
||||
const payAmounts = data.map(item => item.pay_amount)
|
||||
const refundAmounts = data.map(item => item.refund_amount)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.7)',
|
||||
padding: [10, 15],
|
||||
textStyle: { color: '#fff' },
|
||||
formatter: (params: any[]) : string => {
|
||||
let res = `<div style="font-size:12px; color:#ccc; margin-bottom:5px;">${params[0].name}</div>`
|
||||
params.forEach(p => {
|
||||
res += `<div style="display:flex; align-items:center;">
|
||||
<div style="width:8px; height:8px; border-radius:50%; background:${p.color}; margin-right:8px;"></div>
|
||||
<span>${p.seriesName}: ${p.value}</span>
|
||||
</div>`
|
||||
})
|
||||
return res
|
||||
}
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
@@ -252,14 +223,12 @@ function initChart() {
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额',
|
||||
nameTextStyle: { color: '#8c8c8c', padding: [0, 30, 0, 0] },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } },
|
||||
axisLabel: { color: '#8c8c8c' }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
nameTextStyle: { color: '#8c8c8c', padding: [0, 0, 0, 30] },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: '#8c8c8c' }
|
||||
}
|
||||
@@ -272,8 +241,7 @@ function initChart() {
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
itemStyle: { color: '#b37feb' },
|
||||
lineStyle: { width: 2 },
|
||||
data: [90, 110, 115, 100, 95, 80, 60, 40, 70, 85, 75, 65, 70, 80, 100, 120, 110, 90, 60, 95, 115, 110, 85, 50, 45, 55, 75]
|
||||
data: views
|
||||
},
|
||||
{
|
||||
name: '商品访客量',
|
||||
@@ -282,31 +250,23 @@ function initChart() {
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
itemStyle: { color: '#ffbb96' },
|
||||
lineStyle: { width: 2 },
|
||||
data: [15, 12, 10, 8, 11, 14, 13, 8, 9, 11, 10, 15, 12, 11, 9, 12, 14, 15, 11, 10, 13, 15, 11, 8, 12, 10, 14]
|
||||
data: visitors
|
||||
},
|
||||
{
|
||||
name: '支付金额',
|
||||
type: 'bar',
|
||||
barWidth: '25%',
|
||||
itemStyle: { color: '#1890ff' },
|
||||
data: [10, 5, 8, 0, 145, 15, 5, 0, 0, 0, 0, 5, 30, 0, 15, 20, 100, 20, 25, 5, 1, 3, 70, 5, 10, 5, 15, 10]
|
||||
data: payAmounts
|
||||
},
|
||||
{
|
||||
name: '退款金额',
|
||||
type: 'bar',
|
||||
barWidth: '25%',
|
||||
itemStyle: { color: '#52c41a' },
|
||||
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
data: refundAmounts
|
||||
}
|
||||
],
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: ['none', 'none'],
|
||||
label: { show: false },
|
||||
lineStyle: { color: '#bfbfbf', type: 'dashed' },
|
||||
data: [{ yAxis: 145853.16 }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartOption.value = toPlainObject(option)
|
||||
|
||||
@@ -149,3 +149,18 @@ export async function fetchUserBillList(
|
||||
items: (data.items as Array<UserBillRecord>) ?? ([] as Array<UserBillRecord>)
|
||||
} as PagedResult<UserBillRecord>
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账单汇总统计
|
||||
*/
|
||||
export async function fetchFinanceBillSummary(
|
||||
startTime : string,
|
||||
endTime : string,
|
||||
interval : string = 'day'
|
||||
) : Promise<Array<any>> {
|
||||
return (await rpcOrEmptyArray('rpc_admin_finance_bill_summary', {
|
||||
p_start_time: startTime,
|
||||
p_end_time: endTime,
|
||||
p_interval: interval
|
||||
} as any)) as any
|
||||
}
|
||||
|
||||
@@ -50,6 +50,47 @@ export async function fetchAdminProductPage(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品状态汇总统计
|
||||
*/
|
||||
export async function fetchAdminProductCountStats() : Promise<UTSJSONObject | null> {
|
||||
return await rpcOrNull('rpc_admin_product_count_stats', {} as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品概况统计指标
|
||||
*/
|
||||
export async function fetchAdminProductStats(startTime : string, endTime : string) : Promise<UTSJSONObject | null> {
|
||||
return await rpcOrNull('rpc_admin_product_stats', {
|
||||
p_start_time: startTime,
|
||||
p_end_time: endTime
|
||||
} as UTSJSONObject)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品营业趋势数据
|
||||
*/
|
||||
export async function fetchAdminProductTrend(startTime : string, endTime : string) : Promise<Array<UTSJSONObject>> {
|
||||
const res = await rpcOrValue('rpc_admin_product_trend', {
|
||||
p_start_time: startTime,
|
||||
p_end_time: endTime
|
||||
} as UTSJSONObject)
|
||||
return Array.isArray(res) ? (res as Array<UTSJSONObject>) : [] as Array<UTSJSONObject>
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品排行
|
||||
*/
|
||||
export async function fetchAdminProductRanking(startTime : string, endTime : string, sortBy : string = 'sales', limit : number = 10) : Promise<Array<UTSJSONObject>> {
|
||||
const res = await rpcOrValue('rpc_admin_product_ranking', {
|
||||
p_start_time: startTime,
|
||||
p_end_time: endTime,
|
||||
p_sort_by: sortBy,
|
||||
p_limit: limit
|
||||
} as UTSJSONObject)
|
||||
return Array.isArray(res) ? (res as Array<UTSJSONObject>) : [] as Array<UTSJSONObject>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品状态 (上架/下架/回收站)
|
||||
* @param status 1:上架 2:下架 3:草稿 4:删除
|
||||
|
||||
Reference in New Issue
Block a user