接入店铺列表数据
This commit is contained in:
@@ -21,6 +21,7 @@ import UserCenter from '@/pages/mall/admin/userCenter/index.uvue'
|
||||
// --- 店铺模块 ---
|
||||
import ShopManage from '@/pages/mall/admin/shop/manage.uvue'
|
||||
import ShopCreate from '@/pages/mall/admin/shop/create.uvue'
|
||||
import ShopAdminList from '@/pages/mall/admin/shop/shop-manage.uvue'
|
||||
|
||||
// --- 用户模块 ---
|
||||
import UserStatistic from '@/pages/mall/admin/user/statistics/index.uvue'
|
||||
@@ -192,6 +193,7 @@ export const componentMap: Map<string, any> = new Map([
|
||||
// 店铺模块
|
||||
['ShopManage', ShopManage],
|
||||
['ShopCreate', ShopCreate],
|
||||
['ShopAdminList', ShopAdminList],
|
||||
|
||||
// 用户模块
|
||||
['UserStatistic', UserStatistic],
|
||||
|
||||
@@ -244,6 +244,15 @@ export const routes: RouteRecord[] = [
|
||||
},
|
||||
|
||||
// ========== 店铺模块 ==========
|
||||
{
|
||||
id: 'shop_admin_list',
|
||||
title: '店铺列表',
|
||||
path: '/pages/mall/admin/shop/shop-manage',
|
||||
componentKey: 'ShopAdminList',
|
||||
parentId: 'shop',
|
||||
groupId: 'shop-manage',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
id: 'shop_manage',
|
||||
title: '我的店铺',
|
||||
@@ -251,7 +260,7 @@ export const routes: RouteRecord[] = [
|
||||
componentKey: 'ShopManage',
|
||||
parentId: 'shop',
|
||||
groupId: 'shop-manage',
|
||||
order: 1
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
id: 'shop_create',
|
||||
@@ -261,7 +270,7 @@ export const routes: RouteRecord[] = [
|
||||
parentId: 'shop',
|
||||
groupId: 'shop-manage',
|
||||
hidden: true,
|
||||
order: 2
|
||||
order: 3
|
||||
},
|
||||
|
||||
// ========== 用户模块 ==========
|
||||
|
||||
@@ -1604,6 +1604,13 @@
|
||||
{
|
||||
"root": "pages/mall/admin/shop",
|
||||
"pages": [
|
||||
{
|
||||
"path": "shop-manage",
|
||||
"style": {
|
||||
"navigationBarTitleText": "店铺列表",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "manage",
|
||||
"style": {
|
||||
|
||||
506
pages/mall/admin/shop/shop-manage.uvue
Normal file
506
pages/mall/admin/shop/shop-manage.uvue
Normal file
@@ -0,0 +1,506 @@
|
||||
<!-- Admin 店铺列表管理页 (role=admin 专用)
|
||||
数据来源: ml_shops 真实数据,服务端分页,按需请求
|
||||
-->
|
||||
<template>
|
||||
<view class="shop-list-page">
|
||||
<!-- 1. 搜索筛选区 -->
|
||||
<view class="search-card">
|
||||
<view class="search-row">
|
||||
<view class="search-item">
|
||||
<text class="label">注册时间:</text>
|
||||
<view class="date-range-row">
|
||||
<input class="mock-input date-input" v-model="startTime" placeholder="开始 2025-01-01" />
|
||||
<text class="date-sep">~</text>
|
||||
<input class="mock-input date-input" v-model="endTime" placeholder="结束 2025-12-31" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-item">
|
||||
<text class="label">店铺状态:</text>
|
||||
<picker :range="statusLabels" :value="statusPickerIndex" @change="onStatusChange">
|
||||
<view class="mock-select">
|
||||
<text>{{ statusLabels[statusPickerIndex] }}</text>
|
||||
<text class="arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-row mt-16">
|
||||
<view class="search-item">
|
||||
<text class="label">店铺名称:</text>
|
||||
<input class="mock-input" v-model="searchName" placeholder="请输入店铺名称" />
|
||||
</view>
|
||||
<button class="btn-primary" @click="onSearch">查询</button>
|
||||
<button class="btn-white" @click="onReset">重置</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 2. 数据表格 -->
|
||||
<view class="list-card">
|
||||
<!-- 错误提示 -->
|
||||
<view v-if="fetchError !== ''" class="error-tip">
|
||||
<text class="error-txt">{{ fetchError }}</text>
|
||||
<button class="btn-white btn-sm mt-8" @click="loadShops(1)">重试</button>
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="loading-tip">
|
||||
<text class="loading-txt">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="table-v5">
|
||||
<!-- 表头 -->
|
||||
<view class="th-row">
|
||||
<view class="th col-logo"><text>Logo</text></view>
|
||||
<view class="th col-name"><text>店铺名称</text></view>
|
||||
<view class="th col-cid"><text>编号</text></view>
|
||||
<view class="th col-merchant"><text>商户ID</text></view>
|
||||
<view class="th col-contact"><text>联系人/电话</text></view>
|
||||
<view class="th col-status"><text>状态</text></view>
|
||||
<view class="th col-stats"><text>商品/订单</text></view>
|
||||
<view class="th col-rating"><text>评分/评价</text></view>
|
||||
<view class="th col-verify"><text>认证</text></view>
|
||||
<view class="th col-time"><text>注册时间</text></view>
|
||||
<view class="th col-op"><text>操作</text></view>
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view v-if="shopList.length === 0 && !loading && fetchError === ''" class="empty-row">
|
||||
<text class="empty-txt">暂无店铺数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 数据行 -->
|
||||
<view v-for="item in shopList" :key="item.id" class="tr-row">
|
||||
<!-- Logo -->
|
||||
<view class="td col-logo">
|
||||
<image
|
||||
v-if="item.shop_logo != null && item.shop_logo !== ''"
|
||||
class="shop-logo-img"
|
||||
:src="item.shop_logo"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="logo-placeholder">
|
||||
<text class="logo-placeholder-txt">无</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺名称 -->
|
||||
<view class="td col-name">
|
||||
<text class="shop-name-txt">{{ item.shop_name !== '' ? item.shop_name : '—' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 编号 -->
|
||||
<view class="td col-cid">
|
||||
<text class="cid-txt">{{ item.cid > 0 ? item.cid : '—' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 商户ID(截短) -->
|
||||
<view class="td col-merchant">
|
||||
<text class="merchant-txt">{{ item.merchant_id !== '' ? item.merchant_id.substring(0, 8) + '...' : '—' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 联系人/电话 -->
|
||||
<view class="td col-contact">
|
||||
<text class="contact-name">{{ item.contact_name != null && item.contact_name !== '' ? item.contact_name : '—' }}</text>
|
||||
<text class="contact-phone">{{ item.contact_phone != null && item.contact_phone !== '' ? item.contact_phone : '' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 状态 -->
|
||||
<view class="td col-status">
|
||||
<text class="status-tag" :class="getStatusClass(item.status)">
|
||||
{{ getStatusText(item.status) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 商品/订单数 -->
|
||||
<view class="td col-stats">
|
||||
<text class="stats-txt">{{ item.product_count }} / {{ item.order_count }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 评分/评价数 -->
|
||||
<view class="td col-rating">
|
||||
<text class="rating-txt">{{ formatRating(item.rating_avg) }} ({{ item.rating_count }})</text>
|
||||
</view>
|
||||
|
||||
<!-- 认证状态 -->
|
||||
<view class="td col-verify">
|
||||
<text class="verify-tag" :class="item.verified_at != null && item.verified_at !== '' ? 'verified' : 'unverified'">
|
||||
{{ item.verified_at != null && item.verified_at !== '' ? '已认证' : '未认证' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 注册时间 -->
|
||||
<view class="td col-time">
|
||||
<text>{{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<view class="td col-op">
|
||||
<text
|
||||
v-if="item.status !== 1"
|
||||
class="op-link green"
|
||||
@click="handleStatusChange(item.id, 1)"
|
||||
>启用</text>
|
||||
<text
|
||||
v-if="item.status === 1"
|
||||
class="op-link warn"
|
||||
@click="handleStatusChange(item.id, 2)"
|
||||
>暂停</text>
|
||||
<text
|
||||
v-if="item.status !== 3"
|
||||
class="op-link red"
|
||||
@click="handleStatusChange(item.id, 3)"
|
||||
>关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<CommonPagination
|
||||
v-if="total > 0"
|
||||
:total="total"
|
||||
:loading="loading"
|
||||
:currentPage="currentPage"
|
||||
:pageSize="pageSize"
|
||||
:pageSizeOptionLabels="pageSizeOptionLabels"
|
||||
:pageSizeIndex="pageSizeIndex"
|
||||
:visiblePages="visiblePages"
|
||||
:totalPage="totalPage"
|
||||
:jumpPageInput="jumpPageInput"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
@update:jumpPageInput="(val : string) => { jumpPageInput = val }"
|
||||
@jump-page="handleJumpPage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
|
||||
import {
|
||||
fetchAdminShops,
|
||||
updateAdminShopStatus,
|
||||
type AdminShopItem
|
||||
} from '@/services/admin/shopManageService.uts'
|
||||
|
||||
// ========== 搜索筛选状态 ==========
|
||||
const searchName = ref('')
|
||||
const startTime = ref('')
|
||||
const endTime = ref('')
|
||||
// index 0 → 全部(null), 1 → 正常(1), 2 → 暂停(2), 3 → 关闭(3)
|
||||
const statusLabels = ['全部', '正常营业', '暂停营业', '已关闭']
|
||||
const statusPickerIndex = ref(0)
|
||||
|
||||
function onStatusChange(e : any) {
|
||||
statusPickerIndex.value = Number(e.detail.value)
|
||||
}
|
||||
|
||||
// ========== 数据状态 ==========
|
||||
const loading = ref(false)
|
||||
const fetchError = ref('')
|
||||
const shopList = ref<AdminShopItem[]>([])
|
||||
|
||||
// ========== 数据加载(服务端分页,点第几页才请求第几页) ==========
|
||||
async function loadShops(page : number) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
fetchError.value = ''
|
||||
try {
|
||||
// statusPickerIndex: 0=全部, 1~3=对应 status 值
|
||||
const selectedStatus : number | null = statusPickerIndex.value === 0 ? null : statusPickerIndex.value
|
||||
const result = await fetchAdminShops({
|
||||
searchName: searchName.value !== '' ? searchName.value : null,
|
||||
status: selectedStatus,
|
||||
startTime: startTime.value !== '' ? startTime.value : null,
|
||||
endTime: endTime.value !== '' ? endTime.value : null,
|
||||
page: page,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
shopList.value = result.items
|
||||
total.value = result.total
|
||||
currentPage.value = page
|
||||
} catch (e) {
|
||||
fetchError.value = '加载失败,请稍后重试'
|
||||
console.error('[shop-manage] 加载店铺列表失败:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
loadShops(1)
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
startTime.value = ''
|
||||
endTime.value = ''
|
||||
statusPickerIndex.value = 0
|
||||
loadShops(1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadShops(1)
|
||||
})
|
||||
|
||||
// ========== 字段格式化 ==========
|
||||
function formatTime(t : string | null) : string {
|
||||
if (t == null || t === '') return '—'
|
||||
return t.replace('T', ' ').substring(0, 16)
|
||||
}
|
||||
|
||||
function formatRating(val : number) : string {
|
||||
if (val == null || val === 0) return '—'
|
||||
return val.toFixed(1)
|
||||
}
|
||||
|
||||
// status: 1=正常营业 2=暂停营业 3=已关闭
|
||||
function getStatusText(status : number) : string {
|
||||
if (status === 1) return '正常营业'
|
||||
if (status === 2) return '暂停营业'
|
||||
if (status === 3) return '已关闭'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
function getStatusClass(status : number) : string {
|
||||
if (status === 1) return 'normal'
|
||||
if (status === 2) return 'pause'
|
||||
if (status === 3) return 'closed'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
// ========== 操作:变更店铺状态 ==========
|
||||
function handleStatusChange(id : string, targetStatus : number) {
|
||||
let msg = ''
|
||||
if (targetStatus === 1) msg = '确定要启用该店铺吗?'
|
||||
else if (targetStatus === 2) msg = '确定要暂停该店铺营业吗?'
|
||||
else if (targetStatus === 3) msg = '确定要关闭该店铺吗?此操作不可自动恢复。'
|
||||
|
||||
uni.showModal({
|
||||
title: '操作确认',
|
||||
content: msg,
|
||||
success: async (res : any) => {
|
||||
if (res.confirm === true) {
|
||||
const ok = await updateAdminShopStatus(id, targetStatus)
|
||||
if (ok) {
|
||||
const label = targetStatus === 1 ? '已启用' : targetStatus === 2 ? '已暂停' : '已关闭'
|
||||
uni.showToast({ title: label, icon: 'success' })
|
||||
loadShops(currentPage.value)
|
||||
} else {
|
||||
uni.showToast({ title: '操作失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 分页状态(服务端分页) ==========
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const jumpPageInput = ref('')
|
||||
const pageSizeOptions = [10, 20, 30, 50]
|
||||
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n : number) => `${n}条/页`))
|
||||
const pageSizeIndex = computed(() => { const idx = pageSizeOptions.indexOf(pageSize.value); return idx >= 0 ? idx : 1 })
|
||||
const total = ref(0)
|
||||
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||||
const visiblePages = computed((): number[] => {
|
||||
const t = totalPage.value; const cur = currentPage.value
|
||||
if (t <= 7) return Array.from({ length: t }, (_ : any, i : number) => i + 1)
|
||||
if (cur <= 4) return [1, 2, 3, 4, 5, -1, t]
|
||||
if (cur >= t - 3) return [1, -1, t - 4, t - 3, t - 2, t - 1, t]
|
||||
return [1, -1, cur - 1, cur, cur + 1, -1, t]
|
||||
})
|
||||
const handlePageChange = (p : number) => {
|
||||
if (p < 1 || p > totalPage.value) return
|
||||
loadShops(p)
|
||||
}
|
||||
const handlePageSizeChange = (e : any) => {
|
||||
const idx = Number(e.detail.value)
|
||||
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[1]
|
||||
loadShops(1)
|
||||
}
|
||||
const handleJumpPage = () => {
|
||||
const p = parseInt(jumpPageInput.value)
|
||||
if (!isNaN(p) && p >= 1 && p <= totalPage.value) {
|
||||
loadShops(p)
|
||||
}
|
||||
}
|
||||
// ========== END PAGINATION STATE ==========
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.shop-list-page {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
/* 搜索区 */
|
||||
.search-card {
|
||||
background: #fff;
|
||||
padding: var(--admin-card-padding, 16px 24px);
|
||||
border-radius: 4px;
|
||||
margin-bottom: var(--admin-section-gap, 16px);
|
||||
}
|
||||
.search-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mt-16 { margin-top: 16px; }
|
||||
.mt-8 { margin-top: 8px; }
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.label { font-size: 14px; color: #606266; white-space: nowrap; }
|
||||
}
|
||||
|
||||
.date-range-row {
|
||||
display: flex; flex-direction: row; align-items: center; gap: 8px;
|
||||
.date-sep { font-size: 13px; color: #606266; }
|
||||
}
|
||||
|
||||
.mock-select {
|
||||
width: 160px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
|
||||
display: flex; flex-direction: row; align-items: center; justify-content: space-between;
|
||||
padding: 0 12px; font-size: 13px; color: #606266;
|
||||
.arrow { font-size: 10px; color: #c0c4cc; }
|
||||
}
|
||||
.mock-input {
|
||||
width: 200px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
|
||||
padding: 0 12px; font-size: 13px;
|
||||
}
|
||||
.date-input { width: 140px; }
|
||||
|
||||
.btn-primary {
|
||||
height: 32px; padding: 0 16px; background: #1890ff; color: #fff;
|
||||
border: none; border-radius: 4px; font-size: 13px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.btn-white {
|
||||
height: 32px; padding: 0 16px; background: #fff; color: #606266;
|
||||
border: 1px solid #dcdfe6; border-radius: 4px; font-size: 13px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.btn-sm { height: 28px; padding: 0 12px; font-size: 12px; }
|
||||
|
||||
/* 错误/加载提示 */
|
||||
.error-tip {
|
||||
padding: 16px 24px;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.error-txt { font-size: 13px; color: #f56c6c; }
|
||||
.loading-tip {
|
||||
padding: 32px; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.loading-txt { font-size: 14px; color: #999; }
|
||||
|
||||
/* 列表卡片 */
|
||||
.list-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.table-v5 {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.th-row, .tr-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
min-width: 1200px;
|
||||
}
|
||||
.th-row {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
.th, .td {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.th { color: #666; font-size: 12px; }
|
||||
.tr-row:hover { background: #f5f7fa; }
|
||||
|
||||
/* 列宽 */
|
||||
.col-logo { width: 56px; justify-content: center; }
|
||||
.col-name { width: 160px; }
|
||||
.col-cid { width: 60px; justify-content: center; }
|
||||
.col-merchant{ width: 110px; }
|
||||
.col-contact { width: 140px; flex-direction: column; align-items: flex-start; }
|
||||
.col-status { width: 90px; justify-content: center; }
|
||||
.col-stats { width: 90px; justify-content: center; }
|
||||
.col-rating { width: 100px; justify-content: center; }
|
||||
.col-verify { width: 70px; justify-content: center; }
|
||||
.col-time { width: 140px; }
|
||||
.col-op {
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
min-width: 100px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* 空态行 */
|
||||
.empty-row {
|
||||
padding: 48px; display: flex; justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.empty-txt { font-size: 14px; color: #999; }
|
||||
|
||||
/* Logo 图片 */
|
||||
.shop-logo-img {
|
||||
width: 36px; height: 36px; border-radius: 4px; object-fit: cover;
|
||||
}
|
||||
.logo-placeholder {
|
||||
width: 36px; height: 36px; border-radius: 4px;
|
||||
background: #f0f0f0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.logo-placeholder-txt { font-size: 10px; color: #bbb; }
|
||||
|
||||
/* 文本样式 */
|
||||
.shop-name-txt { font-size: 13px; color: #303133; font-weight: 500; }
|
||||
.cid-txt { font-size: 12px; color: #909399; }
|
||||
.merchant-txt { font-size: 12px; color: #909399; font-family: monospace; }
|
||||
.contact-name { font-size: 13px; color: #303133; }
|
||||
.contact-phone { font-size: 12px; color: #909399; }
|
||||
.stats-txt { font-size: 13px; color: #515a6e; }
|
||||
.rating-txt { font-size: 13px; color: #515a6e; }
|
||||
|
||||
/* 状态标签 */
|
||||
.status-tag {
|
||||
padding: 2px 8px; border-radius: 3px; font-size: 12px;
|
||||
&.normal { background: #f0f9eb; color: #67c23a; }
|
||||
&.pause { background: #fdf6ec; color: #e6a23c; }
|
||||
&.closed { background: #fef0f0; color: #f56c6c; }
|
||||
&.unknown { background: #f4f4f5; color: #909399; }
|
||||
}
|
||||
|
||||
/* 认证标签 */
|
||||
.verify-tag {
|
||||
padding: 2px 6px; border-radius: 3px; font-size: 12px;
|
||||
&.verified { background: #e6f7ff; color: #1890ff; }
|
||||
&.unverified { background: #f4f4f5; color: #909399; }
|
||||
}
|
||||
|
||||
/* 操作链接 */
|
||||
.op-link {
|
||||
font-size: 13px; cursor: pointer;
|
||||
&.green { color: #67c23a; }
|
||||
&.warn { color: #e6a23c; }
|
||||
&.red { color: #f56c6c; }
|
||||
}
|
||||
</style>
|
||||
114
services/admin/shopManageService.uts
Normal file
114
services/admin/shopManageService.uts
Normal file
@@ -0,0 +1,114 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
/**
|
||||
* Admin 店铺列表项(对应 ml_shops 列表页所需字段)
|
||||
*/
|
||||
export type AdminShopItem = {
|
||||
id: string
|
||||
cid: number
|
||||
merchant_id: string
|
||||
shop_name: string
|
||||
shop_logo: string | null
|
||||
contact_name: string | null
|
||||
contact_phone: string | null
|
||||
status: number
|
||||
product_count: number
|
||||
order_count: number
|
||||
rating_avg: number
|
||||
rating_count: number
|
||||
verified_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin 店铺查询参数
|
||||
*/
|
||||
export type AdminShopQuery = {
|
||||
searchName?: string | null
|
||||
status?: number | null
|
||||
startTime?: string | null
|
||||
endTime?: string | null
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
// 精确选取列表页需要字段,禁止 SELECT *
|
||||
const LIST_COLUMNS = 'id,cid,merchant_id,shop_name,shop_logo,contact_name,contact_phone,status,product_count,order_count,rating_avg,rating_count,verified_at,created_at'
|
||||
|
||||
/**
|
||||
* 分页查询 ml_shops(服务端分页,按需按页请求)
|
||||
*/
|
||||
export async function fetchAdminShops(query?: AdminShopQuery): Promise<{ total: number; items: Array<AdminShopItem> }> {
|
||||
const page = query?.page ?? 1
|
||||
const pageSize = query?.pageSize ?? 20
|
||||
|
||||
const builder = supa
|
||||
.from('ml_shops')
|
||||
.select(LIST_COLUMNS)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(pageSize)
|
||||
.page(page)
|
||||
|
||||
// 条件过滤(仅非空时才附加,避免无效 filter)
|
||||
if (query?.status != null) {
|
||||
builder.eq('status', query.status)
|
||||
}
|
||||
if (query?.searchName != null && query.searchName !== '') {
|
||||
builder.ilike('shop_name', `%${query.searchName}%`)
|
||||
}
|
||||
if (query?.startTime != null && query.startTime !== '') {
|
||||
builder.gte('created_at', query.startTime)
|
||||
}
|
||||
if (query?.endTime != null && query.endTime !== '') {
|
||||
builder.lte('created_at', query.endTime)
|
||||
}
|
||||
|
||||
const result = await builder.execute()
|
||||
|
||||
if (result.error != null) {
|
||||
console.error('[shopManageService] fetchAdminShops 失败:', result.error)
|
||||
return { total: 0, items: [] as Array<AdminShopItem> }
|
||||
}
|
||||
|
||||
const rawRows = (result.data ?? []) as any[]
|
||||
const items: Array<AdminShopItem> = []
|
||||
|
||||
for (let i = 0; i < rawRows.length; i++) {
|
||||
const row = rawRows[i] as UTSJSONObject
|
||||
items.push({
|
||||
id: row.getString('id') ?? '',
|
||||
cid: row.getNumber('cid') ?? 0,
|
||||
merchant_id: row.getString('merchant_id') ?? '',
|
||||
shop_name: row.getString('shop_name') ?? '',
|
||||
shop_logo: row.getString('shop_logo') ?? null,
|
||||
contact_name: row.getString('contact_name') ?? null,
|
||||
contact_phone: row.getString('contact_phone') ?? null,
|
||||
status: row.getNumber('status') ?? 1,
|
||||
product_count: row.getNumber('product_count') ?? 0,
|
||||
order_count: row.getNumber('order_count') ?? 0,
|
||||
rating_avg: row.getNumber('rating_avg') ?? 0,
|
||||
rating_count: row.getNumber('rating_count') ?? 0,
|
||||
verified_at: row.getString('verified_at') ?? null,
|
||||
created_at: row.getString('created_at') ?? ''
|
||||
} as AdminShopItem)
|
||||
}
|
||||
|
||||
return { total: result.total ?? 0, items }
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定店铺的状态(1=正常 2=暂停 3=关闭)
|
||||
*/
|
||||
export async function updateAdminShopStatus(shopId: string, status: number): Promise<boolean> {
|
||||
const result = await supa
|
||||
.from('ml_shops')
|
||||
.update({ status: status, updated_at: new Date().toISOString() } as UTSJSONObject)
|
||||
.eq('id', shopId)
|
||||
.execute()
|
||||
|
||||
if (result.error != null) {
|
||||
console.error('[shopManageService] updateAdminShopStatus 失败:', result.error)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user