= [
+ {
+ title: '推广链接',
+ key: 'link',
+ width: 260,
+ fixed: 'left',
+ render: (_, row) => (
+
+
+ {row.name}
+
+ {row.status === 'active' ? '启用' : '停用'}
+
+
+ {row.code}
+
+ {formatText(row.short_url)}
+
+
+ ),
+ },
+ {
+ title: '渠道',
+ dataIndex: 'channel_type',
+ width: 110,
+ render: value => formatText(value),
+ },
+ {
+ title: '点击 PV / UV',
+ key: 'clicks',
+ width: 140,
+ align: 'right',
+ render: (_, row) => (
+
+ {row.click_pv} / {row.click_uv}
+ IP {row.click_ip}
+
+ ),
+ },
+ {
+ title: '注册 / 下单',
+ key: 'conversion',
+ width: 140,
+ align: 'right',
+ render: (_, row) => (
+
+ {row.register_count} / {row.order_count}
+
+ 转化 {(row.conversion_rate * 100).toFixed(1)}%
+
+
+ ),
+ },
+ {
+ title: '成交金额',
+ key: 'gmv',
+ width: 160,
+ align: 'right',
+ render: (_, row) => (
+
+ {formatMoney(row.paid_gmv_cent)}
+ 成交 {row.paid_order_count} 单
+
+ ),
+ },
+ {
+ title: '成本',
+ dataIndex: 'cost_cent',
+ width: 120,
+ align: 'right',
+ render: value => formatMoney(value),
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'created_at',
+ width: 170,
+ render: value => formatDateTime(value),
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 210,
+ fixed: 'right',
+ render: (_, row) => (
+
+ }
+ disabled={!row.short_url}
+ onClick={() => void copyShortURL(row)}
+ >
+ 复制
+
+ }
+ disabled={!canManage}
+ onClick={() => openEdit(row)}
+ >
+ 编辑
+
+
+
+ ),
+ },
+ ]
+
+ if (!canManage) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts
index 43bc477..79c7047 100644
--- a/frontend/src/features/admin/api.ts
+++ b/frontend/src/features/admin/api.ts
@@ -59,6 +59,25 @@ import type {
AdminSupportGroup,
AdminSupportGroupPayload,
AdminSupportMember,
+ AdminStaffAccount,
+ AdminStaffAccountPage,
+ AdminStaffAccountQuery,
+ AdminStaffCreatePayload,
+ AdminStaffUpdatePayload,
+ AdminManualEntry,
+ AdminManualEntryAgentStat,
+ AdminManualEntryOption,
+ AdminManualEntryOptions,
+ AdminManualEntryPage,
+ AdminManualEntryPayload,
+ AdminManualEntryQuery,
+ AdminQuickReply,
+ AdminQuickReplyPayload,
+ AdminTrackingLink,
+ AdminTrackingLinkPage,
+ AdminTrackingLinkPayload,
+ AdminTrackingLinkQuery,
+ AdminTrackingOverview,
AdminSyncSiteAccountsPayload,
AdminSyncSiteAccountsResult,
AdminSystemConfig,
@@ -1082,6 +1101,198 @@ export async function assignAdminSupportGroupMembers(id: number, memberIds: numb
})
}
+export async function fetchAdminStaffAccounts(query: AdminStaffAccountQuery = {}) {
+ const { data } = await adminClient.get>>(
+ '/admin/admin-users',
+ { params: compactParams(query) }
+ )
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
+
+export async function fetchAdminStaffAccount(id: number) {
+ const { data } = await adminClient.get>(
+ `/admin/admin-users/${id}`
+ )
+ return data.data
+}
+
+export async function createAdminStaffAccount(payload: AdminStaffCreatePayload) {
+ const { data } = await adminClient.post>(
+ '/admin/admin-users',
+ payload
+ )
+ return data.data
+}
+
+export async function updateAdminStaffAccount(id: number, payload: AdminStaffUpdatePayload) {
+ const { data } = await adminClient.put>(
+ `/admin/admin-users/${id}`,
+ payload
+ )
+ return data.data
+}
+
+export async function deleteAdminStaffAccount(id: number) {
+ await adminClient.delete(`/admin/admin-users/${id}`)
+}
+
+export async function assignAdminStaffRoles(id: number, roleIds: number[]) {
+ await adminClient.put(`/admin/admin-users/${id}/roles`, { role_ids: roleIds })
+}
+
+export async function resetAdminStaffPassword(id: number, newPassword: string) {
+ await adminClient.put(`/admin/admin-users/${id}/password`, { new_password: newPassword })
+}
+
+export async function fetchAdminManualEntries(query: AdminManualEntryQuery = {}) {
+ const { data } = await adminClient.get>>(
+ '/admin/manual-entries',
+ { params: compactParams(query) }
+ )
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
+
+export async function createAdminManualEntry(payload: AdminManualEntryPayload) {
+ const { data } = await adminClient.post>(
+ '/admin/manual-entries',
+ payload
+ )
+ return data.data.entry
+}
+
+export async function updateAdminManualEntry(id: number, payload: AdminManualEntryPayload) {
+ const { data } = await adminClient.put>(
+ `/admin/manual-entries/${id}`,
+ payload
+ )
+ return data.data
+}
+
+export async function deleteAdminManualEntry(id: number) {
+ await adminClient.delete(`/admin/manual-entries/${id}`)
+}
+
+export async function fetchAdminManualEntryOptions() {
+ const { data } = await adminClient.get>(
+ '/admin/manual-entries/options'
+ )
+ return {
+ platforms: data.data?.platforms ?? [],
+ operators: data.data?.operators ?? [],
+ }
+}
+
+export async function createAdminManualEntryOption(payload: { type: string; name: string }) {
+ const { data } = await adminClient.post>(
+ '/admin/manual-entries/options',
+ payload
+ )
+ return data.data
+}
+
+export async function updateAdminManualEntryOption(id: number, name: string) {
+ const { data } = await adminClient.put>(
+ `/admin/manual-entries/options/${id}`,
+ { name }
+ )
+ return data.data
+}
+
+export async function deleteAdminManualEntryOption(id: number) {
+ await adminClient.delete(`/admin/manual-entries/options/${id}`)
+}
+
+export async function fetchAdminManualEntryTodayStats() {
+ const { data } = await adminClient.get>(
+ '/admin/manual-entries/today-stats'
+ )
+ return data.data ?? []
+}
+
+export async function fetchAdminQuickReplies() {
+ const { data } = await adminClient.get>(
+ '/admin/chats/quick-replies'
+ )
+ return data.data ?? []
+}
+
+export async function createAdminQuickReply(payload: AdminQuickReplyPayload) {
+ const { data } = await adminClient.post>(
+ '/admin/chats/quick-replies',
+ payload
+ )
+ return data.data
+}
+
+export async function updateAdminQuickReply(id: number, payload: AdminQuickReplyPayload) {
+ const { data } = await adminClient.put>(
+ `/admin/chats/quick-replies/${id}`,
+ payload
+ )
+ return data.data
+}
+
+export async function deleteAdminQuickReply(id: number) {
+ await adminClient.delete(`/admin/chats/quick-replies/${id}`)
+}
+
+export async function fetchAdminTrackingLinks(query: AdminTrackingLinkQuery = {}) {
+ const { data } = await adminClient.get>>(
+ '/admin/tracking-links',
+ { params: compactParams(query) }
+ )
+ return {
+ items: data.data.items ?? [],
+ total: data.data.total ?? 0,
+ page: data.data.page ?? query.page ?? 1,
+ page_size: data.data.page_size ?? query.page_size ?? 20,
+ }
+}
+
+export async function fetchAdminTrackingOverview() {
+ const { data } = await adminClient.get>(
+ '/admin/tracking-links/overview'
+ )
+ return data.data
+}
+
+export async function createAdminTrackingLink(payload: AdminTrackingLinkPayload) {
+ const { data } = await adminClient.post>(
+ '/admin/tracking-links',
+ payload
+ )
+ return data.data
+}
+
+export async function updateAdminTrackingLink(id: number, payload: AdminTrackingLinkPayload) {
+ const { data } = await adminClient.put>(
+ `/admin/tracking-links/${id}`,
+ payload
+ )
+ return data.data
+}
+
+export async function updateAdminTrackingLinkStatus(id: number, status: string) {
+ const { data } = await adminClient.put>(
+ `/admin/tracking-links/${id}/status`,
+ { status }
+ )
+ return data.data
+}
+
+
+
+
export async function fetchFinanceDisbursements(query: FinanceDisbursementQuery) {
const { data } = await adminClient.get>>(
'/admin/finance/disbursements',
diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts
index 0ef24d1..4eeaa6d 100644
--- a/frontend/src/features/admin/types.ts
+++ b/frontend/src/features/admin/types.ts
@@ -1408,3 +1408,184 @@ export interface AdminSupportGroupPayload {
sort_order?: number
member_ids?: number[]
}
+
+export interface AdminStaffRole {
+ id: number
+ code: string
+ name: string
+ description?: string
+ perm_count?: number
+}
+
+export interface AdminStaffAccount {
+ id: number
+ username: string
+ nickname: string
+ status: string
+ roles: AdminStaffRole[]
+ last_login_at?: string | null
+ created_at: string
+ updated_at: string
+}
+
+export interface AdminStaffAccountPage {
+ items: AdminStaffAccount[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface AdminStaffAccountQuery {
+ page?: number
+ page_size?: number
+}
+
+export interface AdminStaffCreatePayload {
+ username: string
+ password: string
+ nickname?: string
+}
+
+export interface AdminStaffUpdatePayload {
+ nickname?: string
+ status?: string
+}
+
+export interface AdminManualEntry {
+ id: number
+ entry_no: string
+ platform: string
+ operator: string
+ admin_id: number
+ admin_name: string
+ admin_username: string
+ remark: string
+ created_at: string
+}
+
+export interface AdminManualEntryPage {
+ items: AdminManualEntry[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface AdminManualEntryQuery {
+ entry_no?: string
+ keyword?: string
+ admin_id?: number
+ start_date?: string
+ end_date?: string
+ page?: number
+ page_size?: number
+}
+
+export interface AdminManualEntryPayload {
+ entry_no: string
+ platform?: string
+ operator?: string
+ remark?: string
+}
+
+export interface AdminManualEntryOption {
+ id: number
+ type: string
+ name: string
+ sort_order: number
+ created_at: string
+}
+
+export interface AdminManualEntryOptions {
+ platforms: AdminManualEntryOption[]
+ operators: AdminManualEntryOption[]
+}
+
+export interface AdminManualEntryAgentStat {
+ date: string
+ admin_id: number
+ admin_name: string
+ count: number
+}
+
+export interface AdminQuickReply {
+ id: number
+ admin_user_id: number
+ title: string
+ content: string
+ sort_order: number
+ is_global: boolean
+}
+
+export interface AdminQuickReplyPayload {
+ title: string
+ content: string
+ sort_order?: number
+ is_global?: boolean
+}
+
+export interface AdminTrackingLink {
+ id: number
+ name: string
+ code: string
+ channel_type: string
+ target_url: string
+ short_url: string
+ status: string
+ cost_cent: number
+ notes: string
+ created_by: number
+ created_at: string
+ updated_at: string
+ click_pv: number
+ click_uv: number
+ click_ip: number
+ register_count: number
+ order_count: number
+ paid_order_count: number
+ paid_gmv_cent: number
+ conversion_rate: number
+}
+
+export interface AdminTrackingLinkPage {
+ items: AdminTrackingLink[]
+ total: number
+ page: number
+ page_size: number
+}
+
+export interface AdminTrackingLinkQuery {
+ keyword?: string
+ channel_type?: string
+ status?: string
+ page?: number
+ page_size?: number
+}
+
+export interface AdminTrackingLinkPayload {
+ name: string
+ code?: string
+ channel_type?: string
+ target_url?: string
+ cost_cent?: number
+ notes?: string
+}
+
+export interface AdminTrackingOverview {
+ total_links: number
+ active_links: number
+ total_pv: number
+ total_uv: number
+ total_ip: number
+ total_registers: number
+ total_orders: number
+ total_paid_orders: number
+ total_paid_gmv_cent: number
+ today_pv: number
+ today_uv: number
+ today_registers: number
+ today_orders: number
+ today_paid_gmv_cent: number
+}
+
+
+
diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx
index 458daf7..47f77ad 100644
--- a/frontend/src/layouts/AdminLayout.tsx
+++ b/frontend/src/layouts/AdminLayout.tsx
@@ -130,13 +130,17 @@ const allNavGroups: NavGroup[] = [
key: 'operations',
label: '运营',
icon: ,
- children: [{ key: '/announcements', label: '公告管理', permission: 'announcement:view' }],
+ children: [{ key: '/announcements', label: '公告管理', permission: 'announcement:view' }, { key: '/tracking-links', label: '推广追踪', permission: 'ad_tracking:manage' }],
},
{
key: 'support',
label: '客服',
icon: ,
- children: [{ key: '/chat-support-groups', label: '客服群组', permission: 'chat:manage' }],
+ children: [
+ { key: '/chat-support-groups', label: '客服群组', permission: 'chat:manage' },
+ { key: '/manual-entries', label: '手工录单', permission: 'cs_manual_entry:view' },
+ { key: '/chat-quick-replies', label: '快捷回复', permission: 'chat:view' },
+ ],
},
{
key: 'system',
@@ -145,6 +149,7 @@ const allNavGroups: NavGroup[] = [
children: [
{ key: '/system-configs', label: '系统配置', permission: 'system_config:view' },
{ key: '/roles', label: '角色权限', permission: 'role:manage' },
+ { key: '/admin-users', label: '管理员账号', permission: 'admin_user:manage' },
{ key: '/payment-configs', label: '支付配置', permission: 'payment_config:list' },
{ key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' },
],
@@ -177,6 +182,19 @@ const sectionMeta: Record = {
'/distribution/invitation': { title: '邀请返利', description: '配置分站邀请好友的返利比例。' },
'/announcements': { title: '公告管理', description: '维护主站与分站公告。' },
'/roles': { title: '角色权限', description: '维护后台角色并分配资源级操作权限。' },
+ '/admin-users': { title: '管理员账号', description: '维护后台账号、启用状态与角色绑定。' },
+ '/manual-entries': {
+ title: '手工录单',
+ description: '录入系统外成单编号,用于对账与客服绩效统计。',
+ },
+ '/chat-quick-replies': {
+ title: '快捷回复',
+ description: '维护客服会话常用话术与全局可见范围。',
+ },
+ '/tracking-links': {
+ title: '推广追踪',
+ description: '管理推广短链、渠道成本与注册下单转化。',
+ },
'/chat-support-groups': {
title: '客服群组',
description: '维护客服群与坐席分配,支撑会话分流。',