feat platform dashboard and organization management
This commit is contained in:
@@ -2,7 +2,6 @@ package dashboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.apinb.com/bsm-sdk/core/infra"
|
"git.apinb.com/bsm-sdk/core/infra"
|
||||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ func PingHello(ctx *gin.Context) {
|
|||||||
|
|
||||||
// DashboardOverview 返回平台总后台的运营概览数据。
|
// DashboardOverview 返回平台总后台的运营概览数据。
|
||||||
func DashboardOverview(ctx *gin.Context) {
|
func DashboardOverview(ctx *gin.Context) {
|
||||||
overview, err := models.GetDashboardOverview()
|
overview, err := GetDashboardOverview()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
infra.Response.Error(ctx, err)
|
infra.Response.Error(ctx, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
package dashboard
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDashboardOverviewReturnsZeroValuesForAnEmptyDatabase(t *testing.T) {
|
func TestNamedStatusesUsesFallbackForUnknownStatus(t *testing.T) {
|
||||||
_, mock := setupDashboardDatabase(t)
|
got := namedStatuses([]statusAggregate{{Status: 16, Count: 3}, {Status: 999, Count: 1}}, map[int]string{16: "已创建"})
|
||||||
for _, query := range []string{
|
if len(got) != 2 || got[0].Name != "已创建" || got[0].Value != 3 || got[1].Name != "其他" {
|
||||||
`SELECT count\(\*\) FROM "gas_basic" WHERE status = \$1`,
|
t.Fatalf("named statuses = %#v", got)
|
||||||
`SELECT count\(\*\) FROM "delivery_basic" WHERE status = \$1`,
|
}
|
||||||
`SELECT count\(\*\) FROM "staff_account" WHERE work_status = \$1`,
|
}
|
||||||
`SELECT count\(\*\) FROM "user_account" WHERE status = \$1`,
|
|
||||||
} {
|
func TestFillRecentDaysIncludesMissingDays(t *testing.T) {
|
||||||
mock.ExpectQuery(regexp.MustCompile(query).String()).WithArgs(sqlmock.AnyArg()).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
now := time.Date(2026, 7, 29, 12, 0, 0, 0, time.Local)
|
||||||
|
got := fillRecentDays([]dailyAggregate{{
|
||||||
|
Date: time.Date(2026, 7, 28, 0, 0, 0, 0, time.Local), OrderCount: 2, OrderAmount: 3600,
|
||||||
|
}}, now)
|
||||||
|
if len(got) != 7 {
|
||||||
|
t.Fatalf("day count = %d, want 7", len(got))
|
||||||
|
}
|
||||||
|
if got[5].Date != "2026-07-28" || got[5].OrderCount != 2 || got[5].OrderAmount != 3600 {
|
||||||
|
t.Fatalf("filled metric = %#v", got[5])
|
||||||
|
}
|
||||||
|
if got[0].OrderCount != 0 || got[6].OrderAmount != 0 {
|
||||||
|
t.Fatalf("missing days were not zero-filled: %#v", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
overview, err := models.GetDashboardOverview()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if overview != (models.DashboardOverview{}) {
|
|
||||||
t.Fatalf("empty dashboard = %#v, want zero values", overview)
|
|
||||||
}
|
|
||||||
assertDashboardMockExpectations(t, mock)
|
|
||||||
}
|
}
|
||||||
|
|||||||
166
backend/api/internal/logic/platform/dashboard/statistics.go
Normal file
166
backend/api/internal/logic/platform/dashboard/statistics.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||||
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||||
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetricSlice 是仪表盘图表使用的名称/数值序列。
|
||||||
|
type MetricSlice struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value int64 `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DailyMetric 是按自然日聚合的运营指标。
|
||||||
|
type DailyMetric struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
OrderAmount int64 `json:"order_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DashboardStatistics 是平台运营总览。该接口专用结构只属于 dashboard 逻辑层。
|
||||||
|
type DashboardStatistics struct {
|
||||||
|
GasBasicCount int64 `json:"gas_basic_count"`
|
||||||
|
DeliveryBasicCount int64 `json:"delivery_basic_count"`
|
||||||
|
StaffCount int64 `json:"staff_count"`
|
||||||
|
UserCount int64 `json:"user_count"`
|
||||||
|
ProductCount int64 `json:"product_count"`
|
||||||
|
ActiveContractCount int64 `json:"active_contract_count"`
|
||||||
|
TodayOrderCount int64 `json:"today_order_count"`
|
||||||
|
TodayOrderAmount int64 `json:"today_order_amount"`
|
||||||
|
PendingTicketCount int64 `json:"pending_ticket_count"`
|
||||||
|
PaidAmount int64 `json:"paid_amount"`
|
||||||
|
OrderStatuses []MetricSlice `json:"order_statuses"`
|
||||||
|
ProductStatuses []MetricSlice `json:"product_statuses"`
|
||||||
|
PaymentChannels []MetricSlice `json:"payment_channels"`
|
||||||
|
RecentOrders []DailyMetric `json:"recent_orders"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusAggregate struct {
|
||||||
|
Status int
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelAggregate struct {
|
||||||
|
Name string
|
||||||
|
Value int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type dailyAggregate struct {
|
||||||
|
Date time.Time
|
||||||
|
OrderCount int64
|
||||||
|
OrderAmount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderStatusNames = map[int]string{
|
||||||
|
common.StatusCreated: "已创建", common.StatusOrdered: "已下单",
|
||||||
|
common.StatusAssigned: "已分配", common.StatusFilling: "充装中",
|
||||||
|
common.StatusReady: "已就绪", common.StatusDelivering: "配送中",
|
||||||
|
common.StatusAwaitingConfirmation: "待确认", common.StatusCompleted: "已完成",
|
||||||
|
common.StatusCancelled: "已取消", common.StatusException: "异常",
|
||||||
|
}
|
||||||
|
|
||||||
|
var productStatusNames = map[int]string{
|
||||||
|
common.StatusPending: "待处理", common.StatusInStock: "在库",
|
||||||
|
common.StatusInTransit: "运输中", common.StatusInUse: "使用中",
|
||||||
|
common.StatusRepairing: "维修中", common.StatusScrapped: "已报废",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboardOverview 汇总组织、人员、资产、订单、支付及客服维度的数据。
|
||||||
|
func GetDashboardOverview() (DashboardStatistics, error) {
|
||||||
|
var result DashboardStatistics
|
||||||
|
counts := []struct {
|
||||||
|
model any
|
||||||
|
where string
|
||||||
|
args []any
|
||||||
|
out *int64
|
||||||
|
}{
|
||||||
|
{&models.GasBasic{}, "status = ?", []any{common.StatusEnable}, &result.GasBasicCount},
|
||||||
|
{&models.DeliveryBasic{}, "status = ?", []any{common.StatusEnable}, &result.DeliveryBasicCount},
|
||||||
|
{&models.StaffAccount{}, "status = ? AND work_status = ?", []any{common.StatusEnable, "on_duty"}, &result.StaffCount},
|
||||||
|
{&models.UserAccount{}, "status = ?", []any{common.StatusEnable}, &result.UserCount},
|
||||||
|
{&models.ProductInfo{}, "status = ?", []any{common.StatusEnable}, &result.ProductCount},
|
||||||
|
{&models.GasorderContract{}, "status = ? AND contract_status = ?", []any{common.StatusEnable, common.StatusActive}, &result.ActiveContractCount},
|
||||||
|
{&models.GasorderBasic{}, "status = ? AND created_at >= CURRENT_DATE", []any{common.StatusEnable}, &result.TodayOrderCount},
|
||||||
|
{&models.CsTicket{}, "status = ? AND ticket_status = ?", []any{common.StatusEnable, common.StatusOpen}, &result.PendingTicketCount},
|
||||||
|
}
|
||||||
|
for _, item := range counts {
|
||||||
|
if err := impl.DBService.Model(item.model).Where(item.where, item.args...).Count(item.out).Error; err != nil {
|
||||||
|
return DashboardStatistics{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := impl.DBService.Model(&models.GasorderBasic{}).
|
||||||
|
Select("COALESCE(SUM(payable_amount), 0)").
|
||||||
|
Where("status = ? AND created_at >= CURRENT_DATE", common.StatusEnable).
|
||||||
|
Scan(&result.TodayOrderAmount).Error; err != nil {
|
||||||
|
return DashboardStatistics{}, err
|
||||||
|
}
|
||||||
|
if err := impl.DBService.Model(&models.WalletPayment{}).
|
||||||
|
Select("COALESCE(SUM(amount), 0)").
|
||||||
|
Where("status = ? AND payment_status = ?", common.StatusEnable, common.StatusPaid).
|
||||||
|
Scan(&result.PaidAmount).Error; err != nil {
|
||||||
|
return DashboardStatistics{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderStatuses []statusAggregate
|
||||||
|
if err := impl.DBService.Model(&models.GasorderBasic{}).
|
||||||
|
Select("order_status AS status, COUNT(*) AS count").
|
||||||
|
Where("status = ?", common.StatusEnable).Group("order_status").Scan(&orderStatuses).Error; err != nil {
|
||||||
|
return DashboardStatistics{}, err
|
||||||
|
}
|
||||||
|
result.OrderStatuses = namedStatuses(orderStatuses, orderStatusNames)
|
||||||
|
|
||||||
|
var productStatuses []statusAggregate
|
||||||
|
if err := impl.DBService.Model(&models.ProductInfo{}).
|
||||||
|
Select("product_status AS status, COUNT(*) AS count").
|
||||||
|
Where("status = ?", common.StatusEnable).Group("product_status").Scan(&productStatuses).Error; err != nil {
|
||||||
|
return DashboardStatistics{}, err
|
||||||
|
}
|
||||||
|
result.ProductStatuses = namedStatuses(productStatuses, productStatusNames)
|
||||||
|
|
||||||
|
if err := impl.DBService.Model(&models.WalletPayment{}).
|
||||||
|
Select("pay_channel AS name, COALESCE(SUM(amount), 0) AS value").
|
||||||
|
Where("status = ? AND payment_status = ?", common.StatusEnable, common.StatusPaid).
|
||||||
|
Group("pay_channel").Order("value DESC").Scan(&result.PaymentChannels).Error; err != nil {
|
||||||
|
return DashboardStatistics{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var daily []dailyAggregate
|
||||||
|
if err := impl.DBService.Model(&models.GasorderBasic{}).
|
||||||
|
Select("DATE_TRUNC('day', created_at) AS date, COUNT(*) AS order_count, COALESCE(SUM(payable_amount), 0) AS order_amount").
|
||||||
|
Where("status = ? AND created_at >= CURRENT_DATE - INTERVAL '6 days'", common.StatusEnable).
|
||||||
|
Group("DATE_TRUNC('day', created_at)").Order("date").Scan(&daily).Error; err != nil {
|
||||||
|
return DashboardStatistics{}, err
|
||||||
|
}
|
||||||
|
result.RecentOrders = fillRecentDays(daily, time.Now())
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func namedStatuses(rows []statusAggregate, names map[int]string) []MetricSlice {
|
||||||
|
result := make([]MetricSlice, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
name, ok := names[row.Status]
|
||||||
|
if !ok {
|
||||||
|
name = "其他"
|
||||||
|
}
|
||||||
|
result = append(result, MetricSlice{Name: name, Value: row.Count})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillRecentDays(rows []dailyAggregate, now time.Time) []DailyMetric {
|
||||||
|
values := make(map[string]dailyAggregate, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
values[row.Date.Format("2006-01-02")] = row
|
||||||
|
}
|
||||||
|
result := make([]DailyMetric, 0, 7)
|
||||||
|
for offset := -6; offset <= 0; offset++ {
|
||||||
|
date := now.AddDate(0, 0, offset).Format("2006-01-02")
|
||||||
|
row := values[date]
|
||||||
|
result = append(result, DailyMetric{Date: date, OrderCount: row.OrderCount, OrderAmount: row.OrderAmount})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -26,14 +26,9 @@ var PlatformMenus = [][]Menu{
|
|||||||
{Identity: "dashboard_reports", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "统计报表", Path: "/dashboard/reports", SortNo: 2, Status: common.StatusEnable},
|
{Identity: "dashboard_reports", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "统计报表", Path: "/dashboard/reports", SortNo: 2, Status: common.StatusEnable},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{Identity: "gas", GroupCode: "gas", Name: "气站管理", Icon: "icon-storage", Path: "/gas", SortNo: 20, Status: common.StatusEnable},
|
{Identity: "organization", GroupCode: "organization", Name: "机构管理", Icon: "icon-storage", Path: "/organization", SortNo: 20, Status: common.StatusEnable},
|
||||||
{Identity: "gas_basic", ParentIdentity: "gas", GroupCode: "gas", Name: "气站", Path: "/gas/gas-basic", SortNo: 1, Status: common.StatusEnable},
|
{Identity: "gas_basic", ParentIdentity: "organization", GroupCode: "organization", Name: "气站管理", Path: "/gas/gas-basic", SortNo: 1, Status: common.StatusEnable},
|
||||||
{Identity: "gas_account", ParentIdentity: "gas", GroupCode: "gas", Name: "气站账户", Path: "/gas/gas-account", SortNo: 2, Status: common.StatusEnable},
|
{Identity: "delivery_basic", ParentIdentity: "organization", GroupCode: "organization", Name: "配送点管理", Path: "/delivery/delivery-basic", SortNo: 2, Status: common.StatusEnable},
|
||||||
},
|
|
||||||
{
|
|
||||||
{Identity: "delivery", GroupCode: "delivery", Name: "配送站管理", Icon: "icon-send", Path: "/delivery", SortNo: 30, Status: common.StatusEnable},
|
|
||||||
{Identity: "delivery_basic", ParentIdentity: "delivery", GroupCode: "delivery", Name: "配送站", Path: "/delivery/delivery-basic", SortNo: 1, Status: common.StatusEnable},
|
|
||||||
{Identity: "delivery_account", ParentIdentity: "delivery", GroupCode: "delivery", Name: "配送站账户", Path: "/delivery/delivery-account", SortNo: 2, Status: common.StatusEnable},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{Identity: "staff", GroupCode: "staff", Name: "工作人员管理", Icon: "icon-user-group", Path: "/staff", SortNo: 40, Status: common.StatusEnable},
|
{Identity: "staff", GroupCode: "staff", Name: "工作人员管理", Icon: "icon-user-group", Path: "/staff", SortNo: 40, Status: common.StatusEnable},
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ func platformRouteMenuIdentity(resource string) string {
|
|||||||
switch {
|
switch {
|
||||||
case resource == "dashboard":
|
case resource == "dashboard":
|
||||||
return "dashboard_overview"
|
return "dashboard_overview"
|
||||||
|
case resource == "gas_account":
|
||||||
|
return "gas_basic"
|
||||||
|
case resource == "delivery_account":
|
||||||
|
return "delivery_basic"
|
||||||
case strings.HasPrefix(resource, "gasorder_contract"):
|
case strings.HasPrefix(resource, "gasorder_contract"):
|
||||||
return "gasorder_contract"
|
return "gasorder_contract"
|
||||||
case resource == "gasorder_track" || resource == "gasorder_track_point":
|
case resource == "gasorder_track" || resource == "gasorder_track_point":
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ func TestSecondLevelMenuPermissionDoesNotGrantSibling(t *testing.T) {
|
|||||||
if !platformMenuAllowsPath(menus, "/heqi/platform/v1/delivery_basic") {
|
if !platformMenuAllowsPath(menus, "/heqi/platform/v1/delivery_basic") {
|
||||||
t.Fatal("selected second-level menu did not grant its resource")
|
t.Fatal("selected second-level menu did not grant its resource")
|
||||||
}
|
}
|
||||||
if platformMenuAllowsPath(menus, "/heqi/platform/v1/delivery_account") {
|
if !platformMenuAllowsPath(menus, "/heqi/platform/v1/delivery_account") {
|
||||||
t.Fatal("selected second-level menu granted a sibling resource")
|
t.Fatal("delivery management did not grant its embedded account management")
|
||||||
|
}
|
||||||
|
if platformMenuAllowsPath(menus, "/heqi/platform/v1/gas_account") {
|
||||||
|
t.Fatal("delivery management granted gas account management")
|
||||||
}
|
}
|
||||||
if platformMenuAllowsPath(menus, "/heqi/platform/v1/gasorder_basic") {
|
if platformMenuAllowsPath(menus, "/heqi/platform/v1/gasorder_basic") {
|
||||||
t.Fatal("delivery permission leaked into gasorder")
|
t.Fatal("delivery permission leaked into gasorder")
|
||||||
|
|||||||
@@ -2,32 +2,6 @@ package models
|
|||||||
|
|
||||||
import "git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
import "git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||||
|
|
||||||
// DashboardOverview 是平台总后台的跨组织运营概览指标。
|
|
||||||
type DashboardOverview struct {
|
|
||||||
GasBasicCount int64 `json:"gas_basic_count"` // 启用可燃气体站数量
|
|
||||||
DeliveryBasicCount int64 `json:"delivery_basic_count"` // 启用配送点数量
|
|
||||||
StaffCount int64 `json:"staff_count"` // 在岗服务人员数量
|
|
||||||
UserCount int64 `json:"user_count"` // 启用业主客户数量
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDashboardOverview 通过独立查询返回首页概览指标。
|
|
||||||
func GetDashboardOverview() (DashboardOverview, error) {
|
|
||||||
var overview DashboardOverview
|
|
||||||
if err := impl.DBService.Model(&GasBasic{}).Where("status = ?", 1).Count(&overview.GasBasicCount).Error; err != nil {
|
|
||||||
return DashboardOverview{}, err
|
|
||||||
}
|
|
||||||
if err := impl.DBService.Model(&DeliveryBasic{}).Where("status = ?", 1).Count(&overview.DeliveryBasicCount).Error; err != nil {
|
|
||||||
return DashboardOverview{}, err
|
|
||||||
}
|
|
||||||
if err := impl.DBService.Model(&StaffAccount{}).Where("work_status = ?", "on_duty").Count(&overview.StaffCount).Error; err != nil {
|
|
||||||
return DashboardOverview{}, err
|
|
||||||
}
|
|
||||||
if err := impl.DBService.Model(&UserAccount{}).Where("status = ?", 1).Count(&overview.UserCount).Error; err != nil {
|
|
||||||
return DashboardOverview{}, err
|
|
||||||
}
|
|
||||||
return overview, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPlatformAccount 返回平台账号分页列表,敏感字段由接口展示层脱敏。
|
// ListPlatformAccount 返回平台账号分页列表,敏感字段由接口展示层脱敏。
|
||||||
func ListPlatformAccount(page, size int) ([]PlatformAccount, int64, error) {
|
func ListPlatformAccount(page, size int) ([]PlatformAccount, int64, error) {
|
||||||
var list []PlatformAccount
|
var list []PlatformAccount
|
||||||
|
|||||||
@@ -2,11 +2,36 @@ import { request } from './http';
|
|||||||
import { resourceApi } from './resource';
|
import { resourceApi } from './resource';
|
||||||
|
|
||||||
/** 平台角色、菜单、账号和工作台接口。 */
|
/** 平台角色、菜单、账号和工作台接口。 */
|
||||||
|
export interface DashboardMetric {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
export interface DashboardDailyMetric {
|
||||||
|
date: string;
|
||||||
|
order_count: number;
|
||||||
|
order_amount: number;
|
||||||
|
}
|
||||||
|
export interface DashboardOverview {
|
||||||
|
gas_basic_count: number;
|
||||||
|
delivery_basic_count: number;
|
||||||
|
staff_count: number;
|
||||||
|
user_count: number;
|
||||||
|
product_count: number;
|
||||||
|
active_contract_count: number;
|
||||||
|
today_order_count: number;
|
||||||
|
today_order_amount: number;
|
||||||
|
pending_ticket_count: number;
|
||||||
|
paid_amount: number;
|
||||||
|
order_statuses: DashboardMetric[];
|
||||||
|
product_statuses: DashboardMetric[];
|
||||||
|
payment_channels: DashboardMetric[];
|
||||||
|
recent_orders: DashboardDailyMetric[];
|
||||||
|
}
|
||||||
export type PlatformRole = { identity: string; role_code: string; name: string; location_scope: string; is_system: boolean; status: number };
|
export type PlatformRole = { identity: string; role_code: string; name: string; location_scope: string; is_system: boolean; status: number };
|
||||||
export type PlatformMenu = { identity: string; parent_identity?: string; group_code: string; name: string; icon: string; path: string; sort_no: number };
|
export type PlatformMenu = { identity: string; parent_identity?: string; group_code: string; name: string; icon: string; path: string; sort_no: number };
|
||||||
|
|
||||||
export const platformApi = {
|
export const platformApi = {
|
||||||
overview: () => request<Record<string, number>>('/dashboard/overview'),
|
overview: () => request<DashboardOverview>('/dashboard/overview'),
|
||||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
||||||
listRole: () => resourceApi.list<PlatformRole>('/platform_role'),
|
listRole: () => resourceApi.list<PlatformRole>('/platform_role'),
|
||||||
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export type ResourceUiDefinition = {
|
|||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
canChangeStatus: boolean;
|
canChangeStatus: boolean;
|
||||||
canArchive: boolean;
|
canArchive: boolean;
|
||||||
|
accountManagement?: {
|
||||||
|
resource: `/${string}`;
|
||||||
|
relationKey: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldLabels: Record<string, string> = {
|
const fieldLabels: Record<string, string> = {
|
||||||
@@ -276,9 +281,9 @@ function define(
|
|||||||
const reason = [f('reason', { required: true })];
|
const reason = [f('reason', { required: true })];
|
||||||
|
|
||||||
export const resources: ResourceUiDefinition[] = [
|
export const resources: ResourceUiDefinition[] = [
|
||||||
define('gas_basic', '气站', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]),
|
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' } },
|
||||||
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('gas_basic_identity', '/gas_basic', true)]),
|
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('gas_basic_identity', '/gas_basic', true)]),
|
||||||
define('delivery_basic', '配送站', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]),
|
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' } },
|
||||||
define('delivery_account', '配送站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
define('delivery_account', '配送站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('delivery_basic_identity', '/delivery_basic', true)]),
|
||||||
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status')]),
|
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code'), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status')]),
|
||||||
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
|
||||||
|
|||||||
@@ -59,13 +59,11 @@ const routes: AppRouteRecordRaw[] = [
|
|||||||
{ path: 'reports', name: 'dashboard-reports', component: () => import('@/views/dashboard/ReportPage.vue'), meta: { title: '统计报表', requiresAuth: true, menuCode: 'dashboard_reports' } },
|
{ path: 'reports', name: 'dashboard-reports', component: () => import('@/views/dashboard/ReportPage.vue'), meta: { title: '统计报表', requiresAuth: true, menuCode: 'dashboard_reports' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
group('gas', 'gas', '气站管理', 'icon-storage', 10, [
|
group('organization', 'organization', '机构管理', 'icon-storage', 10, [
|
||||||
child('gas', 'gas-basic', 'basic', '气站', '/gas_basic', 'gas_basic'),
|
child('organization', 'gas-basic', 'gas-basic', '气站管理', '/gas_basic', 'gas_basic'),
|
||||||
child('gas', 'gas-account', 'account', '气站账户', '/gas_account', 'gas_account'),
|
child('organization', 'delivery-basic', 'delivery-basic', '配送点管理', '/delivery_basic', 'delivery_basic'),
|
||||||
]),
|
child('organization', 'gas-account', 'gas-account', '气站账户', '/gas_account', 'gas_basic', true, 'gas-basic'),
|
||||||
group('delivery', 'delivery', '配送站管理', 'icon-send', 20, [
|
child('organization', 'delivery-account', 'delivery-account', '配送点账户', '/delivery_account', 'delivery_basic', true, 'delivery-basic'),
|
||||||
child('delivery', 'delivery-basic', 'basic', '配送站', '/delivery_basic', 'delivery_basic'),
|
|
||||||
child('delivery', 'delivery-account', 'account', '配送站账户', '/delivery_account', 'delivery_account'),
|
|
||||||
]),
|
]),
|
||||||
group('staff', 'staff', '工作人员管理', 'icon-user-group', 30, [
|
group('staff', 'staff', '工作人员管理', 'icon-user-group', 30, [
|
||||||
child('staff', 'staff-account', 'account', '工作人员', '/staff_account', 'staff_account'),
|
child('staff', 'staff-account', 'account', '工作人员', '/staff_account', 'staff_account'),
|
||||||
|
|||||||
@@ -1,3 +1,125 @@
|
|||||||
<template><div class="dashboard"><a-grid :cols="{ xs: 1, sm: 2, lg: 5 }" :col-gap="16" :row-gap="16"><a-grid-item v-for="item in cards" :key="item.key"><a-card :bordered="false"><a-statistic :title="item.label" :value="overview[item.key] || 0" /></a-card></a-grid-item></a-grid><a-card title="运营概览" :bordered="false" class="intro"><p>跨组织查看可燃气体站、配送点、服务人员、业主客户与待处置安全事件。</p></a-card></div></template>
|
<template>
|
||||||
<script setup lang="ts">import { Message } from '@arco-design/web-vue'; import { onMounted, ref } from 'vue'; import { platformApi } from '@/api/platform'; const overview = ref<Record<string, number>>({}); const cards = [{ key: 'gas_basic_count', label: '可燃气体站' }, { key: 'delivery_basic_count', label: '配送点' }, { key: 'staff_count', label: '在岗服务人员' }, { key: 'user_count', label: '业主客户' }, { key: 'pending_safety_count', label: '待处理安全事件' }]; onMounted(async () => { try { overview.value = await platformApi.overview(); } catch (error) { Message.error((error as Error).message); } });</script>
|
<div class="dashboard">
|
||||||
<style scoped lang="less">.dashboard { padding: 20px; } .intro { margin-top: 16px; }</style>
|
<a-spin :loading="loading" class="dashboard-spin">
|
||||||
|
<a-grid :cols="{ xs: 1, sm: 2, lg: 4 }" :col-gap="16" :row-gap="16">
|
||||||
|
<a-grid-item v-for="item in cards" :key="item.key">
|
||||||
|
<a-card :bordered="false" class="metric-card">
|
||||||
|
<a-statistic :title="item.label" :value="overview[item.key]" :precision="item.money ? 2 : 0"
|
||||||
|
:prefix="item.money ? '¥' : undefined" show-group-separator />
|
||||||
|
<div class="metric-hint">{{ item.hint }}</div>
|
||||||
|
</a-card>
|
||||||
|
</a-grid-item>
|
||||||
|
</a-grid>
|
||||||
|
|
||||||
|
<a-card title="快捷操作" :bordered="false" class="section-card">
|
||||||
|
<a-grid :cols="{ xs: 2, sm: 3, md: 4, lg: 6 }" :col-gap="12" :row-gap="12">
|
||||||
|
<a-grid-item v-for="action in availableActions" :key="action.route">
|
||||||
|
<a-button long class="quick-action" @click="router.push({ name: action.route })">
|
||||||
|
<template #icon><component :is="action.icon" /></template>
|
||||||
|
{{ action.label }}
|
||||||
|
</a-button>
|
||||||
|
</a-grid-item>
|
||||||
|
</a-grid>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<a-grid :cols="{ xs: 1, lg: 2 }" :col-gap="16" :row-gap="16" class="section-card">
|
||||||
|
<a-grid-item><a-card title="近 7 日配送订单趋势" :bordered="false"><Chart :option="orderTrendOption" height="320px" /></a-card></a-grid-item>
|
||||||
|
<a-grid-item><a-card title="订单状态分布" :bordered="false"><Chart :option="orderStatusOption" height="320px" /></a-card></a-grid-item>
|
||||||
|
<a-grid-item><a-card title="气瓶状态分布" :bordered="false"><Chart :option="productStatusOption" height="320px" /></a-card></a-grid-item>
|
||||||
|
<a-grid-item><a-card title="支付渠道实收金额" :bordered="false"><Chart :option="paymentChannelOption" height="320px" /></a-card></a-grid-item>
|
||||||
|
</a-grid>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { IconApps, IconFile, IconPlus, IconSettings, IconStorage, IconUser } from '@arco-design/web-vue/es/icon';
|
||||||
|
import type { EChartsOption } from 'echarts';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { platformApi, type DashboardOverview } from '@/api/platform';
|
||||||
|
import { useUserStore } from '@/store';
|
||||||
|
|
||||||
|
type CountKey = 'gas_basic_count' | 'delivery_basic_count' | 'staff_count' | 'user_count' |
|
||||||
|
'product_count' | 'active_contract_count' | 'today_order_count' | 'today_order_amount' |
|
||||||
|
'pending_ticket_count' | 'paid_amount';
|
||||||
|
|
||||||
|
const emptyOverview = (): DashboardOverview => ({
|
||||||
|
gas_basic_count: 0, delivery_basic_count: 0, staff_count: 0, user_count: 0,
|
||||||
|
product_count: 0, active_contract_count: 0, today_order_count: 0,
|
||||||
|
today_order_amount: 0, pending_ticket_count: 0, paid_amount: 0,
|
||||||
|
order_statuses: [], product_statuses: [], payment_channels: [], recent_orders: [],
|
||||||
|
});
|
||||||
|
const loading = ref(false);
|
||||||
|
const overview = ref<DashboardOverview>(emptyOverview());
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const cards: { key: CountKey; label: string; hint: string; money?: boolean }[] = [
|
||||||
|
{ key: 'gas_basic_count', label: '启用气站', hint: '当前正常运营' },
|
||||||
|
{ key: 'delivery_basic_count', label: '启用配送点', hint: '跨组织汇总' },
|
||||||
|
{ key: 'staff_count', label: '在岗人员', hint: '当前可接单' },
|
||||||
|
{ key: 'user_count', label: '启用客户', hint: '有效业主账户' },
|
||||||
|
{ key: 'product_count', label: '在册气瓶', hint: '已启用资产' },
|
||||||
|
{ key: 'active_contract_count', label: '生效合同', hint: '当前履约中' },
|
||||||
|
{ key: 'today_order_count', label: '今日订单', hint: '自然日新增' },
|
||||||
|
{ key: 'today_order_amount', label: '今日应付金额', hint: '订单口径', money: true },
|
||||||
|
{ key: 'pending_ticket_count', label: '待受理工单', hint: '客服待办' },
|
||||||
|
{ key: 'paid_amount', label: '累计实收金额', hint: '支付成功口径', money: true },
|
||||||
|
];
|
||||||
|
const actions = [
|
||||||
|
{ label: '新建气站', route: 'gas-basic', menu: 'gas_basic', icon: IconPlus },
|
||||||
|
{ label: '配送订单', route: 'gasorder-orders', menu: 'gasorder_basic', icon: IconFile },
|
||||||
|
{ label: '气瓶档案', route: 'product-info', menu: 'product_info', icon: IconStorage },
|
||||||
|
{ label: '用户管理', route: 'user-account', menu: 'user_account', icon: IconUser },
|
||||||
|
{ label: '商城订单', route: 'ec-orders', menu: 'ec_order', icon: IconApps },
|
||||||
|
{ label: '角色权限', route: 'platform-roles', menu: 'platform_role', icon: IconSettings },
|
||||||
|
];
|
||||||
|
const availableActions = computed(() => actions.filter(
|
||||||
|
(action) => userStore.role === 'root' || userStore.menuCodes.includes(action.menu),
|
||||||
|
));
|
||||||
|
const centsToYuan = (value: number) => value / 100;
|
||||||
|
const pieOption = (data: { name: string; value: number }[]): EChartsOption => ({
|
||||||
|
tooltip: { trigger: 'item' }, legend: { bottom: 0 },
|
||||||
|
series: [{ type: 'pie', radius: ['42%', '68%'], center: ['50%', '44%'], data, label: { formatter: '{b}\\n{c}' } }],
|
||||||
|
});
|
||||||
|
const orderStatusOption = computed(() => pieOption(overview.value.order_statuses));
|
||||||
|
const productStatusOption = computed(() => pieOption(overview.value.product_statuses));
|
||||||
|
const orderTrendOption = computed<EChartsOption>(() => ({
|
||||||
|
tooltip: { trigger: 'axis' }, legend: { data: ['订单数', '订单金额'] },
|
||||||
|
grid: { left: 48, right: 58, bottom: 34 },
|
||||||
|
xAxis: { type: 'category', data: overview.value.recent_orders.map((item) => item.date.slice(5)) },
|
||||||
|
yAxis: [{ type: 'value', name: '单' }, { type: 'value', name: '元' }],
|
||||||
|
series: [
|
||||||
|
{ name: '订单数', type: 'bar', data: overview.value.recent_orders.map((item) => item.order_count) },
|
||||||
|
{ name: '订单金额', type: 'line', yAxisIndex: 1, smooth: true, data: overview.value.recent_orders.map((item) => centsToYuan(item.order_amount)) },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
const paymentChannelOption = computed<EChartsOption>(() => ({
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: 70, right: 24, bottom: 28 },
|
||||||
|
xAxis: { type: 'value', name: '元' },
|
||||||
|
yAxis: { type: 'category', data: overview.value.payment_channels.map((item) => item.name || '未知渠道') },
|
||||||
|
series: [{ type: 'bar', data: overview.value.payment_channels.map((item) => centsToYuan(item.value)) }],
|
||||||
|
}));
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await platformApi.overview();
|
||||||
|
overview.value = { ...data, today_order_amount: centsToYuan(data.today_order_amount), paid_amount: centsToYuan(data.paid_amount) };
|
||||||
|
} catch (error) {
|
||||||
|
Message.error((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.dashboard { padding: 20px; }
|
||||||
|
.dashboard-spin { width: 100%; }
|
||||||
|
.metric-card { min-height: 120px; }
|
||||||
|
.metric-hint { margin-top: 8px; color: var(--color-text-3); font-size: 12px; }
|
||||||
|
.section-card { margin-top: 16px; }
|
||||||
|
.quick-action { height: 52px; justify-content: flex-start; padding: 0 18px; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -16,17 +16,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { platformApi } from '@/api/platform';
|
import { platformApi, type DashboardOverview } from '@/api/platform';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const overview = ref<Record<string, number>>({});
|
const overview = ref<DashboardOverview | null>(null);
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
gas_basic_count: '气站数量',
|
gas_basic_count: '气站数量',
|
||||||
delivery_basic_count: '配送站数量',
|
delivery_basic_count: '配送站数量',
|
||||||
staff_account_count: '工作人员数量',
|
staff_count: '在岗工作人员',
|
||||||
user_account_count: '用户数量',
|
user_count: '启用用户',
|
||||||
|
product_count: '在册气瓶',
|
||||||
|
active_contract_count: '生效合同',
|
||||||
|
today_order_count: '今日订单',
|
||||||
|
today_order_amount: '今日应付金额(分)',
|
||||||
|
pending_ticket_count: '待受理工单',
|
||||||
|
paid_amount: '累计实收金额(分)',
|
||||||
};
|
};
|
||||||
const metrics = computed(() => Object.entries(overview.value));
|
const metrics = computed(() => {
|
||||||
|
if (!overview.value) return [];
|
||||||
|
return Object.entries(overview.value).filter(
|
||||||
|
(entry): entry is [string, number] => typeof entry[1] === 'number',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<a-card :title="definition.title" :bordered="false">
|
<a-card :title="definition.title" :bordered="false">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-space>
|
<a-space>
|
||||||
|
<a-button v-if="managedOwnerIdentity" @click="router.back()">返回机构列表</a-button>
|
||||||
<a-button @click="load">刷新</a-button>
|
<a-button @click="load">刷新</a-button>
|
||||||
<a-button v-if="canCreate" type="primary" @click="openCreate">新建</a-button>
|
<a-button v-if="canCreate" type="primary" @click="openCreate">新建</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -34,13 +35,17 @@
|
|||||||
{{ displayFieldValue(field, record) }}
|
{{ displayFieldValue(field, record) }}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="操作" :width="260" fixed="right">
|
<a-table-column v-if="definition.accountManagement" title="账户数" :width="90">
|
||||||
|
<template #cell="{ record }">{{ accountCounts[String(record.identity)] ?? 0 }}</template>
|
||||||
|
</a-table-column>
|
||||||
|
<a-table-column title="操作" :width="definition.accountManagement ? 350 : 280" fixed="right">
|
||||||
<template #cell="{ record }">
|
<template #cell="{ record }">
|
||||||
<a-space>
|
<a-space>
|
||||||
|
<a-button v-if="definition.accountManagement" size="mini" type="primary" @click="manageAccounts(record)">账户管理</a-button>
|
||||||
<a-button size="mini" @click="openDetail(record)">详情</a-button>
|
<a-button size="mini" @click="openDetail(record)">详情</a-button>
|
||||||
<a-button v-if="canEdit" size="mini" :disabled="isProtectedRecord(record)" @click="openEdit(record)">编辑</a-button>
|
<a-button v-if="canEdit" size="mini" :disabled="isProtectedRecord(record)" @click="openEdit(record)">编辑</a-button>
|
||||||
<a-button v-if="canChangeStatus" size="mini" :disabled="isProtectedRecord(record)" @click="openStatus(record)">状态</a-button>
|
<a-button v-if="canChangeStatus" size="mini" :disabled="isProtectedRecord(record)" @click="openStatus(record)">审核</a-button>
|
||||||
<a-button v-if="canArchive" size="mini" status="danger" :disabled="isProtectedRecord(record)" @click="confirmArchive(record)">归档</a-button>
|
<a-button v-if="canArchive" size="mini" status="danger" :disabled="isProtectedRecord(record)" @click="confirmArchive(record)">删除</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
@@ -147,6 +152,7 @@ const page = ref(Math.max(1, Number(route.query.page) || 1));
|
|||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const list = ref<Row[]>([]);
|
const list = ref<Row[]>([]);
|
||||||
|
const accountCounts = ref<Record<string, number>>({});
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
|
||||||
});
|
});
|
||||||
@@ -163,6 +169,12 @@ const roleOptions = ref<PlatformRole[]>([]);
|
|||||||
const relationOptions = reactive<Record<string, Row[]>>({});
|
const relationOptions = reactive<Record<string, Row[]>>({});
|
||||||
const relationLoading = reactive<Record<string, boolean>>({});
|
const relationLoading = reactive<Record<string, boolean>>({});
|
||||||
const relationSearchTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
const relationSearchTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
const managedOwnerIdentity = computed(() =>
|
||||||
|
typeof route.query.owner_identity === 'string' ? route.query.owner_identity : '',
|
||||||
|
);
|
||||||
|
const managedRelationKey = computed(() =>
|
||||||
|
typeof route.query.relation_key === 'string' ? route.query.relation_key : '',
|
||||||
|
);
|
||||||
const platformRootWriteResources = new Set([
|
const platformRootWriteResources = new Set([
|
||||||
'platform_account',
|
'platform_account',
|
||||||
'platform_role',
|
'platform_role',
|
||||||
@@ -263,14 +275,30 @@ function resetForm(data?: Row) {
|
|||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await resourceApi.list<Row>(
|
if (managedOwnerIdentity.value && managedRelationKey.value) {
|
||||||
props.definition.resource,
|
const accounts = await loadAllRows(props.definition.resource);
|
||||||
page.value,
|
const keyword = filters.keyword.trim().toLowerCase();
|
||||||
pageSize,
|
const filtered = accounts.filter(
|
||||||
filters.keyword ? { keyword: filters.keyword } : {},
|
(row) =>
|
||||||
);
|
String(row[managedRelationKey.value] ?? '') === managedOwnerIdentity.value &&
|
||||||
list.value = result.list;
|
(!keyword ||
|
||||||
total.value = result.total;
|
['username', 'display_name', 'role_code'].some((key) =>
|
||||||
|
String(row[key] ?? '').toLowerCase().includes(keyword),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
list.value = filtered.slice((page.value - 1) * pageSize, page.value * pageSize);
|
||||||
|
total.value = filtered.length;
|
||||||
|
} else {
|
||||||
|
const result = await resourceApi.list<Row>(
|
||||||
|
props.definition.resource,
|
||||||
|
page.value,
|
||||||
|
pageSize,
|
||||||
|
filters.keyword ? { keyword: filters.keyword } : {},
|
||||||
|
);
|
||||||
|
list.value = result.list;
|
||||||
|
total.value = result.total;
|
||||||
|
}
|
||||||
|
await loadAccountCounts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error((error as Error).message);
|
Message.error((error as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -278,8 +306,49 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAllRows(resource: string) {
|
||||||
|
const rows: Row[] = [];
|
||||||
|
for (let currentPage = 1; currentPage <= 100; currentPage += 1) {
|
||||||
|
const result = await resourceApi.list<Row>(resource, currentPage, 100);
|
||||||
|
rows.push(...result.list);
|
||||||
|
if (rows.length >= result.total || result.list.length < 100) break;
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAccountCounts() {
|
||||||
|
const management = props.definition.accountManagement;
|
||||||
|
if (!management) {
|
||||||
|
accountCounts.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const accounts = await loadAllRows(management.resource);
|
||||||
|
accountCounts.value = accounts.reduce<Record<string, number>>((counts, account) => {
|
||||||
|
const identity = String(account[management.relationKey] ?? '');
|
||||||
|
if (identity) counts[identity] = (counts[identity] ?? 0) + 1;
|
||||||
|
return counts;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function manageAccounts(row: Row) {
|
||||||
|
const management = props.definition.accountManagement;
|
||||||
|
if (!management) return;
|
||||||
|
const routeName = management.resource === '/gas_account'
|
||||||
|
? 'organization-gas-account'
|
||||||
|
: 'organization-delivery-account';
|
||||||
|
router.push({
|
||||||
|
name: routeName,
|
||||||
|
query: {
|
||||||
|
owner_identity: String(row.identity ?? ''),
|
||||||
|
relation_key: management.relationKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function syncQuery() {
|
async function syncQuery() {
|
||||||
const query: Record<string, string> = {};
|
const query: Record<string, string> = {};
|
||||||
|
if (managedOwnerIdentity.value) query.owner_identity = managedOwnerIdentity.value;
|
||||||
|
if (managedRelationKey.value) query.relation_key = managedRelationKey.value;
|
||||||
if (page.value > 1) query.page = String(page.value);
|
if (page.value > 1) query.page = String(page.value);
|
||||||
if (filters.keyword.trim()) query.keyword = filters.keyword.trim();
|
if (filters.keyword.trim()) query.keyword = filters.keyword.trim();
|
||||||
await router.replace({ query });
|
await router.replace({ query });
|
||||||
@@ -299,6 +368,9 @@ async function resetSearch() {
|
|||||||
function openCreate() {
|
function openCreate() {
|
||||||
editingIdentity.value = '';
|
editingIdentity.value = '';
|
||||||
resetForm();
|
resetForm();
|
||||||
|
if (managedOwnerIdentity.value && managedRelationKey.value) {
|
||||||
|
form[managedRelationKey.value] = managedOwnerIdentity.value;
|
||||||
|
}
|
||||||
formVisible.value = true;
|
formVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +501,7 @@ async function save() {
|
|||||||
function openStatus(row: Row) {
|
function openStatus(row: Row) {
|
||||||
detail.value = row;
|
detail.value = row;
|
||||||
openDetailAction({
|
openDetailAction({
|
||||||
name: '修改状态',
|
name: '审核状态',
|
||||||
resource: `${props.definition.resource}/:identity/status`,
|
resource: `${props.definition.resource}/:identity/status`,
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
fields: [
|
fields: [
|
||||||
@@ -441,7 +513,6 @@ function openStatus(row: Row) {
|
|||||||
options: [
|
options: [
|
||||||
{ label: '启用', value: 1 },
|
{ label: '启用', value: 1 },
|
||||||
{ label: '停用', value: 2 },
|
{ label: '停用', value: 2 },
|
||||||
{ label: '归档', value: 3 },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -450,15 +521,15 @@ function openStatus(row: Row) {
|
|||||||
|
|
||||||
function confirmArchive(row: Row) {
|
function confirmArchive(row: Row) {
|
||||||
Modal.warning({
|
Modal.warning({
|
||||||
title: '确认归档',
|
title: '确认删除',
|
||||||
content: '归档后该记录将不再参与日常业务。',
|
content: '删除后该记录将被归档,不再参与日常业务。',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
try {
|
try {
|
||||||
await resourceApi.archive(
|
await resourceApi.archive(
|
||||||
props.definition.resource,
|
props.definition.resource,
|
||||||
String(row.identity),
|
String(row.identity),
|
||||||
);
|
);
|
||||||
Message.success('已归档');
|
Message.success('已删除');
|
||||||
await load();
|
await load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error((error as Error).message);
|
Message.error((error as Error).message);
|
||||||
|
|||||||
Reference in New Issue
Block a user