mall数据库文件

This commit is contained in:
comlibmb
2026-01-30 16:11:23 +08:00
parent b53d2376ff
commit cfec4a16c0
71 changed files with 11786 additions and 1009 deletions

View File

@@ -27,8 +27,19 @@
<view class="main-content">
<view class="container">
<!-- 报表列表 -->
<view class="report-list">
<!-- 顶部操作区:新建报表 -->
<view class="toolbar">
<view class="toolbar-left">
<text class="toolbar-title">我的自定义报表</text>
<text class="toolbar-subtitle">按需组合指标和时间范围,生成专属报表</text>
</view>
<view class="toolbar-right">
<button class="btn-primary" @click.stop="createReport"> 新建报表</button>
</view>
</view>
<!-- 报表列表 / 空状态 -->
<view v-if="reports.length > 0" class="report-list">
<view v-for="report in reports" :key="report.id" class="report-card" @click="openReport(report)">
<view class="report-header">
<text class="report-title">{{ report.name }}</text>
@@ -41,14 +52,21 @@
</view>
</view>
</view>
<text class="report-desc">{{ report.description }}</text>
<text class="report-desc">{{ report.description || '点击进入报表详情查看数据' }}</text>
<view class="report-meta">
<text class="meta-item">指标{{ report.metrics.length }}</text>
<text class="meta-item">图表{{ report.charts.length }}</text>
<text class="meta-item">更新:{{ report.updated_at }}</text>
<text class="meta-item">图表周期{{ report.period || '自定义' }}</text>
<text class="meta-item">最近更新{{ report.updated_at || '-' }}</text>
</view>
</view>
</view>
<view v-else class="empty-state">
<text v-if="isLoggedIn" class="empty-title">暂无自定义报表</text>
<text v-else class="empty-title">请先登录</text>
<text v-if="isLoggedIn" class="empty-desc">点击下方按钮创建第一份报表,用于复用常看的指标组合。</text>
<text v-else class="empty-desc">创建自定义报表需要登录账号,请先登录后再使用此功能。</text>
<button v-if="isLoggedIn" class="btn-primary" @click.stop="createReport"> 新建报表</button>
<button v-else class="btn-primary" @click.stop="goToLogin">前往登录</button>
</view>
<!-- 新建报表对话框 -->
<view class="modal" v-if="showCreateModal" @click.stop>
@@ -62,11 +80,23 @@
<view class="modal-body">
<view class="form-item">
<text class="form-label">报表名称</text>
<input class="form-input" v-model="reportForm.name" placeholder="请输入报表名称" />
<input
class="form-input"
v-model="reportForm.name"
placeholder="请输入报表名称1-50个字符"
@input="onNameInput"
/>
<text v-if="formErrors.name" class="form-error">{{ formErrors.name }}</text>
</view>
<view class="form-item">
<text class="form-label">报表描述</text>
<textarea class="form-textarea" v-model="reportForm.description" placeholder="请输入报表描述"></textarea>
<textarea
class="form-textarea"
v-model="reportForm.description"
placeholder="选填最多200个字符"
@input="onDescriptionInput"
></textarea>
<text v-if="formErrors.description" class="form-error">{{ formErrors.description }}</text>
</view>
<view class="form-item">
<text class="form-label">选择指标</text>
@@ -81,6 +111,7 @@
<text>{{ m.label }}</text>
</view>
</view>
<text v-if="formErrors.metrics" class="form-error">{{ formErrors.metrics }}</text>
</view>
<view class="form-item">
<text class="form-label">时间维度</text>
@@ -90,11 +121,12 @@
:key="p.value"
class="period-item"
:class="{ selected: reportForm.period === p.value }"
@click="reportForm.period = p.value"
@click="selectPeriod(p.value)"
>
<text>{{ p.label }}</text>
</view>
</view>
<text v-if="formErrors.period" class="form-error">{{ formErrors.period }}</text>
</view>
<view class="form-item">
<text class="form-label">图表类型</text>
@@ -104,11 +136,12 @@
:key="t.value"
class="chart-type-item"
:class="{ selected: reportForm.chartType === t.value }"
@click="reportForm.chartType = t.value"
@click="selectChartType(t.value)"
>
<text>{{ t.label }}</text>
</view>
</view>
<text v-if="formErrors.chartType" class="form-error">{{ formErrors.chartType }}</text>
</view>
</view>
<view class="modal-footer">
@@ -127,9 +160,13 @@
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
import { goToLogin } from '@/utils/utils.uts'
import { getUserIdOrNull } from '@/services/analytics/auth.uts'
import { listCustomReports, createCustomReport, updateCustomReport, deleteCustomReport } from '@/services/analytics/customReportService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
type Report = {
id: string
@@ -149,6 +186,13 @@ type ReportForm = {
period: string
chartType: string
}
type ReportFormErrors = {
name: string
description: string
metrics: string
period: string
chartType: string
}
export default {
components: {
@@ -164,6 +208,7 @@ export default {
editingReport: null as Report | null,
reports: [] as Array<Report>,
isLoggedIn: false,
reportForm: {
name: '',
@@ -172,6 +217,13 @@ export default {
period: '7d',
chartType: 'line'
} as ReportForm,
formErrors: {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
} as ReportFormErrors,
availableMetrics: [
{ key: 'gmv', label: 'GMV' },
@@ -210,7 +262,38 @@ export default {
methods: {
async loadReports() {
// TODO: 实现报表列表加载
try {
await ensureSupabaseReady()
// 获取当前登录用户,用于按 owner_user_id 过滤自定义报表
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
// 未登录时显示空列表
this.isLoggedIn = false
this.reports = []
return
}
this.isLoggedIn = true
const items = await listCustomReports(uid)
const list: Array<Report> = []
for (let i = 0; i < items.length; i++) {
const r = items[i]
list.push({
id: `${r.id}`,
name: `${r.title}`,
description: `${r.description || ''}`,
metrics: [] as Array<string>,
charts: [] as Array<string>,
updated_at: `${r.updated_at || ''}`
} as Report)
}
this.reports = list
} catch (e) {
console.error('loadReports failed', e)
uni.showToast({ title: '报表加载失败', icon: 'none' })
}
},
createReport() {
@@ -222,6 +305,13 @@ export default {
period: '7d',
chartType: 'line'
}
this.formErrors = {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
}
this.showCreateModal = true
},
@@ -234,6 +324,13 @@ export default {
period: '7d',
chartType: 'line'
}
this.formErrors = {
name: '',
description: '',
metrics: '',
period: '',
chartType: ''
}
this.showCreateModal = true
},
@@ -243,14 +340,26 @@ export default {
content: `确定要删除报表"${report.name}"吗?`,
success: (res) => {
if (res.confirm) {
// TODO: 实现删除逻辑
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadReports()
this.doDeleteReport(report)
}
}
})
},
async doDeleteReport(report: Report) {
try {
await ensureSupabaseReady()
await deleteCustomReport(report.id)
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadReports()
} catch (e: any) {
console.error('doDeleteReport failed', e)
const errorMsg = e?.message || '删除失败'
uni.showToast({ title: errorMsg, icon: 'none' })
}
},
toggleMetric(key: string) {
const index = this.reportForm.metrics.indexOf(key)
if (index >= 0) {
@@ -258,21 +367,133 @@ export default {
} else {
this.reportForm.metrics.push(key)
}
if (this.reportForm.metrics.length > 0) {
this.formErrors.metrics = ''
}
},
saveReport() {
if (!this.reportForm.name.trim()) {
uni.showToast({ title: '请输入报表名称', icon: 'none' })
return
onNameInput() {
const name = this.reportForm.name.trim()
if (name.length === 0) {
this.formErrors.name = '报表名称不能为空'
} else if (name.length > 50) {
this.formErrors.name = '报表名称不能超过50个字符'
} else {
this.formErrors.name = ''
}
},
onDescriptionInput() {
const desc = this.reportForm.description
if (desc.length > 200) {
this.formErrors.description = '报表描述不能超过200个字符'
} else {
this.formErrors.description = ''
}
},
selectPeriod(value: string) {
this.reportForm.period = value
this.formErrors.period = ''
},
selectChartType(value: string) {
this.reportForm.chartType = value
this.formErrors.chartType = ''
},
validateReportForm(): boolean {
this.onNameInput()
this.onDescriptionInput()
if (this.reportForm.metrics.length === 0) {
uni.showToast({ title: '请至少选择一个指标', icon: 'none' })
this.formErrors.metrics = '请至少选择一个指标'
} else {
this.formErrors.metrics = ''
}
if (!this.reportForm.period) {
this.formErrors.period = '请选择时间维度'
}
if (!this.reportForm.chartType) {
this.formErrors.chartType = '请选择图表类型'
}
if (this.formErrors.name || this.formErrors.description || this.formErrors.metrics || this.formErrors.period || this.formErrors.chartType) {
uni.showToast({ title: '请先修正表单中的错误提示', icon: 'none' })
return false
}
return true
},
async saveReport() {
if (!this.validateReportForm()) {
return
}
// TODO: 实现保存逻辑
try {
uni.showLoading({ title: '保存中...' })
await ensureSupabaseReady()
// 获取当前登录用户,作为 owner_user_id
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
uni.hideLoading()
uni.showModal({
title: '需要登录',
content: '创建自定义报表需要先登录,是否前往登录页面?',
success: (res) => {
if (res.confirm) {
goToLogin('/pages/mall/analytics/custom-report')
}
}
})
return
}
let newReportId = ''
// 1) 创建或更新自定义报表
if (this.editingReport == null) {
newReportId = await createCustomReport({
title: this.reportForm.name,
description: this.reportForm.description || '',
period: this.reportForm.period,
metrics: this.reportForm.metrics,
chartType: this.reportForm.chartType || 'line'
})
} else {
await updateCustomReport({
reportId: this.editingReport.id,
title: this.reportForm.name,
description: this.reportForm.description || null,
period: this.reportForm.period || null
})
newReportId = this.editingReport.id
}
uni.hideLoading()
uni.showToast({ title: '保存成功', icon: 'success' })
this.closeModal()
this.loadReports()
// 新建或编辑成功后,直接进入报表详情页,给用户明确反馈
if (newReportId.length > 0) {
setTimeout(() => {
uni.navigateTo({
url: `/pages/mall/analytics/report-detail?reportId=${newReportId}`
})
}, 400)
}
} catch (e: any) {
uni.hideLoading()
console.error('saveReport exception:', e)
uni.showToast({
title: mapAnalyticsError(e, { fallbackMessage: '保存失败' }),
icon: 'none',
duration: 3000
})
}
},
openReport(report: Report) {
@@ -286,6 +507,10 @@ export default {
this.editingReport = null
},
goToLogin() {
goToLogin('/pages/mall/analytics/custom-report')
},
refreshData() {
this.loadReports()
uni.showToast({ title: '已刷新', icon: 'success' })
@@ -489,6 +714,57 @@ export default {
color: #111;
}
/* 工具栏 */
.toolbar {
margin-top: 12px;
padding: 12px 16px;
background: #fff;
border-radius: 14px;
border: 1px solid rgba(0,0,0,0.06);
display: flex;
flex-direction: row !important;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.toolbar-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.toolbar-title {
font-size: 14px;
font-weight: 600;
color: #111;
}
.toolbar-subtitle {
font-size: 12px;
color: rgba(0,0,0,0.55);
}
.toolbar-right {
display: flex;
flex-direction: row !important;
align-items: center;
gap: 8px;
}
.btn-primary {
padding: 8px 16px;
border-radius: 999px;
border: none;
background: #111827;
color: #fff;
font-size: 14px;
}
.btn-primary:active {
opacity: 0.9;
}
/* 报表列表 */
.report-list {
margin-top: 12px;
@@ -570,6 +846,31 @@ export default {
color: rgba(0,0,0,0.45);
}
.empty-state {
margin-top: 24px;
padding: 32px 16px;
background: #fff;
border-radius: 14px;
border: 1px solid rgba(0,0,0,0.06);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: #111;
}
.empty-desc {
font-size: 13px;
color: rgba(0,0,0,0.55);
text-align: center;
}
/* 模态框 */
.modal {
position: fixed;
@@ -644,6 +945,12 @@ export default {
margin-bottom: 8px;
}
.form-error {
margin-top: 4px;
font-size: 12px;
color: #dc2626;
}
.form-input,
.form-textarea {
width: 100%;