修复bug
This commit is contained in:
@@ -106,6 +106,7 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,10 @@
|
|||||||
<view class="content-scroll">
|
<view class="content-scroll">
|
||||||
<view class="content-inner" :class="{ 'is-mobile': isMobile }">
|
<view class="content-inner" :class="{ 'is-mobile': isMobile }">
|
||||||
<slot v-if="hasAccess"></slot>
|
<slot v-if="hasAccess"></slot>
|
||||||
<component :is="currentComponent" v-if="hasAccess && !isPageLoading && currentComponent != null"></component>
|
<!-- currentPage 为空字符串说明是顶层路由容器,才允许渲染动态组件;
|
||||||
|
若 currentPage 非空,说明是子页面自己套了 AdminLayout(作为布局壳使用),
|
||||||
|
此时不再渲染 currentComponent,避免递归挂载导致重复请求。 -->
|
||||||
|
<component :is="currentComponent" v-if="hasAccess && !isPageLoading && currentComponent != null && props.currentPage === ''"></component>
|
||||||
<AdminPageLoading v-if="isPageLoading"></AdminPageLoading>
|
<AdminPageLoading v-if="isPageLoading"></AdminPageLoading>
|
||||||
</view>
|
</view>
|
||||||
<AdminFooter></AdminFooter>
|
<AdminFooter></AdminFooter>
|
||||||
|
|||||||
@@ -1,270 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="admin-decoration-category">
|
|
||||||
<!-- 顶部标题与按钮 -->
|
|
||||||
<view class="page-header border-shadow">
|
|
||||||
<view class="header-left">
|
|
||||||
<text class="page-title">商品分类装修</text>
|
|
||||||
</view>
|
|
||||||
<view class="header-right">
|
|
||||||
<view class="btn-primary" @click="handleSave">
|
|
||||||
<text class="btn-txt">{{ isSaving ? '保存中...' : '保存配置' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="btn-ghost" @click="handleReset">
|
|
||||||
<text class="ghost-txt">重置</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 分类展示区域 -->
|
|
||||||
<view class="content-container">
|
|
||||||
<view v-if="isLoading" class="loading-state">
|
|
||||||
<text>加载配置中...</text>
|
|
||||||
</view>
|
|
||||||
<view v-else class="style-list">
|
|
||||||
|
|
||||||
<!-- 样式1 -->
|
|
||||||
<view class="style-card-wrapper">
|
|
||||||
<view :class="['style-card', selectedStyle === 1 ? 'active' : '']" @click="selectedStyle = 1">
|
|
||||||
<view class="phone-mock">
|
|
||||||
<view class="phone-header">
|
|
||||||
<text class="p-title">产品分类</text>
|
|
||||||
<text class="p-dots">••• Ⓞ</text>
|
|
||||||
</view>
|
|
||||||
<view class="phone-body">
|
|
||||||
<view class="search-bar">
|
|
||||||
<text class="ic-search">🔍</text>
|
|
||||||
<text class="search-ph">点击搜索商品信息</text>
|
|
||||||
</view>
|
|
||||||
<view class="style1-content">
|
|
||||||
<view class="sidebar-mock">
|
|
||||||
<text class="sb-item active">精选水果</text>
|
|
||||||
<text class="sb-item" v-for="name in ['肉制品','水产海鲜','米面粮油','厨房主食','新鲜蛋品','调味品','日配冷藏','豆制品']" :key="name">{{name}}</text>
|
|
||||||
</view>
|
|
||||||
<view class="main-mock">
|
|
||||||
<view class="category-section">
|
|
||||||
<view class="section-title"><text class="st-txt">精选水果</text></view>
|
|
||||||
<view class="grid-container">
|
|
||||||
<view class="grid-item" v-for="i in 6" :key="i">
|
|
||||||
<view class="item-img-box"><text class="item-placeholder">🍐</text></view>
|
|
||||||
<text class="item-txt">{{ ['精品香蕉','坚果优选','猕猴桃','大肉块','五花肉','鸡腿'][i-1] }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="phone-tabbar">
|
|
||||||
<view class="tb-item"><text class="tb-ic">🏠</text></view>
|
|
||||||
<view class="tb-item active"><text class="tb-ic">📂</text></view>
|
|
||||||
<view class="tb-item"><text class="tb-ic">👤</text></view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="style-name" :style="{color: selectedStyle === 1 ? '#2d8cf0' : '#666'}">样式1</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 样式2 -->
|
|
||||||
<view class="style-card-wrapper">
|
|
||||||
<view :class="['style-card', selectedStyle === 2 ? 'active' : '']" @click="selectedStyle = 2">
|
|
||||||
<view class="phone-mock">
|
|
||||||
<view class="phone-header-img"></view>
|
|
||||||
<view class="phone-header-v2">
|
|
||||||
<text class="p2-title">分类</text>
|
|
||||||
<view class="home-ic">🏠</view>
|
|
||||||
</view>
|
|
||||||
<view class="phone-body p2-body">
|
|
||||||
<view class="search-bar-v2">
|
|
||||||
<text class="ic-search">🔍</text>
|
|
||||||
<text class="search-ph">点击搜索商品信息</text>
|
|
||||||
</view>
|
|
||||||
<view class="style2-content">
|
|
||||||
<view class="sidebar-v2">
|
|
||||||
<text class="s2-item active">乳品</text>
|
|
||||||
<text class="s2-item" v-for="n in 5" :key="n">分类{{n}}</text>
|
|
||||||
</view>
|
|
||||||
<view class="main-v2">
|
|
||||||
<view class="banner-mock-v2">
|
|
||||||
<text class="b-txt">深层 V8 高清直屏\n双镜头/VR科技体验</text>
|
|
||||||
</view>
|
|
||||||
<view class="prod-v2" v-for="i in 2" :key="i">
|
|
||||||
<text class="p-name">精选爆款商品标题示例内容展示</text>
|
|
||||||
<view class="p-price-row">
|
|
||||||
<text class="p-price">¥99.00</text>
|
|
||||||
<view class="btn-buy"><text class="buy-txt">立即购买</text></view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="style-name" :style="{color: selectedStyle === 2 ? '#2d8cf0' : '#666'}">样式2</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 样式3 -->
|
|
||||||
<view class="style-card-wrapper">
|
|
||||||
<view :class="['style-card', selectedStyle === 3 ? 'active' : '']" @click="selectedStyle = 3">
|
|
||||||
<view class="phone-mock">
|
|
||||||
<view class="phone-header">
|
|
||||||
<text class="p-title">产品分类</text>
|
|
||||||
</view>
|
|
||||||
<view class="phone-body">
|
|
||||||
<view class="search-bar-v3">
|
|
||||||
<view class="home-btn">🏠</view>
|
|
||||||
<view class="search-input-v3">
|
|
||||||
<text class="ic-search">🔍</text>
|
|
||||||
<text class="search-ph">搜索商品</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="style3-content">
|
|
||||||
<view class="sidebar-v3">
|
|
||||||
<text class="s3-item active">乳品</text>
|
|
||||||
<text class="s3-item" v-for="n in 5" :key="n">分类{{n}}</text>
|
|
||||||
</view>
|
|
||||||
<view class="main-v3">
|
|
||||||
<view class="prod-v3" v-for="i in 5" :key="i">
|
|
||||||
<view class="pv-img"></view>
|
|
||||||
<view class="pv-info">
|
|
||||||
<text class="pv-name">优质精选商品名称展示示例</text>
|
|
||||||
<text class="pv-price">¥25.99</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="style-name" :style="{color: selectedStyle === 3 ? '#2d8cf0' : '#666'}">样式3</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="uts">
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { getActiveDiyConfig, saveDiyPage, type DiyPage } from '@/services/admin/decorationService.uts'
|
|
||||||
|
|
||||||
const selectedStyle = ref(1)
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const isSaving = ref(false)
|
|
||||||
const currentPageId = ref<string | null>(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadConfig()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadConfig() {
|
|
||||||
isLoading.value = true
|
|
||||||
try {
|
|
||||||
const config = await getActiveDiyConfig('category')
|
|
||||||
if (config != null) {
|
|
||||||
currentPageId.value = config.id
|
|
||||||
const style = config.config.getNumber('style')
|
|
||||||
if (style != null) {
|
|
||||||
selectedStyle.value = style.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load category decoration config', e)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
isSaving.value = true
|
|
||||||
try {
|
|
||||||
const config = { style: selectedStyle.value } as UTSJSONObject
|
|
||||||
const id = await saveDiyPage(currentPageId.value, '商品分类默认配置', 'category', config, true)
|
|
||||||
if (id != null) {
|
|
||||||
currentPageId.value = id
|
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
|
||||||
} finally {
|
|
||||||
isSaving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
selectedStyle.value = 1
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.admin-decoration-category { background-color: #f0f2f5; min-height: 100vh; display: flex; flex-direction: column; }
|
|
||||||
.border-shadow { background-color: #fff; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
|
||||||
.page-header { height: 60px; padding: 0 24px; display: flex; flex-direction: row; justify-content: space-between; align-items: center; z-index: 10; }
|
|
||||||
.page-title { font-size: 16px; font-weight: 600; color: #303133; }
|
|
||||||
.header-right { display: flex; flex-direction: row; gap: 12px; }
|
|
||||||
.btn-primary, .btn-ghost { height: 32px; padding: 0 20px; border-radius: 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
|
||||||
.btn-primary { background-color: #2d8cf0; }
|
|
||||||
.btn-ghost { border: 1px solid #dcdfe6; position: relative; }
|
|
||||||
.btn-txt { color: #fff; font-size: 14px; }
|
|
||||||
.ghost-txt { color: #606266; font-size: 14px; }
|
|
||||||
.content-container { flex: 1; padding: 30px 40px; }
|
|
||||||
.loading-state { padding: 100px; text-align: center; color: #999; }
|
|
||||||
|
|
||||||
.style-list { display: flex; flex-direction: row; gap: 30px; flex-wrap: wrap; }
|
|
||||||
.style-card-wrapper { display: flex; flex-direction: column; align-items: center; gap: 15px; }
|
|
||||||
.style-card { width: 300px; height: 600px; background-color: #fff; border: 2px solid transparent; border-radius: 20px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1); cursor: pointer; transition: all 0.3s; }
|
|
||||||
.style-card.active { border-color: #2d8cf0; box-shadow: 0 4px 20px rgba(45, 140, 240, 0.2); }
|
|
||||||
.style-name { font-size: 14px; font-weight: bold; }
|
|
||||||
|
|
||||||
/* Phone Mockup Common */
|
|
||||||
.phone-mock { width: 100%; height: 100%; display: flex; flex-direction: column; background-color: #fff; }
|
|
||||||
.phone-header { height: 44px; display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 0 15px; border-bottom: 1px solid #eee; }
|
|
||||||
.p-title { font-size: 14px; font-weight: bold; color: #333; }
|
|
||||||
.p-dots { font-size: 12px; color: #333; }
|
|
||||||
.phone-body { flex: 1; background-color: #f8f8f8; display: flex; flex-direction: column; overflow: hidden; }
|
|
||||||
|
|
||||||
/* Styles logic */
|
|
||||||
.style1-content, .style2-content, .style3-content { flex: 1; display: flex; flex-direction: row; }
|
|
||||||
.sidebar-mock, .sidebar-v2, .sidebar-v3 { width: 70px; background-color: #f7f7f7; display: flex; flex-direction: column; }
|
|
||||||
.sb-item, .s2-item, .s3-item { height: 44px; display: flex; align-items: center; justify-content: center; font-size: 11px; color: #666; }
|
|
||||||
.sb-item.active, .s2-item.active, .s3-item.active { background-color: #fff; color: #f2270c; font-weight: bold; position: relative; }
|
|
||||||
.sb-item.active::before, .s3-item.active::before { content: ''; position: absolute; left: 0; top: 15px; height: 14px; width: 3px; background-color: #f2270c; }
|
|
||||||
|
|
||||||
.main-mock, .main-v2, .main-v3 { flex: 1; background-color: #fff; padding: 10px; }
|
|
||||||
.search-bar, .search-bar-v2, .search-bar-v3 { height: 32px; background-color: #f2f2f2; margin: 10px; border-radius: 16px; display: flex; flex-direction: row; align-items: center; padding: 0 12px; }
|
|
||||||
.ic-search { font-size: 12px; margin-right: 6px; color: #999; }
|
|
||||||
.search-ph { font-size: 10px; color: #999; }
|
|
||||||
|
|
||||||
.category-section { margin-bottom: 10px; }
|
|
||||||
.st-txt { font-size: 12px; font-weight: bold; color: #333; }
|
|
||||||
.grid-container { display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; }
|
|
||||||
.grid-item { width: 60px; display: flex; flex-direction: column; align-items: center; margin-bottom: 10px; }
|
|
||||||
.item-img-box { width: 44px; height: 44px; background-color: #f5f5f5; border-radius: 4px; display: flex; align-items: center; justify-content: center; margin-bottom: 4px; }
|
|
||||||
.item-txt { font-size: 9px; color: #666; text-align: center; }
|
|
||||||
|
|
||||||
.phone-tabbar { height: 48px; display: flex; flex-direction: row; border-top: 1px solid #eee; }
|
|
||||||
.tb-item { flex: 1; display: flex; align-items: center; justify-content: center; font-size: 18px; color: #999; }
|
|
||||||
.tb-item.active { color: #f2270c; }
|
|
||||||
|
|
||||||
.banner-mock-v2 { height: 70px; background-color: #0081ff; border-radius: 6px; padding: 10px; display: flex; align-items: center; margin-bottom: 10px; }
|
|
||||||
.b-txt { font-size: 10px; color: #fff; font-weight: bold; }
|
|
||||||
.prod-v2 { border-bottom: 1px solid #f5f5f5; padding-bottom: 8px; margin-bottom: 8px; }
|
|
||||||
.p-name { font-size: 10px; color: #333; line-height: 1.3; }
|
|
||||||
.p-price { font-size: 11px; color: #f2270c; font-weight: bold; }
|
|
||||||
.btn-buy { background-color: #f2270c; padding: 3px 8px; border-radius: 10px; }
|
|
||||||
.buy-txt { font-size: 8px; color: #fff; }
|
|
||||||
|
|
||||||
.prod-v3 { display: flex; flex-direction: row; margin-bottom: 10px; }
|
|
||||||
.pv-img { width: 60px; height: 60px; background-color: #f5f5f5; border-radius: 4px; margin-right: 8px; }
|
|
||||||
.pv-name { font-size: 10px; color: #333; }
|
|
||||||
.pv-price { font-size: 12px; color: #f2270c; font-weight: bold; }
|
|
||||||
|
|
||||||
.settings-panel { flex: 1; padding: 30px; }
|
|
||||||
.group-title { display: flex; flex-direction: row; align-items: center; margin-bottom: 20px; }
|
|
||||||
.title-line { width: 3px; height: 16px; background-color: #2d8cf0; margin-right: 10px; }
|
|
||||||
.title-txt { font-size: 15px; font-weight: bold; }
|
|
||||||
.radio-group { display: flex; flex-direction: column; gap: 15px; }
|
|
||||||
.radio-item { display: flex; flex-direction: row; align-items: center; cursor: pointer; }
|
|
||||||
.radio-dot { width: 16px; height: 16px; border: 1px solid #dcdfe6; border-radius: 8px; margin-right: 10px; }
|
|
||||||
.radio-dot.active { border-color: #2d8cf0; background-color: #2d8cf0; }
|
|
||||||
.radio-txt { font-size: 14px; color: #333; }
|
|
||||||
.hint-txt { font-size: 12px; color: #999; margin-top: 20px; line-height: 1.6; }
|
|
||||||
</style>
|
|
||||||
@@ -281,31 +281,35 @@
|
|||||||
❌ select() 调用没有 limit/offset 参数
|
❌ select() 调用没有 limit/offset 参数
|
||||||
```
|
```
|
||||||
- **正确实现(服务端分页模板)**:
|
- **正确实现(服务端分页模板)**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ state
|
// ✅ state
|
||||||
const total = ref(0) // 来自服务端,不是 .length
|
const total = ref(0); // 来自服务端,不是 .length
|
||||||
const pagedList = computed(() => userList.value) // 无前端 slice,服务端已分页
|
const pagedList = computed(() => userList.value); // 无前端 slice,服务端已分页
|
||||||
|
|
||||||
// ✅ fetch:每次翻页都重新请求
|
// ✅ fetch:每次翻页都重新请求
|
||||||
const fetchUsers = async (page: number, ps: number) => {
|
const fetchUsers = async (page: number, ps: number) => {
|
||||||
const offset = (page - 1) * ps
|
const offset = (page - 1) * ps;
|
||||||
const offsetFilter = offset > 0 ? `offset=${offset}` : null
|
const offsetFilter = offset > 0 ? `offset=${offset}` : null;
|
||||||
const res = await supabase.select('table', offsetFilter, {
|
const res = await supabase.select("table", offsetFilter, {
|
||||||
limit: ps, order: 'created_at.desc', count: 'exact'
|
limit: ps,
|
||||||
})
|
order: "created_at.desc",
|
||||||
userList.value = (res.data as UTSJSONObject[]).map(mapDbRow)
|
count: "exact",
|
||||||
|
});
|
||||||
|
userList.value = (res.data as UTSJSONObject[]).map(mapDbRow);
|
||||||
// 从 Content-Range 头解析总数(见原因二十八)
|
// 从 Content-Range 头解析总数(见原因二十八)
|
||||||
total.value = parseTotalFromContentRange(res)
|
total.value = parseTotalFromContentRange(res);
|
||||||
}
|
};
|
||||||
|
|
||||||
// ✅ 翻页 handler:每次翻页都调用 fetch
|
// ✅ 翻页 handler:每次翻页都调用 fetch
|
||||||
const handlePageChange = (p: number) => {
|
const handlePageChange = (p: number) => {
|
||||||
currentPage.value = p
|
currentPage.value = p;
|
||||||
fetchUsers(p, pageSize.value)
|
fetchUsers(p, pageSize.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
onMounted(() => fetchUsers(1, pageSize.value))
|
onMounted(() => fetchUsers(1, pageSize.value));
|
||||||
```
|
```
|
||||||
|
|
||||||
- **强制规则**: 任何使用 Supabase 的列表页面,`total` 必须来自后端 `Content-Range` 响应头或 `res.count`,严禁使用 `computed(() => localArray.value.length)` 作为总数。
|
- **强制规则**: 任何使用 Supabase 的列表页面,`total` 必须来自后端 `Content-Range` 响应头或 `res.count`,严禁使用 `computed(() => localArray.value.length)` 作为总数。
|
||||||
|
|
||||||
#### **原因二十七(补充):已在另一位置记录(响应式预览网格)**
|
#### **原因二十七(补充):已在另一位置记录(响应式预览网格)**
|
||||||
@@ -321,6 +325,7 @@
|
|||||||
3. UI 层的"禁用"保护(`if (p > totalPage) return`)在 `total` 来自错误来源时本身就不可靠(见原因二十六),因此无法阻止越界请求。
|
3. UI 层的"禁用"保护(`if (p > totalPage) return`)在 `total` 来自错误来源时本身就不可靠(见原因二十六),因此无法阻止越界请求。
|
||||||
4. 即使 UI 保护正确,在数据量恰好是页大小整数倍时(如共 15 条、每页 15 条),请求第 2 页仍会触发 416(`rangeFrom=15 >= total=15`)。
|
4. 即使 UI 保护正确,在数据量恰好是页大小整数倍时(如共 15 条、每页 15 条),请求第 2 页仍会触发 416(`rangeFrom=15 >= total=15`)。
|
||||||
- **错误修复思路(踩坑路径)**:
|
- **错误修复思路(踩坑路径)**:
|
||||||
|
|
||||||
```
|
```
|
||||||
❌ 第一轮尝试:在 handlePageChange / onPageBtnClick 中加 if (p > totalPage) return
|
❌ 第一轮尝试:在 handlePageChange / onPageBtnClick 中加 if (p > totalPage) return
|
||||||
结果:仍然 416。因为 UI 保护只能拦截按钮点击,无法修复协议层行为。
|
结果:仍然 416。因为 UI 保护只能拦截按钮点击,无法修复协议层行为。
|
||||||
@@ -328,44 +333,48 @@
|
|||||||
❌ 第二轮尝试:在 fetchUsers 中加 if (res.status === 416) { ... } 分支处理
|
❌ 第二轮尝试:在 fetchUsers 中加 if (res.status === 416) { ... } 分支处理
|
||||||
结果:错误被吞但数据仍为空,用户体验差,根本原因未消除。
|
结果:错误被吞但数据仍为空,用户体验差,根本原因未消除。
|
||||||
```
|
```
|
||||||
|
|
||||||
- **正确修复方案**:
|
- **正确修复方案**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ 禁止使用 Range 头分页
|
// ❌ 禁止使用 Range 头分页
|
||||||
const res = await supabase
|
const res = await supabase
|
||||||
.from('ak_users')
|
.from("ak_users")
|
||||||
.select(columns)
|
.select(columns)
|
||||||
.page(page) // ← 产生 Range 头
|
.page(page) // ← 产生 Range 头
|
||||||
.limit(pageSize) // ← 产生 Range 头
|
.limit(pageSize) // ← 产生 Range 头
|
||||||
.execute()
|
.execute();
|
||||||
|
|
||||||
// ✅ 使用 URL 参数分页(永远不返回 416)
|
// ✅ 使用 URL 参数分页(永远不返回 416)
|
||||||
const offset = (page - 1) * ps
|
const offset = (page - 1) * ps;
|
||||||
const offsetFilter = offset > 0 ? `offset=${offset}` : null
|
const offsetFilter = offset > 0 ? `offset=${offset}` : null;
|
||||||
const res = await supabase.select('ak_users', offsetFilter, {
|
const res = await supabase.select("ak_users", offsetFilter, {
|
||||||
columns: 'id, username, ...',
|
columns: "id, username, ...",
|
||||||
limit: ps,
|
limit: ps,
|
||||||
order: 'created_at.desc',
|
order: "created_at.desc",
|
||||||
count: 'exact' // → Prefer: count=exact → 响应包含 Content-Range: 0-14/26
|
count: "exact", // → Prefer: count=exact → 响应包含 Content-Range: 0-14/26
|
||||||
})
|
});
|
||||||
// PostgREST 对 ?offset=N&limit=N 参数:offset 超出 total 时返回 200 + 空数组,绝不 416
|
// PostgREST 对 ?offset=N&limit=N 参数:offset 超出 total 时返回 200 + 空数组,绝不 416
|
||||||
```
|
```
|
||||||
|
|
||||||
- **总数解析(Content-Range 手动解析)**:
|
- **总数解析(Content-Range 手动解析)**:
|
||||||
```typescript
|
```typescript
|
||||||
// 当 aksupa 使用 count: 'exact' 时,响应头会包含 Content-Range: 0-14/26
|
// 当 aksupa 使用 count: 'exact' 时,响应头会包含 Content-Range: 0-14/26
|
||||||
// 必须手动解析,因为绕过了 .page().limit() 的自动汇总逻辑
|
// 必须手动解析,因为绕过了 .page().limit() 的自动汇总逻辑
|
||||||
const parseTotal = (res: AkReqResponse): number => {
|
const parseTotal = (res: AkReqResponse): number => {
|
||||||
const cr = (res.headers?.['content-range'] ?? res.headers?.['Content-Range']) as string | null
|
const cr = (res.headers?.["content-range"] ??
|
||||||
|
res.headers?.["Content-Range"]) as string | null;
|
||||||
if (cr) {
|
if (cr) {
|
||||||
const slash = cr.lastIndexOf('/')
|
const slash = cr.lastIndexOf("/");
|
||||||
if (slash !== -1) {
|
if (slash !== -1) {
|
||||||
const n = parseInt(cr.substring(slash + 1), 10)
|
const n = parseInt(cr.substring(slash + 1), 10);
|
||||||
if (!isNaN(n)) return n
|
if (!isNaN(n)) return n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 降级:至少知道 offset + 当前页行数
|
// 降级:至少知道 offset + 当前页行数
|
||||||
return offset + (res.data as UTSJSONObject[]).length
|
return offset + (res.data as UTSJSONObject[]).length;
|
||||||
}
|
};
|
||||||
total.value = parseTotal(res)
|
total.value = parseTotal(res);
|
||||||
```
|
```
|
||||||
- **强制规则**:
|
- **强制规则**:
|
||||||
1. **项目内所有列表页禁止使用 `.page().limit()` 链式调用**(aksupa 或其他类似封装)。
|
1. **项目内所有列表页禁止使用 `.page().limit()` 链式调用**(aksupa 或其他类似封装)。
|
||||||
@@ -378,28 +387,30 @@
|
|||||||
- **现象**: 在第 1 页时点击"上一页"按钮,或在最后一页时点击"下一页"按钮,虽然按钮视觉上呈灰色/禁用态,但仍会向父组件 `emit('page-change', 0)` 或 `emit('page-change', totalPage+1)`,导致 `fetchUsers(0, ps)` 被调用、产生 `offset=-ps`(负数),请求异常。
|
- **现象**: 在第 1 页时点击"上一页"按钮,或在最后一页时点击"下一页"按钮,虽然按钮视觉上呈灰色/禁用态,但仍会向父组件 `emit('page-change', 0)` 或 `emit('page-change', totalPage+1)`,导致 `fetchUsers(0, ps)` 被调用、产生 `offset=-ps`(负数),请求异常。
|
||||||
- **根本原因**: `disabled` class 仅控制 CSS 样式(`opacity`、`cursor: not-allowed`),没有在事件处理函数中加入边界检查,点击事件照常触发和冒泡。
|
- **根本原因**: `disabled` class 仅控制 CSS 样式(`opacity`、`cursor: not-allowed`),没有在事件处理函数中加入边界检查,点击事件照常触发和冒泡。
|
||||||
- **修复方案**:
|
- **修复方案**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// CommonPagination.uvue — onPageBtnClick
|
// CommonPagination.uvue — onPageBtnClick
|
||||||
// ❌ 修复前:直接 emit,无边界保护
|
// ❌ 修复前:直接 emit,无边界保护
|
||||||
const onPageBtnClick = (p: number) => {
|
const onPageBtnClick = (p: number) => {
|
||||||
if (p !== -1) emit('page-change', p)
|
if (p !== -1) emit("page-change", p);
|
||||||
}
|
};
|
||||||
|
|
||||||
// ✅ 修复后:加入 p >= 1 && p <= props.totalPage 双向边界检查
|
// ✅ 修复后:加入 p >= 1 && p <= props.totalPage 双向边界检查
|
||||||
const onPageBtnClick = (p: number) => {
|
const onPageBtnClick = (p: number) => {
|
||||||
if (p !== -1 && p >= 1 && p <= props.totalPage) {
|
if (p !== -1 && p >= 1 && p <= props.totalPage) {
|
||||||
emit('page-change', p)
|
emit("page-change", p);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
- **父页面的双重保护(纵深防御)**:
|
- **父页面的双重保护(纵深防御)**:
|
||||||
```typescript
|
```typescript
|
||||||
// 在 handlePageChange 中同样加入边界检查,防止其他调用路径绕过组件保护
|
// 在 handlePageChange 中同样加入边界检查,防止其他调用路径绕过组件保护
|
||||||
const handlePageChange = (p: number) => {
|
const handlePageChange = (p: number) => {
|
||||||
if (p < 1 || p > totalPage.value) return
|
if (p < 1 || p > totalPage.value) return;
|
||||||
currentPage.value = p
|
currentPage.value = p;
|
||||||
fetchUsers(p, pageSize.value)
|
fetchUsers(p, pageSize.value);
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
- **推广规则**:
|
- **推广规则**:
|
||||||
1. 任何分页组件,`disabled` 状态必须同时在 CSS **和**事件处理函数两层保护,视觉禁用 ≠ 逻辑禁用。
|
1. 任何分页组件,`disabled` 状态必须同时在 CSS **和**事件处理函数两层保护,视觉禁用 ≠ 逻辑禁用。
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="finance-balance-record">
|
|
||||||
<!-- 筛选卡片 -->
|
|
||||||
<view class="filter-card border-shadow">
|
|
||||||
<view class="filter-row">
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 列表表格 -->
|
|
||||||
<view class="table-container border-shadow">
|
|
||||||
<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-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>
|
|
||||||
|
|
||||||
<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.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-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>
|
|
||||||
</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, onMounted, computed } from 'vue'
|
|
||||||
import { fetchUserBillList } from '@/services/admin/financeService.uts'
|
|
||||||
import { UserBillRecord } from '@/types/admin/finance.uts'
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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">
|
|
||||||
.finance-balance-record {
|
|
||||||
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: 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 { 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;
|
|
||||||
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: #e6f0ff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th {
|
|
||||||
padding: 12px 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 { width: 200px; justify-content: flex-start; }
|
|
||||||
.col-time { width: 160px; }
|
|
||||||
.col-amount { width: 120px; }
|
|
||||||
.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: #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>
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="finance-commission">
|
|
||||||
<!-- 筛选卡片 -->
|
|
||||||
<view class="filter-card border-shadow">
|
|
||||||
<view class="filter-row">
|
|
||||||
<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="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>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 列表表格 -->
|
|
||||||
<view class="table-card border-shadow">
|
|
||||||
<view class="action-bar">
|
|
||||||
<view class="btn-export">
|
|
||||||
<text class="export-txt">导出</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<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-type"><text class="th-txt">业务类型</text></view>
|
|
||||||
<view class="th col-remark"><text class="th-txt">备注</text></view>
|
|
||||||
</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 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">
|
|
||||||
<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, onMounted, computed } from 'vue'
|
|
||||||
import { fetchUserBillList } from '@/services/admin/financeService.uts'
|
|
||||||
import { UserBillRecord } from '@/types/admin/finance.uts'
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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">
|
|
||||||
.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: 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 { flex: 1; height: 36px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 14px; }
|
|
||||||
|
|
||||||
.data-select { width: 160px; }
|
|
||||||
|
|
||||||
.btn-query {
|
|
||||||
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-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-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; }
|
|
||||||
|
|
||||||
.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>
|
|
||||||
@@ -201,8 +201,10 @@ async function loadData() {
|
|||||||
const endTime = new Date().toISOString()
|
const endTime = new Date().toISOString()
|
||||||
const startTime = getStartTime()
|
const startTime = getStartTime()
|
||||||
|
|
||||||
try {
|
// 各区块独立 try-catch,单个接口失败不影响其他数据展示
|
||||||
|
|
||||||
// 1. 获取财务概况
|
// 1. 获取财务概况
|
||||||
|
try {
|
||||||
const finRes = await fetchFinanceOverview(startTime, endTime)
|
const finRes = await fetchFinanceOverview(startTime, endTime)
|
||||||
if (finRes != null) {
|
if (finRes != null) {
|
||||||
stats.rechargeAmount = finRes.recharge_amount.toFixed(2)
|
stats.rechargeAmount = finRes.recharge_amount.toFixed(2)
|
||||||
@@ -212,8 +214,12 @@ async function loadData() {
|
|||||||
stats.balancePay = finRes.total_user_balance.toFixed(2)
|
stats.balancePay = finRes.total_user_balance.toFixed(2)
|
||||||
stats.commissionPay = finRes.total_user_brokerage.toFixed(2)
|
stats.commissionPay = finRes.total_user_brokerage.toFixed(2)
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[finance-stats] 财务概况加载失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 获取订单统计
|
// 2. 获取订单统计(与订单统计页共用同一 RPC,此处独立请求)
|
||||||
|
try {
|
||||||
const orderRes = await fetchOrderStats(startTime, endTime)
|
const orderRes = await fetchOrderStats(startTime, endTime)
|
||||||
if (orderRes != null) {
|
if (orderRes != null) {
|
||||||
stats.revenue = orderRes.total_amount.toFixed(2)
|
stats.revenue = orderRes.total_amount.toFixed(2)
|
||||||
@@ -221,15 +227,19 @@ async function loadData() {
|
|||||||
stats.orderCount = String(orderRes.order_count)
|
stats.orderCount = String(orderRes.order_count)
|
||||||
stats.refundAmount = orderRes.refund_amount.toFixed(2)
|
stats.refundAmount = orderRes.refund_amount.toFixed(2)
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[finance-stats] 订单统计加载失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 获取趋势数据驱动图表
|
// 3. 获取趋势数据驱动图表
|
||||||
|
try {
|
||||||
const trendRes = await fetchFinanceBillSummary(startTime, endTime, 'day')
|
const trendRes = await fetchFinanceBillSummary(startTime, endTime, 'day')
|
||||||
updateChart(trendRes)
|
updateChart(trendRes)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.showToast({ title: '加载统计失败', icon: 'none' })
|
console.error('[finance-stats] 趋势图加载失败', e)
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDateTabChange(index : number) {
|
function handleDateTabChange(index : number) {
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="page-container">
|
|
||||||
<view class="page-content">
|
|
||||||
<view class="placeholder-card">
|
|
||||||
<text class="placeholder-title">正在跳转...</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="uts">
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
// 跳转到最新的订单管理页面
|
|
||||||
openRoute('OrderList')
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.page-container {
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
.page-content {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
.placeholder-card {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
.placeholder-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<AdminLayout :currentPage="currentPage">
|
<view class="admin-page">
|
||||||
<view class="order-statistic-page">
|
<view class="admin-sections">
|
||||||
|
|
||||||
<!-- 时间选择卡片 -->
|
<!-- 时间选择卡片 -->
|
||||||
<view class="filter-card">
|
<view class="admin-card filter-section">
|
||||||
<view class="filter-item">
|
<view class="filter-item">
|
||||||
<text class="filter-label">时间选择:</text>
|
<text class="filter-label">时间选择:</text>
|
||||||
<AnalyticsDateRangePicker
|
<AnalyticsDateRangePicker
|
||||||
@@ -14,12 +15,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 数据汇总卡片 -->
|
<!-- 数据汇总卡片 (响应式 kpi-grid 4列) -->
|
||||||
<view class="stat-cards-row">
|
<view class="kpi-grid">
|
||||||
<!-- 订单量 -->
|
<!-- 订单量 -->
|
||||||
<view class="stat-card">
|
<view class="admin-card stat-card">
|
||||||
<view class="icon-wrap blue-bg">
|
<view class="icon-wrap blue-bg">
|
||||||
<view class="custom-icon icon-order"></view>
|
<text class="icon-char">≡</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">{{ orderStats?.order_count ?? 0 }}</text>
|
<text class="stat-value">{{ orderStats?.order_count ?? 0 }}</text>
|
||||||
@@ -28,9 +29,9 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 订单销售额 -->
|
<!-- 订单销售额 -->
|
||||||
<view class="stat-card">
|
<view class="admin-card stat-card">
|
||||||
<view class="icon-wrap orange-bg">
|
<view class="icon-wrap orange-bg">
|
||||||
<view class="custom-icon icon-money"></view>
|
<text class="icon-char">¥</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">{{ orderStats?.total_amount?.toFixed(2) ?? '0.00' }}</text>
|
<text class="stat-value">{{ orderStats?.total_amount?.toFixed(2) ?? '0.00' }}</text>
|
||||||
@@ -39,9 +40,9 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 退款订单数 -->
|
<!-- 退款订单数 -->
|
||||||
<view class="stat-card">
|
<view class="admin-card stat-card">
|
||||||
<view class="icon-wrap green-bg">
|
<view class="icon-wrap green-bg">
|
||||||
<view class="custom-icon icon-refund"></view>
|
<text class="icon-char">↩</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">{{ orderStats?.refund_count ?? 0 }}</text>
|
<text class="stat-value">{{ orderStats?.refund_count ?? 0 }}</text>
|
||||||
@@ -50,9 +51,9 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 退款金额 -->
|
<!-- 退款金额 -->
|
||||||
<view class="stat-card last-card">
|
<view class="admin-card stat-card">
|
||||||
<view class="icon-wrap pink-bg">
|
<view class="icon-wrap pink-bg">
|
||||||
<view class="custom-icon icon-refund-money"></view>
|
<text class="icon-char">↺</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">{{ orderStats?.refund_amount?.toFixed(2) ?? '0.00' }}</text>
|
<text class="stat-value">{{ orderStats?.refund_amount?.toFixed(2) ?? '0.00' }}</text>
|
||||||
@@ -62,19 +63,18 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 营业趋势图表 -->
|
<!-- 营业趋势图表 -->
|
||||||
<view class="chart-card">
|
<view class="admin-card chart-card">
|
||||||
<view class="card-header">
|
<view class="chart-card-header">
|
||||||
<text class="card-title">营业趋势</text>
|
<text class="card-title">营业趋势</text>
|
||||||
|
<text class="download-icon-text">↓</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="chart-container">
|
|
||||||
<EChartsView :option="trendOption" class="trend-chart" />
|
<EChartsView :option="trendOption" class="trend-chart" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 底部双图表区域 -->
|
<!-- 底部双图表区域 -->
|
||||||
<view class="bottom-charts-row">
|
<view class="bottom-charts-grid">
|
||||||
<!-- 订单来源分析 -->
|
<!-- 订单来源分析 -->
|
||||||
<view class="bottom-chart-card">
|
<view class="admin-card bottom-chart-card">
|
||||||
<view class="card-header-row">
|
<view class="card-header-row">
|
||||||
<text class="card-title">订单来源分析</text>
|
<text class="card-title">订单来源分析</text>
|
||||||
<view class="style-toggle">
|
<view class="style-toggle">
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 订单类型分析 -->
|
<!-- 订单类型分析 -->
|
||||||
<view class="bottom-chart-card">
|
<view class="admin-card bottom-chart-card">
|
||||||
<view class="card-header-row">
|
<view class="card-header-row">
|
||||||
<text class="card-title">订单类型分析</text>
|
<text class="card-title">订单类型分析</text>
|
||||||
<view class="style-toggle">
|
<view class="style-toggle">
|
||||||
@@ -117,8 +117,9 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</AdminLayout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
@@ -173,23 +174,37 @@ async function loadAllData() {
|
|||||||
const st = startDate.value ? (startDate.value + ' 00:00:00') : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
const st = startDate.value ? (startDate.value + ' 00:00:00') : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
const et = endDate.value ? (endDate.value + ' 23:59:59') : new Date().toISOString()
|
const et = endDate.value ? (endDate.value + ' 23:59:59') : new Date().toISOString()
|
||||||
|
|
||||||
try {
|
// 各图表独立 try-catch,单个接口失败不影响其他图表展示
|
||||||
|
|
||||||
// 1. 加载汇总数据
|
// 1. 加载汇总数据
|
||||||
|
try {
|
||||||
orderStats.value = await fetchOrderStats(st, et)
|
orderStats.value = await fetchOrderStats(st, et)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[order-stats] 汇总数据加载失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 加载趋势数据
|
// 2. 加载趋势数据
|
||||||
|
try {
|
||||||
const trendData = await fetchOrderTrend(st, et)
|
const trendData = await fetchOrderTrend(st, et)
|
||||||
initTrendChart(trendData)
|
initTrendChart(trendData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[order-stats] 趋势图加载失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 加载来源数据
|
// 3. 加载来源数据
|
||||||
|
try {
|
||||||
const sourceData = await fetchOrderSourceStats(st, et)
|
const sourceData = await fetchOrderSourceStats(st, et)
|
||||||
initSourceChart(sourceData)
|
initSourceChart(sourceData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[order-stats] 来源图加载失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 加载订单类型数据
|
// 4. 加载订单类型数据(rpc_admin_order_type_stats 目前返回 400,单独隔离)
|
||||||
|
try {
|
||||||
const typeData = await fetchOrderTypeStats(st, et)
|
const typeData = await fetchOrderTypeStats(st, et)
|
||||||
orderTypeData.value = typeData
|
orderTypeData.value = typeData
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.showToast({ title: '加载统计数据失败', icon: 'none' })
|
console.error('[order-stats] 订单类型数据加载失败', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,97 +355,125 @@ function initTrendChart(data: any[]) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.order-statistic-page {
|
/* ===== 筛选区 ===== */
|
||||||
/* 使用 Layout 的背景和内边距 */
|
.filter-section {
|
||||||
min-height: 100%;
|
/* admin-card 提供 background / padding / border-radius */
|
||||||
}
|
|
||||||
|
|
||||||
.filter-card {
|
|
||||||
background-color: #fff;
|
|
||||||
padding: var(--admin-card-padding);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: var(--admin-section-gap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-item {
|
.filter-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-right: 12px;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-picker-mock {
|
/* ===== KPI 统计卡片 ===== */
|
||||||
|
/* .kpi-grid 由 admin-responsive.css 全局提供响应式 grid:>=1200px 4列 / >=768px 2列 / <768px 1列 */
|
||||||
|
.stat-card {
|
||||||
|
/* admin-card 提供背景/内边距,这里只定义内部 flex 布局 */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid #d9d9d9;
|
min-width: 0;
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
width: 240px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-icon {
|
.icon-wrap {
|
||||||
width: 16px;
|
width: 54px;
|
||||||
height: 16px;
|
height: 54px;
|
||||||
margin-right: 8px;
|
border-radius: 27px;
|
||||||
opacity: 0.45;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-range {
|
.blue-bg { background-color: #1890ff; }
|
||||||
font-size: 14px;
|
.orange-bg { background-color: #fa8c16; }
|
||||||
color: #595959;
|
.green-bg { background-color: #52c41a; }
|
||||||
|
.pink-bg { background-color: #eb2f96; }
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-cards-row {
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义图标 */
|
||||||
|
.icon-char {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 趋势图表 ===== */
|
||||||
|
.chart-card {
|
||||||
|
/* admin-card 提供背景/内边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: var(--admin-section-gap);
|
justify-content: space-between;
|
||||||
margin-bottom: var(--admin-section-gap);
|
align-items: center;
|
||||||
}
|
margin-bottom: 12px;
|
||||||
|
|
||||||
.chart-card {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: var(--admin-card-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.download-icon-text {
|
||||||
width: 100%;
|
font-size: 18px;
|
||||||
height: 400px;
|
color: #bfbfbf;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-chart {
|
.trend-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-charts-row {
|
/* ===== 底部双图表 ===== */
|
||||||
display: flex;
|
.bottom-charts-grid {
|
||||||
flex-direction: row;
|
display: grid;
|
||||||
gap: var(--admin-section-gap);
|
grid-template-columns: 1fr 1fr;
|
||||||
margin-top: var(--admin-section-gap);
|
gap: var(--admin-section-gap, 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.bottom-charts-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-chart-card {
|
.bottom-chart-card {
|
||||||
flex: 1;
|
/* admin-card 提供背景/内边距 */
|
||||||
background-color: #fff;
|
min-width: 0;
|
||||||
border-radius: 4px;
|
|
||||||
padding: var(--admin-card-padding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header-row {
|
.card-header-row {
|
||||||
@@ -445,6 +488,7 @@ function initTrendChart(data: any[]) {
|
|||||||
border: 1px solid #d9d9d9;
|
border: 1px solid #d9d9d9;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-text {
|
.toggle-text {
|
||||||
@@ -452,6 +496,7 @@ function initTrendChart(data: any[]) {
|
|||||||
color: #595959;
|
color: #595959;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 来源饼图 */
|
||||||
.pie-chart-container {
|
.pie-chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 320px;
|
height: 320px;
|
||||||
@@ -462,6 +507,7 @@ function initTrendChart(data: any[]) {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 类型分析表格 */
|
||||||
.type-table-container {
|
.type-table-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -479,18 +525,13 @@ function initTrendChart(data: any[]) {
|
|||||||
color: #595959;
|
color: #595959;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统一列宽与对齐方式 */
|
|
||||||
.col-id { width: 80px; text-align: center; }
|
|
||||||
.col-name { flex: 1; text-align: left; padding-left: 40px; }
|
|
||||||
.col-money { width: 180px; text-align: left; }
|
|
||||||
.col-rate { width: 240px; text-align: left; }
|
|
||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.td-text {
|
.td-text {
|
||||||
@@ -498,20 +539,27 @@ function initTrendChart(data: any[]) {
|
|||||||
color: #262626;
|
color: #262626;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 列宽 */
|
||||||
|
.col-id { width: 60px; text-align: center; flex-shrink: 0; }
|
||||||
|
.col-name { flex: 1; text-align: left; padding-left: 24px; min-width: 0; }
|
||||||
|
.col-money { width: 120px; text-align: left; flex-shrink: 0; }
|
||||||
|
.col-rate { width: 200px; text-align: left; flex-shrink: 0; }
|
||||||
|
|
||||||
.rate-box {
|
.rate-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start; /* 改为左对齐,与表头对齐样式一致 */
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-wrap {
|
.progress-wrap {
|
||||||
width: 120px;
|
width: 100px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin-right: 12px;
|
margin-right: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
@@ -522,124 +570,14 @@ function initTrendChart(data: any[]) {
|
|||||||
.rate-val {
|
.rate-val {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #595959;
|
color: #595959;
|
||||||
width: 50px;
|
min-width: 44px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
flex: 1;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: var(--admin-card-padding);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-wrap {
|
|
||||||
width: 54px;
|
|
||||||
height: 54px;
|
|
||||||
border-radius: 27px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 颜色背景 - 1:1 匹配截图 */
|
|
||||||
.blue-bg {
|
|
||||||
background-color: #e6f7ff;
|
|
||||||
border: 2px solid #bae7ff;
|
|
||||||
}
|
|
||||||
.orange-bg {
|
|
||||||
background-color: #fff7e6;
|
|
||||||
border: 2px solid #ffe58f;
|
|
||||||
}
|
|
||||||
.green-bg {
|
|
||||||
background-color: #f6ffed;
|
|
||||||
border: 2px solid #b7eb8f;
|
|
||||||
}
|
|
||||||
.pink-bg {
|
|
||||||
background-color: #fff0f6;
|
|
||||||
border: 2px solid #ffadd2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a1a1a;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-desc {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 自定义图标 1:1 形状模拟 - 内部使用伪元素或形状模拟截图形状 */
|
|
||||||
.custom-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-order {
|
|
||||||
background-color: #1890ff;
|
|
||||||
border-radius: 4px;
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 6px; left: 4px; right: 4px; height: 2px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-money {
|
|
||||||
background-color: #faad14;
|
|
||||||
border-radius: 50%;
|
|
||||||
&::after {
|
|
||||||
content: '¥';
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
display: flex; justify-content: center; align-items: center; height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-refund {
|
|
||||||
background-color: #52c41a;
|
|
||||||
border-radius: 4px;
|
|
||||||
&::before {
|
|
||||||
content: '↺';
|
|
||||||
color: #fff;
|
|
||||||
font-size: 16px;
|
|
||||||
display: flex; justify-content: center; align-items: center; height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-refund-money {
|
|
||||||
background-color: #eb2f96;
|
|
||||||
border-radius: 50%;
|
|
||||||
&::after {
|
|
||||||
content: '−';
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
display: flex; justify-content: center; align-items: center; height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '@/uni.scss';
|
@import '@/uni.scss';
|
||||||
.page {
|
.page { }
|
||||||
/* 使用 Layout 的背景和内边距 */
|
|
||||||
}
|
|
||||||
.header {
|
.header {
|
||||||
padding: var(--admin-card-padding);
|
padding: var(--admin-card-padding);
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="page-container">
|
|
||||||
<view class="page-content">
|
|
||||||
<view class="placeholder-card">
|
|
||||||
<text class="placeholder-title">正在跳转...</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="uts">
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
openRoute('product_productList')
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.page-container {
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-subtitle {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-content {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-card {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-desc {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-info {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -173,8 +173,10 @@ async function loadAllData() {
|
|||||||
const startTime = startDate.value ? (startDate.value + ' 00:00:00') : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
const startTime = startDate.value ? (startDate.value + ' 00:00:00') : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
const endTime = endDate.value ? (endDate.value + ' 23:59:59') : new Date().toISOString()
|
const endTime = endDate.value ? (endDate.value + ' 23:59:59') : new Date().toISOString()
|
||||||
|
|
||||||
try {
|
// 各图表独立 try-catch,单个接口失败(如 404)不影响其他区块展示
|
||||||
|
|
||||||
// 1. 加载核心指标
|
// 1. 加载核心指标
|
||||||
|
try {
|
||||||
const stats = await fetchAdminProductStats(startTime, endTime)
|
const stats = await fetchAdminProductStats(startTime, endTime)
|
||||||
if (stats != null) {
|
if (stats != null) {
|
||||||
statItems.value.forEach(item => {
|
statItems.value.forEach(item => {
|
||||||
@@ -182,16 +184,24 @@ async function loadAllData() {
|
|||||||
item.value = typeof val === 'number' ? (item.key.includes('amount') ? val.toFixed(2) : String(val)) : String(val ?? '0')
|
item.value = typeof val === 'number' ? (item.key.includes('amount') ? val.toFixed(2) : String(val)) : String(val ?? '0')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[product-stats] 核心指标加载失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 加载趋势图
|
// 2. 加载趋势图(rpc_admin_product_trend 404 时返回空数组,图表为空但不崩溃)
|
||||||
|
try {
|
||||||
const trendData = await fetchAdminProductTrend(startTime, endTime)
|
const trendData = await fetchAdminProductTrend(startTime, endTime)
|
||||||
initChart(trendData)
|
initChart(trendData)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[product-stats] 趋势图加载失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 加载排行
|
// 3. 加载排行(rpc_admin_product_ranking 404 时返回空数组)
|
||||||
|
try {
|
||||||
const rankingData = await fetchAdminProductRanking(startTime, endTime, 'sales', 10)
|
const rankingData = await fetchAdminProductRanking(startTime, endTime, 'sales', 10)
|
||||||
rankingList.value = rankingData
|
rankingList.value = rankingData
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.showToast({ title: '加载统计失败', icon: 'none' })
|
console.error('[product-stats] 排行加载失败', e)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,561 +0,0 @@
|
|||||||
<template>
|
|
||||||
<AdminLayout currentPage="service-autoReply">
|
|
||||||
<view class="service-container">
|
|
||||||
<view class="content-card">
|
|
||||||
<view class="header-row">
|
|
||||||
<view class="header-title">自动回复管理</view>
|
|
||||||
<view class="header-actions">
|
|
||||||
<button class="btn-primary" @click="handleCreate">新增自动回复</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="filter-section">
|
|
||||||
<view class="filter-row">
|
|
||||||
<view class="filter-item">
|
|
||||||
<text class="filter-label">关键词</text>
|
|
||||||
<input v-model="searchKeyword" class="filter-input" placeholder="请输入关键词" />
|
|
||||||
</view>
|
|
||||||
<view class="filter-item">
|
|
||||||
<text class="filter-label">状态</text>
|
|
||||||
<picker v-model="filterStatus" class="picker-wrapper" :range="statusOptions" range-key="label">
|
|
||||||
<view>{{ filterStatus > -1 ? statusOptions[filterStatus].label : '全部' }}</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
<button class="btn-search" @click="handleSearch">搜索</button>
|
|
||||||
<button class="btn-reset" @click="handleReset">重置</button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-if="selectedIds.length > 0" class="batch-actions">
|
|
||||||
<text class="batch-text">已选择 {{ selectedIds.length }} 条</text>
|
|
||||||
<button class="btn-batch-delete" @click="handleBatchDelete">批量删除</button>
|
|
||||||
<button class="btn-cancel-select" @click="handleCancelSelect">取消选择</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="table">
|
|
||||||
<view class="table-header">
|
|
||||||
<view class="cell cell-checkbox"><input type="checkbox" :checked="selectAll" @change="handleSelectAll"></view>
|
|
||||||
<view class="cell cell-id">ID</view>
|
|
||||||
<view class="cell cell-keyword">关键词</view>
|
|
||||||
<view class="cell cell-reply">回复内容</view>
|
|
||||||
<view class="cell cell-status">状态</view>
|
|
||||||
<view class="cell cell-time">更新时间</view>
|
|
||||||
<view class="cell cell-actions">操作</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="list.length === 0" class="empty">暂无数据</view>
|
|
||||||
<view v-for="item in list" :key="item.id" class="table-row" :class="{ selected: selectedIds.includes(item.id) }">
|
|
||||||
<view class="cell cell-checkbox"><input type="checkbox" :checked="selectedIds.includes(item.id)" @change="() => handleSelectItem(item.id)"></view>
|
|
||||||
<view class="cell cell-id">{{ item.id }}</view>
|
|
||||||
<view class="cell cell-keyword">{{ item.keyword }}</view>
|
|
||||||
<view class="cell cell-reply">{{ item.reply }}</view>
|
|
||||||
<view class="cell cell-status">
|
|
||||||
<text class="badge" :class="{ on: item.status === 1 }">{{ item.status === 1 ? '启用' : '禁用' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="cell cell-time">{{ item.updated_at }}</view>
|
|
||||||
<view class="cell cell-actions">
|
|
||||||
<button class="btn-action" @click="handleEdit(item.id)">编辑</button>
|
|
||||||
<button class="btn-action btn-danger" @click="handleDelete(item.id)">删除</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="pagination">
|
|
||||||
<button class="btn-page" :disabled="page === 1" @click="prevPage">上一页</button>
|
|
||||||
<text class="page-text">第 {{ page }} 页</text>
|
|
||||||
<button class="btn-page" :disabled="page >= totalPage" @click="nextPage">下一页</button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 编辑/新增自动回复 -->
|
|
||||||
<view v-if="showModal" class="modal-overlay" @click="closeModal">
|
|
||||||
<view class="modal-content" @click.stop>
|
|
||||||
<view class="modal-header">
|
|
||||||
<text class="modal-title">{{ editingId ? '编辑自动回复' : '新增自动回复' }}</text>
|
|
||||||
<button class="modal-close" @click="closeModal">关闭</button>
|
|
||||||
</view>
|
|
||||||
<view class="modal-body">
|
|
||||||
<view class="form-group">
|
|
||||||
<label class="form-label">关键词<text class="required">*</text></label>
|
|
||||||
<input v-model="form.keyword" class="form-input" placeholder="请输入关键词" />
|
|
||||||
</view>
|
|
||||||
<view class="form-group">
|
|
||||||
<label class="form-label">回复内容 <text class="required">*</text></label>
|
|
||||||
<textarea v-model="form.reply" class="form-textarea" placeholder="请输入回复内容"></textarea>
|
|
||||||
</view>
|
|
||||||
<view class="form-group">
|
|
||||||
<label class="form-label">状态</label>
|
|
||||||
<picker v-model="form.status" class="picker-wrapper" :range="statusOptions" range-key="label">
|
|
||||||
<view>{{ statusOptions[form.status].label }}</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="modal-footer">
|
|
||||||
<button class="btn-cancel" @click="closeModal">取消</button>
|
|
||||||
<button class="btn-confirm" @click="handleSave">保存</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</AdminLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="uts">
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
|
||||||
import { getAutoReplyList, saveAutoReply, deleteAutoReply } from './service.uts'
|
|
||||||
|
|
||||||
const list = ref<any[]>([])
|
|
||||||
const page = ref<number>(1)
|
|
||||||
const pageSize = 10
|
|
||||||
const total = ref<number>(0)
|
|
||||||
|
|
||||||
const selectedIds = ref<number[]>([])
|
|
||||||
const selectAll = ref<boolean>(false)
|
|
||||||
const showModal = ref<boolean>(false)
|
|
||||||
const editingId = ref<number | null>(null)
|
|
||||||
const searchKeyword = ref<string>('')
|
|
||||||
const filterStatus = ref<number>(-1)
|
|
||||||
|
|
||||||
const form = ref<any>({
|
|
||||||
keyword: '',
|
|
||||||
reply: '',
|
|
||||||
status: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const statusOptions = [
|
|
||||||
{ label: '启用', value: 1 },
|
|
||||||
{ label: '禁用', value: 0 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize)))
|
|
||||||
|
|
||||||
const loadList = async () => {
|
|
||||||
const res = await getAutoReplyList({
|
|
||||||
page: page.value,
|
|
||||||
limit: pageSize,
|
|
||||||
keyword: searchKeyword.value,
|
|
||||||
status: filterStatus.value > -1 ? filterStatus.value : undefined
|
|
||||||
})
|
|
||||||
list.value = res.items
|
|
||||||
total.value = res.total
|
|
||||||
selectedIds.value = []
|
|
||||||
selectAll.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
|
||||||
selectAll.value = !selectAll.value
|
|
||||||
if (selectAll.value) {
|
|
||||||
selectedIds.value = list.value.map(item => item.id)
|
|
||||||
} else {
|
|
||||||
selectedIds.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectItem = (id: number) => {
|
|
||||||
const index = selectedIds.value.indexOf(id)
|
|
||||||
if (index > -1) {
|
|
||||||
selectedIds.value.splice(index, 1)
|
|
||||||
} else {
|
|
||||||
selectedIds.value.push(id)
|
|
||||||
}
|
|
||||||
selectAll.value = selectedIds.value.length === list.value.length && list.value.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancelSelect = () => {
|
|
||||||
selectedIds.value = []
|
|
||||||
selectAll.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreate = () => {
|
|
||||||
editingId.value = null
|
|
||||||
form.value = { keyword: '', reply: '', status: 1 }
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (id: number) => {
|
|
||||||
const item = list.value.find(i => i.id === id)
|
|
||||||
if (item) {
|
|
||||||
editingId.value = id
|
|
||||||
form.value = { keyword: item.keyword, reply: item.reply, status: item.status }
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
if (!form.value.keyword.trim()) {
|
|
||||||
uni.showToast({ title: '请输入关键词', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.value.reply.trim()) {
|
|
||||||
uni.showToast({ title: '请输入回复内容', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await saveAutoReply({
|
|
||||||
id: editingId.value,
|
|
||||||
...form.value
|
|
||||||
})
|
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
|
||||||
closeModal()
|
|
||||||
loadList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
showModal.value = false
|
|
||||||
editingId.value = null
|
|
||||||
form.value = { keyword: '', reply: '', status: 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBatchDelete = () => {
|
|
||||||
uni.showModal({
|
|
||||||
title: '批量删除',
|
|
||||||
content: `确认删除选中的 ${selectedIds.value.length} 条自动回复吗?`,
|
|
||||||
success: async (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
for (const id of selectedIds.value) {
|
|
||||||
await deleteAutoReply(id)
|
|
||||||
}
|
|
||||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
||||||
loadList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
|
||||||
uni.showModal({
|
|
||||||
title: '删除自动回复',
|
|
||||||
content: '确认删除该自动回复吗?',
|
|
||||||
success: async (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
await deleteAutoReply(id)
|
|
||||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
||||||
loadList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
page.value = 1
|
|
||||||
loadList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
searchKeyword.value = ''
|
|
||||||
filterStatus.value = -1
|
|
||||||
page.value = 1
|
|
||||||
loadList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevPage = () => {
|
|
||||||
if (page.value > 1) {
|
|
||||||
page.value--
|
|
||||||
loadList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextPage = () => {
|
|
||||||
if (page.value < totalPage.value) {
|
|
||||||
page.value++
|
|
||||||
loadList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import '@/uni.scss';
|
|
||||||
|
|
||||||
.service-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
padding: $space-lg;
|
|
||||||
background: $background-secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-card {
|
|
||||||
flex: 1;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
box-shadow: $shadow-sm;
|
|
||||||
padding: $space-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: $space-lg;
|
|
||||||
}
|
|
||||||
.header-title { font-size: $font-size-lg; font-weight: 600; color: $text-primary; }
|
|
||||||
|
|
||||||
.btn-primary { background: $primary-color; color: #fff; padding: $space-sm $space-lg; border-radius: $radius-sm; border: none; font-size: $font-size-sm; }
|
|
||||||
|
|
||||||
.filter-section {
|
|
||||||
margin-bottom: $space-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
gap: $space-md;
|
|
||||||
margin-bottom: $space-lg;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: $space-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-label {
|
|
||||||
color: $text-secondary;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-wrapper {
|
|
||||||
padding: $space-xs $space-sm;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-input {
|
|
||||||
padding: $space-xs $space-sm;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-search {
|
|
||||||
background: $primary-color;
|
|
||||||
color: #fff;
|
|
||||||
padding: $space-xs $space-lg;
|
|
||||||
border: none;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-reset {
|
|
||||||
background: transparent;
|
|
||||||
color: $primary-color;
|
|
||||||
border: 1px solid $primary-color;
|
|
||||||
padding: $space-xs $space-lg;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
gap: $space-md;
|
|
||||||
padding: $space-md;
|
|
||||||
background: #fef3c7;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
margin-top: $space-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-text {
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
color: #92400e;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-batch-delete,
|
|
||||||
.btn-cancel-select {
|
|
||||||
padding: $space-xs $space-md;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
border: none;
|
|
||||||
font-size: $font-size-xs;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-batch-delete {
|
|
||||||
background: $error-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel-select {
|
|
||||||
background: #d1d5db;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table { border: 1px solid $border-color; border-radius: $radius-sm; overflow: hidden; }
|
|
||||||
.table-header,
|
|
||||||
.table-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.table-header { background: $background-tertiary; font-weight: 600; }
|
|
||||||
.table-row { border-top: 1px solid $border-color; }
|
|
||||||
.table-row.selected { background: #f3f4f6; }
|
|
||||||
|
|
||||||
.cell { padding: $space-sm $space-md; font-size: $font-size-sm; color: $text-primary; display: flex; align-items: center; }
|
|
||||||
.cell-checkbox { width: 50px; justify-content: center; }
|
|
||||||
.cell-id { width: 60px; justify-content: center; }
|
|
||||||
.cell-keyword { width: 140px; }
|
|
||||||
.cell-reply {
|
|
||||||
flex: 1;
|
|
||||||
flex-direction:row
|
|
||||||
}
|
|
||||||
.cell-status { width: 100px; justify-content: center; }
|
|
||||||
.cell-time { width: 180px; }
|
|
||||||
.cell-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction:row;
|
|
||||||
width: 180px;
|
|
||||||
gap: $space-sm;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge { padding: 2px 10px; border-radius: $radius-sm; background: lighten($error-color, 40%); color: $error-color; font-size: $font-size-xs; }
|
|
||||||
.badge.on { background: lighten($success-color, 40%); color: $success-color; }
|
|
||||||
|
|
||||||
.btn-action { border: 1px solid $primary-color; color: $primary-color; background: transparent; padding: 4px 10px; border-radius: $radius-sm; font-size: $font-size-xs; }
|
|
||||||
.btn-action.btn-danger { border-color: $error-color; color: $error-color; }
|
|
||||||
|
|
||||||
.empty { padding: $space-xl; text-align: center; color: $text-tertiary; }
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: $space-md;
|
|
||||||
margin-top: $space-lg;
|
|
||||||
}
|
|
||||||
.btn-page { padding: $space-xs $space-md; border: 1px solid $border-color; border-radius: $radius-sm; background: #fff; }
|
|
||||||
.page-text { color: $text-secondary; font-size: $font-size-sm; }
|
|
||||||
|
|
||||||
/* Modal Styles */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: $radius-lg;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 80vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: $space-lg;
|
|
||||||
border-bottom: 1px solid $border-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
font-size: $font-size-base;
|
|
||||||
font-weight: 600;
|
|
||||||
color: $text-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: 28px;
|
|
||||||
color: $text-tertiary;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: $space-lg;
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: $space-lg;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $space-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
font-weight: 500;
|
|
||||||
color: $text-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required {
|
|
||||||
color: $error-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
padding: $space-sm;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
color: $text-primary;
|
|
||||||
background: $background-secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-textarea {
|
|
||||||
padding: $space-sm;
|
|
||||||
border: 1px solid $border-color;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
color: $text-primary;
|
|
||||||
background: $background-secondary;
|
|
||||||
min-height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
gap: $space-md;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: $space-lg;
|
|
||||||
border-top: 1px solid $border-color;
|
|
||||||
background: $background-secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel,
|
|
||||||
.btn-confirm {
|
|
||||||
padding: $space-sm $space-lg;
|
|
||||||
border-radius: $radius-sm;
|
|
||||||
border: none;
|
|
||||||
font-size: $font-size-sm;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
background: $background-tertiary;
|
|
||||||
color: $text-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-confirm {
|
|
||||||
background: $primary-color;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,99 +1,5 @@
|
|||||||
main.uts:30 [Vue warn]: Unhandled error during execution of async component loader
|
ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_product_ranking 404 (Not Found)
|
||||||
at <AsyncComponentWrapper>
|
ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_product_trend 404 (Not Found)
|
||||||
at <PageBody>
|
ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_product_trend 404 (Not Found)
|
||||||
at <Page>
|
[AkReq.request] headers: {"apikey":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890","Content-Type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmOTkyZGZmYS1hOGZkLTQ1YmItODY3MC02ZmVlNWE1YWU4NGQiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzczOTc4NDY1LCJpYXQiOjE3NzM5NzQ4NjUsImVtYWlsIjoiYWRtaW5AMTYzLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsiZW1haWwiOiJhZG1pbkAxNjMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiZjk5MmRmZmEtYThmZC00NWJiLTg2NzAtNmZlZTVhNWFlODRkIiwidXNlcl9yb2xlIjoibWVyY2hhbnQifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3Mzk2NzY4Mn1dLCJzZXNzaW9uX2lkIjoiODhkMDhhNmEtNzBiOS00M2RhLWEwNmQtZWQ5ODIzZTM1MTQwIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.WitVmu_GTafuNQ5mxQbQPTmsEldjU0HA6qtCy0p6utM","Accept":"application/json"}
|
||||||
at <Anonymous>
|
ak-req.uts:218 POST http://119.146.131.237:9126/rest/v1/rpc/rpc_admin_order_type_stats 400 (Bad Request)
|
||||||
at <KeepAlive>
|
|
||||||
at <RouterView>
|
|
||||||
at <Layout>
|
|
||||||
at <App>
|
|
||||||
warnHandler @ uni-h5.es.js:19975
|
|
||||||
callWithErrorHandling @ vue.runtime.esm.js:1381
|
|
||||||
warn$1 @ vue.runtime.esm.js:1207
|
|
||||||
logError @ vue.runtime.esm.js:1438
|
|
||||||
errorHandler @ uni-h5.es.js:19600
|
|
||||||
callWithErrorHandling @ vue.runtime.esm.js:1381
|
|
||||||
handleError @ vue.runtime.esm.js:1421
|
|
||||||
onError @ vue.runtime.esm.js:3724
|
|
||||||
(anonymous) @ vue.runtime.esm.js:3767
|
|
||||||
Promise.catch
|
|
||||||
setup @ vue.runtime.esm.js:3766
|
|
||||||
callWithErrorHandling @ vue.runtime.esm.js:1381
|
|
||||||
setupStatefulComponent @ vue.runtime.esm.js:8985
|
|
||||||
setupComponent @ vue.runtime.esm.js:8946
|
|
||||||
mountComponent @ vue.runtime.esm.js:7262
|
|
||||||
processComponent @ vue.runtime.esm.js:7228
|
|
||||||
patch @ vue.runtime.esm.js:6694
|
|
||||||
mountChildren @ vue.runtime.esm.js:6942
|
|
||||||
processFragment @ vue.runtime.esm.js:7158
|
|
||||||
patch @ vue.runtime.esm.js:6668
|
|
||||||
mountChildren @ vue.runtime.esm.js:6942
|
|
||||||
processFragment @ vue.runtime.esm.js:7158
|
|
||||||
patch @ vue.runtime.esm.js:6668
|
|
||||||
mountChildren @ vue.runtime.esm.js:6942
|
|
||||||
mountElement @ vue.runtime.esm.js:6849
|
|
||||||
processElement @ vue.runtime.esm.js:6814
|
|
||||||
patch @ vue.runtime.esm.js:6682
|
|
||||||
mountChildren @ vue.runtime.esm.js:6942
|
|
||||||
mountElement @ vue.runtime.esm.js:6849
|
|
||||||
processElement @ vue.runtime.esm.js:6814
|
|
||||||
patch @ vue.runtime.esm.js:6682
|
|
||||||
mountChildren @ vue.runtime.esm.js:6942
|
|
||||||
processFragment @ vue.runtime.esm.js:7158
|
|
||||||
patch @ vue.runtime.esm.js:6668
|
|
||||||
componentUpdateFn @ vue.runtime.esm.js:7372
|
|
||||||
run @ vue.runtime.esm.js:153
|
|
||||||
instance.update @ vue.runtime.esm.js:7497
|
|
||||||
setupRenderEffect @ vue.runtime.esm.js:7507
|
|
||||||
mountComponent @ vue.runtime.esm.js:7274
|
|
||||||
processComponent @ vue.runtime.esm.js:7228
|
|
||||||
patch @ vue.runtime.esm.js:6694
|
|
||||||
mountChildren @ vue.runtime.esm.js:6942
|
|
||||||
mountElement @ vue.runtime.esm.js:6849
|
|
||||||
processElement @ vue.runtime.esm.js:6814
|
|
||||||
patch @ vue.runtime.esm.js:6682
|
|
||||||
componentUpdateFn @ vue.runtime.esm.js:7372
|
|
||||||
run @ vue.runtime.esm.js:153
|
|
||||||
instance.update @ vue.runtime.esm.js:7497
|
|
||||||
setupRenderEffect @ vue.runtime.esm.js:7507
|
|
||||||
mountComponent @ vue.runtime.esm.js:7274
|
|
||||||
processComponent @ vue.runtime.esm.js:7228
|
|
||||||
patch @ vue.runtime.esm.js:6694
|
|
||||||
componentUpdateFn @ vue.runtime.esm.js:7372
|
|
||||||
run @ vue.runtime.esm.js:153
|
|
||||||
instance.update @ vue.runtime.esm.js:7497
|
|
||||||
setupRenderEffect @ vue.runtime.esm.js:7507
|
|
||||||
mountComponent @ vue.runtime.esm.js:7274
|
|
||||||
processComponent @ vue.runtime.esm.js:7228
|
|
||||||
patch @ vue.runtime.esm.js:6694
|
|
||||||
componentUpdateFn @ vue.runtime.esm.js:7453
|
|
||||||
run @ vue.runtime.esm.js:153
|
|
||||||
instance.update @ vue.runtime.esm.js:7497
|
|
||||||
updateComponent @ vue.runtime.esm.js:7305
|
|
||||||
processComponent @ vue.runtime.esm.js:7239
|
|
||||||
patch @ vue.runtime.esm.js:6694
|
|
||||||
componentUpdateFn @ vue.runtime.esm.js:7453
|
|
||||||
run @ vue.runtime.esm.js:153
|
|
||||||
instance.update @ vue.runtime.esm.js:7497
|
|
||||||
callWithErrorHandling @ vue.runtime.esm.js:1381
|
|
||||||
flushJobs @ vue.runtime.esm.js:1585
|
|
||||||
Promise.then
|
|
||||||
queueFlush @ vue.runtime.esm.js:1494
|
|
||||||
queueJob @ vue.runtime.esm.js:1488
|
|
||||||
scheduler @ vue.runtime.esm.js:3179
|
|
||||||
resetScheduling @ vue.runtime.esm.js:236
|
|
||||||
triggerEffects @ vue.runtime.esm.js:280
|
|
||||||
triggerRefValue @ vue.runtime.esm.js:1033
|
|
||||||
set value @ vue.runtime.esm.js:1078
|
|
||||||
finalizeNavigation @ vue-router.mjs?v=ed041164:2474
|
|
||||||
(anonymous) @ vue-router.mjs?v=ed041164:2384
|
|
||||||
Promise.then
|
|
||||||
pushWithRedirect @ vue-router.mjs?v=ed041164:2352
|
|
||||||
push @ vue-router.mjs?v=ed041164:2278
|
|
||||||
install @ vue-router.mjs?v=ed041164:2631
|
|
||||||
use @ vue.runtime.esm.js:5190
|
|
||||||
initRouter @ uni-h5.es.js:19886
|
|
||||||
install @ uni-h5.es.js:19955
|
|
||||||
use @ vue.runtime.esm.js:5190
|
|
||||||
(anonymous) @ main.uts:30
|
|
||||||
main.uts:30 SyntaxError: The requested module '/services/admin/financeService.uts?import' does not provide an export named 'fetchAdminBalanceDistribution' (at index.uvue:150:3)
|
|
||||||
Reference in New Issue
Block a user