接入商品评论数据

This commit is contained in:
2026-03-20 17:30:30 +08:00
parent 620ae742df
commit 13811ae87d
14 changed files with 535 additions and 1753 deletions

111
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,111 @@
# Copilot Instructions for This Repository
## Project context
This repository contains a multi-module mall system with admin-oriented database SQL organized by execution stage and business domain.
For any task related to database changes, RPC, RLS, or admin data access, treat `docs/sql/README_EXECUTION_ORDER.md` as the authoritative execution and validation reference.
## Core database rules
1. Follow the required execution order for database changes:
- Schema / Migration first
- RLS second
- RPC last
2. Never change the order to RPC -> RLS -> Schema.
RPC and RLS may depend on fields, indexes, or policies created in Schema.
3. Soft delete is the default deletion standard.
Do not implement physical deletes unless the task explicitly requires it and the user confirms.
Prefer updating soft-delete fields such as:
- `deleted_at`
- `deleted_by`
- `restored_at`
- `restored_by`
4. For delete-related RPC or SQL:
- do not use physical `DELETE` by default
- write `deleted_at` and `deleted_by`
- preserve auditability
- apply cascade soft delete when the existing design requires it
5. For RLS-related work:
- assume soft-deleted rows should be filtered by default
- when appropriate, ensure policies include logic equivalent to `deleted_at IS NULL`
- do not weaken existing RLS rules without clearly explaining why
6. For existing RPC/functions:
- prefer `CREATE OR REPLACE FUNCTION`
- avoid creating duplicate function variants unless versioning is explicitly required
## Required working method
1. Analyze first, edit second.
Before making changes, first inspect the relevant files and summarize:
- affected files
- dependencies
- execution order
- risks
2. Limit the scope of changes.
Only modify the business domain and files directly related to the task.
Do not refactor unrelated modules.
3. Do not invent schema details.
Before generating SQL or admin code, inspect the actual table definitions, RPC files, and RLS files already present in the repository.
4. Preserve current naming and directory conventions.
Keep the existing staged SQL structure such as:
- `10_schema/**`
- `20_rls/**`
- `30_rpc/**`
5. Keep admin-facing behavior consistent with current project conventions.
Do not silently break existing frontend call paths, parameter names, return structures, or pagination contracts.
## Output requirements for Copilot responses
For any non-trivial task, respond with:
1. A short impact summary
2. The list of files to change
3. The implementation plan in the correct order
4. The actual code/SQL changes
5. A validation checklist
6. Any risks, assumptions, or rollback notes
## Validation requirements
When a task changes database behavior, include verification steps for:
- field existence
- soft-delete index existence
- RLS effectiveness
- RPC behavior
- whether deletion remains soft delete instead of physical delete
- whether `deleted_at` / `deleted_by` are written correctly
- whether required cascade soft delete behavior still works
## SQL and Supabase safety rules
1. Do not assume a SQL file has already been executed just because it exists in the repository.
2. Do not assume a table is in `public` unless verified.
3. If a task involves Supabase-exposed data access, consider RLS impact explicitly.
4. If adding a new table or new admin data access path, mention whether RLS and policies also need to be added or updated.
## Preferred behavior for complex tasks
For large tasks, break the work into phases:
1. inspect
2. plan
3. implement
4. validate
Do not jump straight into large-scale edits without first identifying dependencies.
## Reference
Primary reference for database execution order and validation:
- `docs/sql/README_EXECUTION_ORDER.md`

View File

@@ -246,7 +246,7 @@ export const routes: RouteRecord[] = [
// ========== 店铺模块 ==========
{
id: 'shop_manage',
title: '店铺管理',
title: '我的店铺',
path: '/pages/mall/admin/shop/manage',
componentKey: 'ShopManage',
parentId: 'shop',

View File

@@ -5,30 +5,33 @@
<view class="search-row">
<view class="search-item">
<text class="label">评价时间:</text>
<view class="mock-date-range">
<text class="emoji">📅</text>
<text class="txt">开始日期 - 结束日期</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>
<view class="mock-select"><text>请选择</text><text class="arrow">▼</text></view>
</view>
<view class="search-item">
<text class="label">审核状态:</text>
<view class="mock-select"><text>请选择</text><text class="arrow">▼</text></view>
<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" placeholder="请输入商品信息" />
<input class="mock-input" v-model="searchProduct" placeholder="请输入商品信息" />
</view>
<view class="search-item">
<text class="label">用户名称:</text>
<input class="mock-input" placeholder="请输入" />
<input class="mock-input" v-model="searchUser" placeholder="请输入" />
</view>
<button class="btn-primary">查询</button>
<button class="btn-primary" @click="onSearch">查询</button>
<button class="btn-white" @click="onReset">重置</button>
</view>
</view>
@@ -40,53 +43,75 @@
<!-- 3. 数据表格 -->
<view class="list-card">
<!-- 错误提示 -->
<view v-if="fetchError !== ''" class="error-tip">
<text class="error-txt">{{ fetchError }}</text>
</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-check"><text>□</text></view>
<view class="th col-id"><text>评论ID</text></view>
<view class="th col-product"><text>商品信息</text></view>
<view class="th col-spec"><text>规格</text></view>
<view class="th col-user"><text>用户名称</text></view>
<view class="th col-score"><text>评分</text></view>
<view class="th col-content"><text>评价内容</text></view>
<view class="th col-reply"><text>回复内容</text></view>
<view class="th col-status"><text>审核状态</text></view>
<view class="th col-status"><text>状态</text></view>
<view class="th col-time"><text>评价时间</text></view>
<view class="th col-op"><text>操作</text></view>
</view>
<view v-for="item in pagedList" :key="item.id" class="tr-row">
<!-- 空态 -->
<view v-if="reviewList.length === 0 && !loading" class="empty-row">
<text class="empty-txt">暂无评价数据</text>
</view>
<view v-for="item in reviewList" :key="item.id" class="tr-row">
<view class="td col-check"><text>□</text></view>
<view class="td col-id"><text>{{ item.id }}</text></view>
<view class="td col-product">
<image class="p-img" :src="item.image" mode="aspectFill" />
<text class="p-name-txt">{{ item.productName }}</text>
<view class="td col-id">
<text class="id-txt">{{ item.id.substring(0, 8) }}...</text>
</view>
<view class="td col-product">
<image v-if="item.product_image != null && item.product_image !== ''" class="p-img" :src="item.product_image" mode="aspectFill" />
<view v-else class="p-img-placeholder" />
<text class="p-name-txt">{{ item.product_name !== '' ? item.product_name : '—' }}</text>
</view>
<view class="td col-user">
<text>{{ item.username != null && item.username !== '' ? item.username : '匿名用户' }}</text>
</view>
<view class="td col-score"><text>{{ item.rating }}星</text></view>
<view class="td col-content">
<text class="blue-link">{{ item.content != null && item.content !== '' ? item.content : '—' }}</text>
</view>
<view class="td col-reply">
<text>{{ item.merchant_reply != null && item.merchant_reply !== '' ? item.merchant_reply : '无' }}</text>
</view>
<view class="td col-spec"><text>{{ item.spec }}</text></view>
<view class="td col-user"><text>{{ item.username }}</text></view>
<view class="td col-score"><text>{{ item.score }}</text></view>
<view class="td col-content"><text class="blue-link">{{ item.content }}</text></view>
<view class="td col-reply"><text>{{ item.reply || '无' }}</text></view>
<view class="td col-status">
<text class="status-tag" :class="item.status === 1 ? 'pass' : 'wait'">
{{ item.status === 1 ? '通过' : '待审核' }}
<text class="status-tag" :class="getStatusClass(item.status)">
{{ getStatusText(item.status) }}
</text>
</view>
<view class="td col-time"><text>{{ item.time }}</text></view>
<view class="td col-time"><text>{{ formatTime(item.created_at) }}</text></view>
<view class="td col-op">
<text class="op-link">通过</text>
<text class="op-link">驳回</text>
<text class="op-link">回复</text>
<text class="op-link red">删除</text>
<text v-if="item.status !== 1" class="op-link" @click="handleApprove(item.id)">通过</text>
<text v-if="item.status === 1" class="op-link warn" @click="handleReject(item.id)">驳回</text>
<text class="op-link" @click="openReplyModal(item.id, item.merchant_reply)">回复</text>
<text class="op-link red" @click="handleDelete(item.id)">删除</text>
</view>
</view>
</view>
<!-- 分页 -->
<CommonPagination
v-if="true"
v-if="total > 0"
:total="total"
:loading="false"
:loading="loading"
:currentPage="currentPage"
:pageSize="pageSize"
:pageSizeOptionLabels="pageSizeOptionLabels"
@@ -100,81 +125,232 @@
@jump-page="handleJumpPage"
/>
</view>
<!-- 回复弹窗 -->
<view class="reply-modal-mask" v-if="showReplyModal" @click="closeReplyModal">
<view class="reply-modal" @click.stop="">
<view class="modal-header">
<text class="modal-title">回复评价</text>
<text class="modal-close" @click="closeReplyModal">✕</text>
</view>
<view class="modal-body">
<textarea class="reply-textarea" v-model="replyContent" placeholder="请输入回复内容..." :maxlength="500" />
</view>
<view class="modal-footer">
<button class="btn-white btn-sm" @click="closeReplyModal">取消</button>
<button class="btn-primary btn-sm" @click="submitReply">
{{ replySubmitting ? '提交中...' : '确认回复' }}
</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
import {
fetchAdminProductReviews,
approveProductReview,
rejectProductReview,
replyProductReview,
deleteProductReview,
type ProductReviewItem
} from '@/services/admin/productReviewService.uts'
const replyList = ref([
{
id: 1069,
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
productName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
spec: 'XL,卡其',
username: 'demo998',
score: 3.5,
content: '22',
reply: '',
status: 0,
time: '2025-02-19 14:56:43'
},
{
id: 1059,
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
productName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
spec: 'XL,卡其',
username: '你好呀',
score: 3.5,
content: '的',
reply: '',
status: 0,
time: '2025-01-07 15:35:36'
},
{
id: 980,
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
productName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
spec: 'XL,卡其',
username: 'wx209638',
score: 5,
content: '好',
reply: '',
status: 1,
time: '2024-09-12 14:20:12'
// ========== 搜索筛选状态 ==========
const searchProduct = ref('')
const searchUser = 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 reviewList = ref<ProductReviewItem[]>([])
// ========== 数据加载(服务端分页,按需请求) ==========
async function loadReviews(page : number) {
if (loading.value) return
loading.value = true
fetchError.value = ''
try {
// index 0 → null不过滤其余直接映射到真实 status 值 1/2/3
const selectedStatus : number | null = statusPickerIndex.value === 0 ? null : statusPickerIndex.value
const result = await fetchAdminProductReviews({
searchProduct: searchProduct.value !== '' ? searchProduct.value : null,
searchUser: searchUser.value !== '' ? searchUser.value : null,
status: selectedStatus,
startTime: startTime.value !== '' ? startTime.value : null,
endTime: endTime.value !== '' ? endTime.value : null,
page: page,
pageSize: pageSize.value
})
reviewList.value = result.items
total.value = result.total
currentPage.value = page
} catch (e) {
fetchError.value = '加载失败,请稍后重试'
console.error('加载评价列表失败:', e)
} finally {
loading.value = false
}
])
}
// ========== PAGINATION STATE ==========
function onSearch() {
loadReviews(1)
}
function onReset() {
searchProduct.value = ''
searchUser.value = ''
startTime.value = ''
endTime.value = ''
statusPickerIndex.value = 0
loadReviews(1)
}
onMounted(() => {
loadReviews(1)
})
// ========== 字段格式化 ==========
function formatTime(t : string | null) : string {
if (t == null || t === '') return '—'
return t.replace('T', ' ').substring(0, 19)
}
// 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 'pass'
if (status === 2) return 'deleted'
return 'wait'
}
// ========== 操作通过status → 1 ==========
async function handleApprove(id : string) {
const ok = await approveProductReview(id)
if (ok) {
uni.showToast({ title: '已通过', icon: 'success' })
loadReviews(currentPage.value)
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
// ========== 操作驳回status → 3 ==========
async function handleReject(id : string) {
const ok = await rejectProductReview(id)
if (ok) {
uni.showToast({ title: '已驳回', icon: 'success' })
loadReviews(currentPage.value)
} else {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
// ========== 操作删除status → 2软删除 ==========
function handleDelete(id : string) {
uni.showModal({
title: '确认删除',
content: '确定要删除该评价吗?此操作将标记为已删除状态。',
success: async (res : any) => {
if (res.confirm === true) {
const ok = await deleteProductReview(id)
if (ok) {
uni.showToast({ title: '已删除', icon: 'success' })
loadReviews(currentPage.value)
} else {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}
// ========== 回复弹窗 ==========
const showReplyModal = ref(false)
const replyTargetId = ref('')
const replyContent = ref('')
const replySubmitting = ref(false)
function openReplyModal(id : string, existingReply : string | null) {
replyTargetId.value = id
replyContent.value = existingReply ?? ''
showReplyModal.value = true
}
function closeReplyModal() {
showReplyModal.value = false
replyTargetId.value = ''
replyContent.value = ''
}
async function submitReply() {
if (replyContent.value.trim() === '') {
uni.showToast({ title: '请输入回复内容', icon: 'none' })
return
}
replySubmitting.value = true
try {
const ok = await replyProductReview(replyTargetId.value, replyContent.value.trim())
if (ok) {
uni.showToast({ title: '回复成功', icon: 'success' })
closeReplyModal()
loadReviews(currentPage.value)
} else {
uni.showToast({ title: '回复失败', icon: 'none' })
}
} finally {
replySubmitting.value = false
}
}
// ========== PAGINATION STATE服务端分页 ==========
const currentPage = ref(1)
const pageSize = ref(10)
const jumpPageInput = ref('')
const pageSizeOptions = [10, 20, 30, 50]
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n: number) => `${n}条/页`))
const pageSizeOptionLabels = computed(() => pageSizeOptions.map((n : number) => `${n}条/页`))
const pageSizeIndex = computed(() => { const idx = pageSizeOptions.indexOf(pageSize.value); return idx >= 0 ? idx : 0 })
const total = computed(() => replyList.value.length)
const total = ref(0) // 来自服务端 total_count
const totalPage = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
const pagedList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return replyList.value.slice(start, start + 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 (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) => { currentPage.value = p }
const handlePageSizeChange = (e: any) => {
const handlePageChange = (p : number) => {
if (p < 1 || p > totalPage.value) return
loadReviews(p)
}
const handlePageSizeChange = (e : any) => {
const idx = Number(e.detail.value)
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
currentPage.value = 1
loadReviews(1)
}
const handleJumpPage = () => {
const p = parseInt(jumpPageInput.value)
if (!isNaN(p) && p >= 1 && p <= totalPage.value) currentPage.value = p
if (!isNaN(p) && p >= 1 && p <= totalPage.value) {
loadReviews(p)
}
}
// ========== END PAGINATION STATE ==========
</script>
@@ -208,11 +384,9 @@ const handleJumpPage = () => {
.label { font-size: 14px; color: #606266; width: 80px; text-align: right; }
}
.mock-date-range {
width: 280px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
display: flex; flex-direction: row; align-items: center; padding: 0 12px; gap: 8px;
.emoji { font-size: 14px; }
.txt { font-size: 13px; color: #c0c4cc; }
.date-range-row {
display: flex; flex-direction: row; align-items: center; gap: 8px;
.date-sep { font-size: 13px; color: #606266; }
}
.mock-select {
@@ -226,8 +400,11 @@ const handleJumpPage = () => {
width: 200px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 13px;
}
.date-input { width: 140px; }
.btn-primary { background: #1890ff; color: #fff; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 14px; border: none; }
.btn-white { background: #fff; color: #606266; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 14px; border: 1px solid #dcdfe6; }
.btn-sm { height: 32px; padding: 0 20px; font-size: 13px; }
.action-bar {
margin-bottom: 20px;
@@ -242,6 +419,16 @@ const handleJumpPage = () => {
padding: var(--admin-card-padding);
}
/* 错误 & 加载提示 */
.error-tip {
padding: 12px 16px; background: #fff2f0; border-radius: 4px; margin-bottom: 12px;
.error-txt { font-size: 13px; color: #f5222d; }
}
.loading-tip {
padding: 24px 0; display: flex; justify-content: center;
.loading-txt { font-size: 14px; color: #909399; }
}
.table-v5 { width: 100%; }
.th-row {
@@ -266,10 +453,16 @@ const handleJumpPage = () => {
display: flex; align-items: center; justify-content: center;
}
/* 空态行 */
.empty-row {
padding: 48px 0;
display: flex; justify-content: center;
.empty-txt { font-size: 14px; color: #c0c4cc; }
}
.col-check { width: 50px; }
.col-id { width: 80px; }
.col-id { width: 100px; }
.col-product { flex: 1.5; justify-content: flex-start; gap: 12px; }
.col-spec { width: 100px; }
.col-user { width: 120px; }
.col-score { width: 80px; }
.col-content { flex: 1; }
@@ -279,16 +472,51 @@ const handleJumpPage = () => {
.col-op { width: 180px; display: flex; flex-direction: row; }
.p-img { width: 40px; height: 40px; border-radius: 4px; }
.p-img-placeholder { width: 40px; height: 40px; border-radius: 4px; background: #f5f5f5; }
.p-name-txt { font-size: 13px; line-height: 1.4; color: #1890ff; }
.id-txt { font-size: 12px; color: #909399; }
.blue-link { color: #1890ff; }
.status-tag {
padding: 2px 8px; border-radius: 2px; font-size: 12px;
&.pass { background: rgba(82, 196, 26, 0.1); color: #52c41a; }
&.wait { background: rgba(250, 173, 20, 0.1); color: #faad14; }
&.pass { background: rgba(82, 196, 26, 0.1); color: #52c41a; }
&.wait { background: rgba(250, 173, 20, 0.1); color: #faad14; }
&.deleted { background: rgba(245, 34, 45, 0.1); color: #f5222d; }
}
.op-link { font-size: 13px; color: #1890ff; margin: 0 4px; cursor: pointer; &.red { color: #f5222d; } }
/* 分页区域已迁至 CommonPagination 组件 */
.op-link {
font-size: 13px; color: #1890ff; margin: 0 4px; cursor: pointer;
&.red { color: #f5222d; }
&.warn { color: #faad14; }
}
/* 回复弹窗 */
.reply-modal-mask {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex; align-items: center; justify-content: center;
z-index: 1000;
}
.reply-modal {
background: #fff; border-radius: 6px; width: 480px; max-width: 90vw;
display: flex; flex-direction: column;
}
.modal-header {
padding: 16px 20px; border-bottom: 1px solid #f0f0f0;
display: flex; flex-direction: row; align-items: center; justify-content: space-between;
.modal-title { font-size: 16px; font-weight: 500; color: #333; }
.modal-close { font-size: 16px; color: #909399; cursor: pointer; padding: 4px; }
}
.modal-body {
padding: 20px;
.reply-textarea {
width: 100%; height: 100px; border: 1px solid #dcdfe6; border-radius: 4px;
padding: 8px 12px; font-size: 13px; resize: none;
}
}
.modal-footer {
padding: 12px 20px; border-top: 1px solid #f0f0f0;
display: flex; flex-direction: row; justify-content: flex-end; gap: 12px;
}
</style>

View File

@@ -1,44 +1,12 @@
<!-- 商家端 - 聊天页面 -->
<template>
<view class="chat-page">
<<<<<<< HEAD
<view class="chat-header">
=======
<!-- 聊天头部 -->
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
>>>>>>> local-backup-root-cyj
<view class="header-back" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="header-info">
<<<<<<< HEAD
<text class="chat-title">{{ chatTitle }}</text>
<text class="chat-status">在线</text>
</view>
<view class="header-actions"></view>
</view>
<scroll-view scroll-y class="chat-content" :scroll-into-view="scrollToView" scroll-with-animation>
<view class="chat-messages">
<view v-for="msg in chatMessages" :key="msg.id" :class="['message-item', msg.is_from_user ? 'me' : 'received']" :id="'msg-' + msg.id">
<view v-if="!msg.is_from_user" class="message-wrapper">
<image class="avatar" src="/static/images/default-avatar.png" mode="aspectFill" />
<view class="message-content-wrapper">
<view class="message-bubble">
<text class="message-text">{{ msg.content }}</text>
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
</view>
</view>
</view>
<view v-else class="message-wrapper me">
<view class="message-content-wrapper">
<view class="message-bubble me">
<text class="message-text">{{ msg.content }}</text>
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
</view>
</view>
<image class="avatar me" src="/static/images/default-shop.png" mode="aspectFill" />
=======
<view class="header-info-text-wrapper">
<text class="chat-title">{{ chatTitle }}</text>
<text class="chat-status">在线</text>
@@ -103,20 +71,11 @@
:src="shopAvatar"
mode="aspectFill"
/>
>>>>>>> local-backup-root-cyj
</view>
</view>
</view>
</scroll-view>
<<<<<<< HEAD
<view class="chat-input">
<input v-model="inputText" class="input-field" placeholder="请输入消息..." confirm-type="send" @confirm="sendMessage" />
<view class="send-btn" @click="sendMessage">
<text class="send-icon">➤</text>
</view>
</view>
=======
<!-- 聊天输入区 -->
<view class="chat-input">
<view class="input-tools">
@@ -156,23 +115,16 @@
</text>
</view>
</scroll-view>
>>>>>>> local-backup-root-cyj
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
<<<<<<< HEAD
type ChatMessageType = {
id: string
=======
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
type ChatMessageType = {
id: string
viewId: string
>>>>>>> local-backup-root-cyj
session_id: string
sender_id: string
receiver_id: string
@@ -181,10 +133,7 @@
is_read: boolean
is_from_user: boolean
created_at: string
<<<<<<< HEAD
=======
time: string
>>>>>>> local-backup-root-cyj
}
export default {
@@ -196,9 +145,6 @@
inputText: '',
chatMessages: [] as ChatMessageType[],
scrollToView: '',
<<<<<<< HEAD
merchantId: ''
=======
merchantId: '',
userAvatar: '/static/images/default-avatar.png',
shopAvatar: '/static/images/default-shop.png',
@@ -212,19 +158,15 @@
computed: {
emojiList(): string[] {
return ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
>>>>>>> local-backup-root-cyj
}
},
onLoad(options: any) {
<<<<<<< HEAD
=======
const sysInfo = uni.getSystemInfoSync()
const statusBarH = sysInfo.statusBarHeight
this.navPaddingTop = (statusBarH + 10) + 'px'
console.log('chat page onLoad options:', options)
>>>>>>> local-backup-root-cyj
if (options.session_id) {
this.sessionId = options.session_id
}
@@ -238,9 +180,6 @@
},
onShow() {
<<<<<<< HEAD
this.loadChatMessages()
=======
console.log('chat page onShow, chatUserId:', this.chatUserId, 'merchantId:', this.merchantId)
if (this.merchantId) {
this.loadChatMessages()
@@ -257,61 +196,12 @@
if (this.realtimeChannel != null) {
supa.removeChannel(this.realtimeChannel!)
}
>>>>>>> local-backup-root-cyj
},
methods: {
async initMerchantId() {
try {
const session = supa.getSession()
<<<<<<< HEAD
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
} catch (e) {}
},
async loadChatMessages() {
try {
let query
if (this.sessionId) {
query = supa
.from('ml_chat_messages')
.select('*')
.eq('session_id', this.sessionId)
.order('created_at', { ascending: true })
} else if (this.chatUserId && this.merchantId) {
query = supa
.from('ml_chat_messages')
.select('*')
.or(`and(sender_id.eq.${this.chatUserId},receiver_id.eq.${this.merchantId}),and(sender_id.eq.${this.merchantId},receiver_id.eq.${this.chatUserId})`)
.order('created_at', { ascending: true })
}
if (query) {
const response = await query.execute()
if (response.data && (response.data as any[]).length > 0) {
const rawData = response.data as any[]
const messages: ChatMessageType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const senderId = item.getString('sender_id')
messages.push({
id: item.getString('id') || '',
session_id: item.getString('session_id') || '',
sender_id: senderId || '',
receiver_id: item.getString('receiver_id') || '',
content: item.getString('content') || '',
msg_type: item.getString('msg_type') || 'text',
is_read: item.getBoolean('is_read') || false,
is_from_user: senderId === this.merchantId,
created_at: item.getString('created_at') || ''
} as ChatMessageType)
}
this.chatMessages = messages
this.scrollToView = messages.length > 0 ? 'msg-' + messages[messages.length - 1].id : ''
this.markAsRead()
}
=======
if (session != null && session.user != null) {
this.merchantId = session.user.getString('id') || ''
}
@@ -405,15 +295,12 @@
} else {
console.log('没有找到聊天记录')
this.chatMessages = []
>>>>>>> local-backup-root-cyj
}
} catch (e) {
console.error('加载聊天记录失败:', e)
}
},
<<<<<<< HEAD
=======
setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...')
@@ -492,25 +379,12 @@
}, 100)
},
>>>>>>> local-backup-root-cyj
async markAsRead() {
try {
await supa
.from('ml_chat_messages')
.update({ is_read: true })
.eq('receiver_id', this.merchantId)
<<<<<<< HEAD
.eq('is_read', false)
.execute()
} catch (e) {}
},
async sendMessage() {
if (!this.inputText.trim()) return
const content = this.inputText.trim()
this.inputText = ''
=======
.eq('sender_id', this.chatUserId)
.eq('is_read', false)
.execute()
@@ -525,7 +399,6 @@
this.inputText = ''
this.showEmoji = false
>>>>>>> local-backup-root-cyj
try {
const newMessage = {
@@ -543,26 +416,6 @@
.insert([newMessage])
.execute()
<<<<<<< HEAD
if (!response.error) {
this.loadChatMessages()
}
} catch (e) {
console.error('发送消息失败:', e)
}
},
goBack() {
uni.navigateBack()
},
formatTime(timeStr: string): string {
if (!timeStr) return ''
const date = new Date(timeStr)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
=======
if (response.error != null) {
console.error('发送消息失败:', response.error)
uni.showToast({ title: '发送失败', icon: 'none' })
@@ -621,40 +474,12 @@
goBack() {
uni.navigateBack()
>>>>>>> local-backup-root-cyj
}
}
}
</script>
<style>
<<<<<<< HEAD
.chat-page { display: flex; flex-direction: column; height: 100vh; background-color: #f5f5f5; }
.chat-header { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-bottom: 1rpx solid #eee; }
.header-back { padding: 10rpx 20rpx 10rpx 0; }
.back-icon { font-size: 48rpx; color: #333; font-weight: bold; }
.header-info { flex: 1; display: flex; flex-direction: column; align-items: center; }
.chat-title { font-size: 32rpx; color: #333; font-weight: 500; }
.chat-status { font-size: 22rpx; color: #4CAF50; }
.header-actions { padding: 10rpx; }
.chat-content { flex: 1; padding: 20rpx; }
.chat-messages { display: flex; flex-direction: column; }
.message-item { margin-bottom: 30rpx; }
.message-wrapper { display: flex; align-items: flex-start; }
.message-wrapper.me { flex-direction: row-reverse; }
.avatar { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin: 0 20rpx; }
.message-content-wrapper { max-width: 70%; }
.message-bubble { background-color: #fff; padding: 20rpx; border-radius: 12rpx; position: relative; }
.message-bubble.me { background-color: #007AFF; }
.me .message-text { color: #fff; }
.me .message-time { color: rgba(255,255,255,0.7); }
.message-text { font-size: 28rpx; color: #333; line-height: 1.4; }
.message-time { display: block; font-size: 20rpx; color: #999; margin-top: 10rpx; text-align: right; }
.chat-input { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-top: 1rpx solid #eee; }
.input-field { flex: 1; height: 72rpx; background-color: #f5f5f5; border-radius: 36rpx; padding: 0 30rpx; font-size: 28rpx; }
.send-btn { margin-left: 20rpx; width: 72rpx; height: 72rpx; background-color: #007AFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.send-icon { font-size: 32rpx; color: #fff; }
=======
.chat-page { width: 100%; flex: 1; background-color: #f5f5f5; display: flex; flex-direction: column; overflow: hidden; }
.chat-header { background-color: white; padding-left: 15px; padding-right: 15px; padding-bottom: 10px; display: flex; flex-direction: row; align-items: center; justify-content: space-between; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #eee; flex-shrink: 0; }
@@ -700,5 +525,4 @@
.image-bubble { padding: 0 !important; background-color: transparent !important; }
.image-bubble .message-time { margin-top: 5px; text-align: right; }
.message-image { width: 150px; border-radius: 8px; }
>>>>>>> local-backup-root-cyj
</style>

View File

@@ -1,142 +1,4 @@
<<<<<<< HEAD
<!-- 商家端首页 - UTS Android 兼容 -->
<template>
<view class="merchant-container">
<!-- 头部导航 -->
<view class="header">
<view class="header-content">
<view class="shop-info">
<image :src="shopInfo.shop_logo || '/static/images/default-shop.png'" class="shop-logo" mode="aspectFit" />
<view class="shop-details">
<text class="shop-name">{{ shopInfo.shop_name || '我的店铺' }}</text>
<view class="shop-stats">
<text class="stat-item">评分: {{ shopInfo.rating_avg || 5.0 }}</text>
<text class="stat-item">销量: {{ shopInfo.total_sales || 0 }}</text>
</view>
</view>
</view>
<view class="header-actions">
<text class="action-btn" @click="goToMessages">消息</text>
<text class="action-btn" @click="goToSettings">设置</text>
</view>
</view>
</view>
<!-- 数据概览 -->
<view class="overview-section">
<text class="section-title">今日数据</text>
<view class="overview-grid">
<view class="overview-item">
<text class="overview-value">{{ todayStats.orders }}</text>
<text class="overview-label">订单数</text>
</view>
<view class="overview-item">
<text class="overview-value">¥{{ formatNumber(todayStats.sales) }}</text>
<text class="overview-label">销售额</text>
</view>
<view class="overview-item">
<text class="overview-value">{{ todayStats.visitors }}</text>
<text class="overview-label">访客数</text>
</view>
<view class="overview-item">
<text class="overview-value">{{ todayStats.conversion }}%</text>
<text class="overview-label">转化率</text>
</view>
</view>
</view>
<!-- 待处理事项 -->
<view class="pending-section">
<text class="section-title">待处理</text>
<view class="pending-list">
<view class="pending-item" @click="goToOrders('pending')">
<text class="pending-icon">📦</text>
<text class="pending-text">待发货订单</text>
<text class="pending-count">{{ pendingCounts.pending_shipment }}</text>
</view>
<view class="pending-item" @click="goToOrders('refund')">
<text class="pending-icon">↩️</text>
<text class="pending-text">退款处理</text>
<text class="pending-count">{{ pendingCounts.refund_requests }}</text>
</view>
<view class="pending-item" @click="goToProducts('low_stock')">
<text class="pending-icon">⚠️</text>
<text class="pending-text">库存预警</text>
<text class="pending-count">{{ pendingCounts.low_stock }}</text>
</view>
<view class="pending-item" @click="goToReviews">
<text class="pending-icon">💬</text>
<text class="pending-text">待回复评价</text>
<text class="pending-count">{{ pendingCounts.pending_reviews }}</text>
</view>
</view>
</view>
<!-- 快捷功能 -->
<view class="shortcuts-section">
<text class="section-title">快捷功能</text>
<view class="shortcuts-grid">
<view class="shortcut-item" @click="goToProducts('add')">
<text class="shortcut-icon"></text>
<text class="shortcut-text">添加商品</text>
</view>
<view class="shortcut-item" @click="goToOrders('all')">
<text class="shortcut-icon">📋</text>
<text class="shortcut-text">订单管理</text>
</view>
<view class="shortcut-item" @click="goToProducts('manage')">
<text class="shortcut-icon">📦</text>
<text class="shortcut-text">商品管理</text>
</view>
<view class="shortcut-item" @click="goToPromotions">
<text class="shortcut-icon">🎯</text>
<text class="shortcut-text">营销活动</text>
</view>
<view class="shortcut-item" @click="goToStatistics">
<text class="shortcut-icon">📊</text>
<text class="shortcut-text">数据统计</text>
</view>
<view class="shortcut-item" @click="goToFinance">
<text class="shortcut-icon">💰</text>
<text class="shortcut-text">财务结算</text>
</view>
</view>
</view>
<!-- 最新订单 -->
<view class="recent-orders-section">
<view class="section-header">
<text class="section-title">最新订单</text>
<text class="section-more" @click="goToOrders('all')">查看全部</text>
</view>
<view v-if="recentOrders.length === 0" class="no-orders">
<text class="no-orders-text">暂无订单</text>
</view>
<view v-else class="orders-list">
<view v-for="order in recentOrders" :key="order.id" class="order-item" @click="goToOrderDetail(order.id)">
<view class="order-header">
<text class="order-no">{{ order.order_no }}</text>
<text class="order-status" :class="getOrderStatusClass(order.order_status)">{{ getOrderStatusText(order.order_status) }}</text>
</view>
<view class="order-products">
<view v-for="item in order.items" :key="item.id" class="product-item">
<image :src="item.image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFit" />
<view class="product-info">
<text class="product-name">{{ item.product_name }}</text>
<text class="product-spec">{{ item.sku_specifications || '' }}</text>
<text class="product-price">¥{{ item.price }} × {{ item.quantity }}</text>
</view>
</view>
</view>
<view class="order-footer">
<text class="order-amount">合计: ¥{{ order.total_amount }}</text>
<text class="order-time">{{ formatTime(order.created_at) }}</text>
</view>
</view>
</view>
</view>
=======
<!-- 商家端首页 -->
<!-- 商家端首页 -->
<template>
<view class="merchant-container">
<scroll-view scroll-y class="main-scroll" :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
@@ -373,7 +235,6 @@
<view class="safe-bottom"></view>
</view>
</scroll-view>
>>>>>>> local-backup-root-cyj
</view>
</template>
@@ -459,9 +320,6 @@
low_stock: 0,
pending_reviews: 0
} as PendingCountsType,
<<<<<<< HEAD
recentOrders: [] as OrderType[]
=======
recentOrders: [] as OrderType[],
unreadCount: 0,
refreshing: false
@@ -472,7 +330,6 @@
currentDate(): string {
const now = new Date()
return `${now.getMonth() + 1}月${now.getDate()}日`
>>>>>>> local-backup-root-cyj
}
},
@@ -482,37 +339,16 @@
onShow() {
if (this.merchantId) {
<<<<<<< HEAD
this.loadMerchantData()
this.loadTodayStats()
this.loadPendingCounts()
this.loadRecentOrders()
} else {
setTimeout(() => {
this.loadMerchantData()
this.loadTodayStats()
this.loadPendingCounts()
this.loadRecentOrders()
=======
this.loadAllData()
this.startRealtimeSubscription()
} else {
setTimeout(() => {
this.loadAllData()
this.startRealtimeSubscription()
>>>>>>> local-backup-root-cyj
}, 500)
}
},
<<<<<<< HEAD
methods: {
formatNumber(value: number | null): string {
if (value == null) return '0.00'
return value.toFixed(2)
},
=======
onHide() {
this.stopRealtimeSubscription()
},
@@ -522,7 +358,6 @@
},
methods: {
>>>>>>> local-backup-root-cyj
async initMerchantId() {
try {
const session = supa.getSession()
@@ -537,8 +372,6 @@
}
},
<<<<<<< HEAD
=======
startRealtimeSubscription() {
if (!this.merchantId) return
@@ -586,7 +419,6 @@
return value.toFixed(2)
},
>>>>>>> local-backup-root-cyj
async loadMerchantData() {
try {
const response = await supa
@@ -596,12 +428,8 @@
.limit(1)
.execute()
<<<<<<< HEAD
if (response.error != null || !response.data || (response.data as any[]).length === 0) {
=======
if (response.error != null) { console.error('ml_shops请求500报错', response.error) }
if (response.error != null || !response.data || (response.data as any[]).length === 0) {
>>>>>>> local-backup-root-cyj
this.shopInfo = {
id: null,
merchant_id: this.merchantId,
@@ -620,11 +448,7 @@
const rawData = (response.data as any[])[0] as UTSJSONObject
this.shopInfo = {
<<<<<<< HEAD
id: rawData.getString('id') || null,
=======
id: rawData.getString('id') || null,
>>>>>>> local-backup-root-cyj
merchant_id: rawData.getString('merchant_id') || null,
shop_name: rawData.getString('shop_name') || '我的店铺',
shop_logo: rawData.getString('shop_logo') || null,
@@ -636,8 +460,6 @@
total_sales: rawData.getNumber('total_sales') || 0,
status: rawData.getNumber('status') || 1
}
<<<<<<< HEAD
=======
// 重新动态查询并计算该店铺下所有商品的真实销量总和
try {
@@ -672,7 +494,6 @@
console.error('获取店铺真实销量失败:', e)
}
>>>>>>> local-backup-root-cyj
} catch (e) {
console.error('加载店铺信息失败:', e)
}
@@ -680,21 +501,6 @@
async loadTodayStats() {
try {
<<<<<<< HEAD
const response = await supa
.from('ml_orders')
.select('total_amount, order_status', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.execute()
if (response.error != null) {
console.error('获取统计数据失败:', response.error)
return
}
let totalOrders = 0
let totalSales = 0
=======
// 1. 获取所有订单
const response = await supa
.from('ml_orders')
@@ -716,18 +522,12 @@
const now = new Date()
// 获取今日0点的毫秒数 (本地时间)
const todayStartMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
>>>>>>> local-backup-root-cyj
const rawData = response.data as any[]
if (rawData != null) {
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const status = item.getNumber('order_status')
<<<<<<< HEAD
if (status >= 2) {
totalOrders++
totalSales += item.getNumber('total_amount') || 0
=======
// 有效订单(已支付、已发货、已完成) >= 2
// 如果是退款(0)或取消(5),可能不计入今日销售额,这里按需调整
@@ -753,18 +553,10 @@
todaySales += item.getNumber('total_amount') || 0
}
}
>>>>>>> local-backup-root-cyj
}
}
}
<<<<<<< HEAD
this.todayStats = {
orders: totalOrders,
sales: totalSales,
visitors: Math.floor(totalOrders * 3),
conversion: totalOrders > 0 ? 15 : 0
=======
// 更新店铺总销量显示
let currentShopSales = Number(this.shopInfo.total_sales || 0)
if (allTimeSalesVolume > currentShopSales) {
@@ -776,7 +568,6 @@
sales: todaySales,
visitors: Math.floor(todayOrders * (2.5 + Math.random())) + 5, // 模拟访客数
conversion: todayOrders > 0 ? (12 + Math.floor(Math.random() * 8)) : 0 // 模拟转化率
>>>>>>> local-backup-root-cyj
}
} catch (e) {
console.error('获取今日统计异常:', e)
@@ -792,39 +583,24 @@
.eq('order_status', 2)
.execute()
<<<<<<< HEAD
const refundRes = await supa
=======
if (pendingShipmentRes.error != null) { console.error('pendingShipment报错', pendingShipmentRes.error) }
const refundRes = await supa
>>>>>>> local-backup-root-cyj
.from('ml_orders')
.select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.eq('order_status', 0)
.execute()
<<<<<<< HEAD
const lowStockRes = await supa
=======
if (refundRes.error != null) { console.error('refundRes报错', refundRes.error) }
const lowStockRes = await supa
>>>>>>> local-backup-root-cyj
.from('ml_products')
.select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId)
.lte('total_stock', 10)
<<<<<<< HEAD
.gte('total_stock', 0)
.execute()
this.pendingCounts = {
=======
.execute()
if (lowStockRes.error != null) { console.error('lowStockRes报错', lowStockRes.error) }
this.pendingCounts = {
>>>>>>> local-backup-root-cyj
pending_shipment: pendingShipmentRes.total || 0,
refund_requests: refundRes.total || 0,
low_stock: lowStockRes.total || 0,
@@ -841,11 +617,7 @@
.from('ml_orders')
.select(`
*,
<<<<<<< HEAD
order_items!inner (
=======
order_items (
>>>>>>> local-backup-root-cyj
id,
product_id,
product_name,
@@ -860,15 +632,8 @@
.limit(5)
.execute()
<<<<<<< HEAD
if (response.error != null || !response.data) {
this.recentOrders = []
return
}
=======
if (response.error != null) { console.error('recentOrders报错', response.error) }
if (response.error != null || !response.data) { this.recentOrders = []; return; }
>>>>>>> local-backup-root-cyj
const rawData = response.data as any[]
const ordersData: OrderType[] = []
@@ -909,33 +674,6 @@
this.recentOrders = ordersData
} catch (e) {
<<<<<<< HEAD
console.error('加载最新订单异常:', e)
}
},
getOrderStatusClass(status: number): string {
switch (status) {
case 1: return 'status-pending'
case 2: return 'status-paid'
case 3: return 'status-shipped'
case 4: return 'status-delivered'
case 5: return 'status-completed'
default: return 'status-default'
}
},
getOrderStatusText(status: number): string {
switch (status) {
case 1: return '待付款'
case 2: return '待发货'
case 3: return '已发货'
case 4: return '已收货'
case 5: return '已完成'
case 0: return '退款中'
default: return '未知状态'
}
=======
console.error('加载最新订单异常:', e); uni.showModal({title: '最新订单报错', content: e.toString()})
}
},
@@ -979,7 +717,6 @@
if (status === 4) return '已完成'
if (status === 0) return '退款中'
return '未知'
>>>>>>> local-backup-root-cyj
},
formatTime(timeStr: string): string {
@@ -988,70 +725,6 @@
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / (1000 * 60))
<<<<<<< HEAD
if (minutes < 60) {
return `${minutes}分钟前`
} else if (minutes < 1440) {
return `${Math.floor(minutes / 60)}小时前`
} else {
return `${Math.floor(minutes / 1440)}天前`
}
},
goToMessages() {
uni.navigateTo({
url: '/pages/mall/merchant/messages'
})
},
goToSettings() {
uni.navigateTo({
url: '/pages/mall/merchant/shop-edit'
})
},
goToOrders(type: string) {
uni.navigateTo({
url: `/pages/mall/merchant/orders?type=${type}`
})
},
goToProducts(type: string) {
uni.navigateTo({
url: `/pages/mall/merchant/products?type=${type}`
})
},
goToPromotions() {
uni.navigateTo({
url: '/pages/mall/merchant/promotions'
})
},
goToStatistics() {
uni.navigateTo({
url: '/pages/mall/merchant/statistics'
})
},
goToFinance() {
uni.navigateTo({
url: '/pages/mall/merchant/finance'
})
},
goToReviews() {
uni.navigateTo({
url: '/pages/mall/merchant/reviews'
})
},
goToOrderDetail(orderId: string) {
uni.navigateTo({
url: `/pages/mall/merchant/order-detail?id=${orderId}`
})
=======
if (minutes < 60) return `${minutes}分钟前`
if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`
@@ -1104,339 +777,12 @@
goToOrderDetail(orderId: string) {
uni.navigateTo({ url: `/pages/mall/merchant/order-detail?id=${orderId}` })
>>>>>>> local-backup-root-cyj
}
}
}
</script>
<style>
<<<<<<< HEAD
.merchant-container {
background-color: #f5f5f5;
min-height: 100vh;
}
.header {
background-color: #fff;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #e5e5e5;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.shop-info {
display: flex;
align-items: center;
}
.shop-logo {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 20rpx;
background-color: #f5f5f5;
}
.shop-details {
display: flex;
flex-direction: column;
}
.shop-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.shop-stats {
display: flex;
}
.stat-item {
font-size: 24rpx;
color: #666;
margin-right: 20rpx;
}
.header-actions {
display: flex;
}
.action-btn {
font-size: 28rpx;
color: #007AFF;
margin-left: 30rpx;
}
.overview-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.overview-grid {
display: flex;
justify-content: space-between;
}
.overview-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.overview-value {
font-size: 36rpx;
font-weight: bold;
color: #FF6B35;
margin-bottom: 10rpx;
}
.overview-label {
font-size: 24rpx;
color: #666;
}
.pending-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.pending-list {
display: flex;
flex-direction: column;
}
.pending-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.pending-item:last-child {
border-bottom: none;
}
.pending-icon {
font-size: 32rpx;
margin-right: 20rpx;
width: 40rpx;
}
.pending-text {
font-size: 28rpx;
color: #333;
flex: 1;
}
.pending-count {
font-size: 28rpx;
color: #FF6B35;
font-weight: bold;
}
.shortcuts-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.shortcuts-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.shortcut-item {
display: flex;
flex-direction: column;
align-items: center;
width: 30%;
margin-bottom: 30rpx;
}
.shortcut-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.shortcut-text {
font-size: 24rpx;
color: #333;
text-align: center;
}
.recent-orders-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-more {
font-size: 24rpx;
color: #007AFF;
}
.no-orders {
text-align: center;
padding: 60rpx 0;
}
.no-orders-text {
font-size: 26rpx;
color: #999;
}
.orders-list {
display: flex;
flex-direction: column;
}
.order-item {
border: 1rpx solid #e5e5e5;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.order-item:last-child {
margin-bottom: 0;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.order-no {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.order-status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
.status-pending {
background-color: #FFF3CD;
color: #856404;
}
.status-paid {
background-color: #D4EDDA;
color: #155724;
}
.status-shipped {
background-color: #CCE5FF;
color: #004085;
}
.status-delivered {
background-color: #E2E3E5;
color: #383D41;
}
.status-completed {
background-color: #D1ECF1;
color: #0C5460;
}
.order-products {
margin-bottom: 15rpx;
}
.product-item {
display: flex;
align-items: center;
margin-bottom: 10rpx;
}
.product-item:last-child {
margin-bottom: 0;
}
.product-image {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
margin-right: 15rpx;
background-color: #f5f5f5;
}
.product-info {
display: flex;
flex-direction: column;
flex: 1;
}
.product-name {
font-size: 26rpx;
color: #333;
margin-bottom: 5rpx;
}
.product-spec {
font-size: 22rpx;
color: #999;
margin-bottom: 5rpx;
}
.product-price {
font-size: 24rpx;
color: #666;
}
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-amount {
font-size: 28rpx;
color: #FF6B35;
font-weight: bold;
}
.order-time {
font-size: 22rpx;
color: #999;
}
</style>
=======
.merchant-container { background-color: #f5f7fa; min-height: 100vh; }
.main-scroll { height: 100vh; }
@@ -1547,4 +893,3 @@
>>>>>>> local-backup-root-cyj

View File

@@ -57,11 +57,6 @@
<text class="label">当前库存</text>
<text class="value">{{ currentProduct?.total_stock }}</text>
</view>
<<<<<<< HEAD
<view class="form-item">
<text class="label">新库存</text>
<input class="input" type="number" v-model="newStock" placeholder="请输入新库存"/>
=======
<view class="adjust-type">
<view class="type-btn" :class="{ active: adjustType === 'set' }" @click="adjustType = 'set'">直接设为</view>
@@ -77,16 +72,11 @@
<view class="form-item">
<text class="label">备注 (可选)</text>
<input class="input" v-model="stockRemark" placeholder="如:入库、损耗等"/>
>>>>>>> local-backup-root-cyj
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @click="closeStockModal">取消</view>
<<<<<<< HEAD
<view class="modal-btn confirm" @click="saveStock">保存</view>
=======
<view class="modal-btn confirm" @click="saveStock">确认提交</view>
>>>>>>> local-backup-root-cyj
</view>
</view>
</view>
@@ -118,13 +108,9 @@
stats: { totalProducts: 0, lowStock: 0, outOfStock: 0 },
showStockModal: false,
currentProduct: null as ProductType | null,
<<<<<<< HEAD
newStock: ''
=======
newStock: '',
adjustType: 'set', // 'set', 'add', 'sub'
stockRemark: ''
>>>>>>> local-backup-root-cyj
}
},
@@ -133,10 +119,7 @@
},
onShow() {
<<<<<<< HEAD
=======
this.page = 1
>>>>>>> local-backup-root-cyj
this.loadProducts()
this.loadStats()
},
@@ -145,31 +128,16 @@
async initMerchantId() {
try {
const session = supa.getSession()
<<<<<<< HEAD
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
=======
if (session != null && session.user != null) {
this.merchantId = session.user.getString('id') || ''
}
if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || ''
}
>>>>>>> local-backup-root-cyj
} catch (e) {}
},
async loadProducts() {
<<<<<<< HEAD
this.loading = true
try {
let query = supa.from('ml_products').select('id, name, main_image_url, total_stock, warning_stock').eq('merchant_id', this.merchantId).order('total_stock', { ascending: true }).page(this.page).limit(this.limit)
const response = await query.execute()
if (response.error != null || !response.data) {
this.products = []
=======
if (this.loading && this.page === 1) return
this.loading = true
@@ -191,41 +159,19 @@
if (response.error != null) {
console.error('加载商品失败:', response.error)
>>>>>>> local-backup-root-cyj
return
}
const rawData = response.data as any[]
<<<<<<< HEAD
let productsData: ProductType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const stock = item.getNumber('total_stock') || 0
const warning = item.getNumber('warning_stock') || 10
if (this.currentFilter === 'low' && stock > warning) continue
if (this.currentFilter === 'out' && stock > 0) continue
=======
if (!rawData) return
const productsData: ProductType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
>>>>>>> local-backup-root-cyj
productsData.push({
id: item.getString('id') || '',
name: item.getString('name') || '',
main_image_url: item.getString('main_image_url') || '',
<<<<<<< HEAD
total_stock: stock,
warning_stock: warning
})
}
this.products = productsData
=======
total_stock: item.getNumber('total_stock') || 0,
warning_stock: item.getNumber('warning_stock') || 10
} as ProductType)
@@ -238,7 +184,6 @@
}
this.hasMore = rawData.length === this.limit
>>>>>>> local-backup-root-cyj
} catch (e) {
console.error('加载失败:', e)
} finally {
@@ -288,13 +233,9 @@
editStock(product: ProductType) {
this.currentProduct = product
<<<<<<< HEAD
this.newStock = String(product.total_stock)
=======
this.newStock = ''
this.adjustType = 'set'
this.stockRemark = ''
>>>>>>> local-backup-root-cyj
this.showStockModal = true
},
@@ -305,15 +246,6 @@
},
async saveStock() {
<<<<<<< HEAD
if (!this.newStock || isNaN(parseInt(this.newStock))) {
uni.showToast({ title: '请输入有效库存', icon: 'none' })
return
}
try {
const response = await supa.from('ml_products').update({ total_stock: parseInt(this.newStock), updated_at: new Date().toISOString() }).eq('id', this.currentProduct!.id).execute()
=======
const val = parseInt(this.newStock)
if (isNaN(val)) {
uni.showToast({ title: '请输入有效数值', icon: 'none' })
@@ -345,21 +277,12 @@
})
.eq('id', this.currentProduct!.id)
.execute()
>>>>>>> local-backup-root-cyj
if (response.error != null) {
uni.showToast({ title: '保存失败', icon: 'none' })
return
}
<<<<<<< HEAD
uni.showToast({ title: '保存成功', icon: 'success' })
this.closeStockModal()
this.loadProducts()
this.loadStats()
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
=======
uni.showToast({ title: '更新成功', icon: 'success' })
this.closeStockModal()
this.page = 1
@@ -369,7 +292,6 @@
uni.showToast({ title: '操作异常', icon: 'none' })
} finally {
uni.hideLoading()
>>>>>>> local-backup-root-cyj
}
},
@@ -413,17 +335,10 @@
.action-btn { padding: 12rpx 24rpx; font-size: 24rpx; background-color: #E3F2FD; color: #1976D2; border-radius: 24rpx; }
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-content { width: 80%; background-color: #fff; border-radius: 16rpx; }
<<<<<<< HEAD
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
.modal-close { font-size: 44rpx; color: #999; }
.modal-body { padding: 30rpx; }
=======
.modal-body { padding: 30rpx; }
.adjust-type { display: flex; justify-content: space-between; margin-bottom: 30rpx; }
.type-btn { flex: 1; height: 64rpx; line-height: 64rpx; text-align: center; font-size: 24rpx; background-color: #f5f5f5; color: #666; margin: 0 10rpx; border-radius: 32rpx; border: 1rpx solid #eee; }
.type-btn.active { background-color: #E3F2FD; color: #007AFF; border-color: #007AFF; }
>>>>>>> local-backup-root-cyj
.form-item { margin-bottom: 20rpx; }
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
.form-item .value { font-size: 28rpx; color: #333; }

View File

@@ -1,47 +1,6 @@
<!-- 商家端 - 消息中心页面 -->
<template>
<view class="messages-page">
<<<<<<< HEAD
<view class="tabs">
<view class="tab" :class="{ active: currentTab === 'chat' }" @click="switchTab('chat')">会话列表</view>
<view class="tab" :class="{ active: currentTab === 'all' }" @click="switchTab('all')">全部消息</view>
</view>
<scroll-view class="messages-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
<view v-if="loading && conversations.length === 0" class="loading-container"><text class="loading-text">加载中...</text></view>
<view v-else-if="currentTab === 'chat' && conversations.length === 0" class="empty-container"><text class="empty-icon">💬</text><text class="empty-text">暂无会话</text></view>
<view v-else-if="currentTab === 'all' && messages.length === 0" class="empty-container"><text class="empty-icon">📭</text><text class="empty-text">暂无消息</text></view>
<!-- 会话列表 -->
<view v-else-if="currentTab === 'chat'">
<view v-for="conv in conversations" :key="conv.sessionId" class="conversation-card" @click="goToChat(conv)">
<image class="conv-avatar" :src="conv.avatar || '/static/images/default-avatar.png'" mode="aspectFill" />
<view class="conv-info">
<view class="conv-header">
<text class="conv-name">{{ conv.name }}</text>
<text class="conv-time">{{ conv.lastTime }}</text>
</view>
<text class="conv-preview">{{ conv.lastMessage }}</text>
</view>
<view v-if="conv.unread > 0" class="unread-badge"><text>{{ conv.unread > 99 ? '99+' : conv.unread }}</text></view>
</view>
</view>
<!-- 全部消息 -->
<view v-else>
<view v-for="msg in messages" :key="msg.id" class="message-card" :class="{ unread: !msg.is_read }" @click="viewMessage(msg)">
<view class="message-icon">{{ msg.is_from_user ? '👤' : '🏪' }}</view>
<view class="message-content">
<view class="message-header">
<text class="message-title">{{ msg.is_from_user ? '发给客户' : '收到消息' }}</text>
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
</view>
<text class="message-text">{{ msg.content }}</text>
</view>
<view v-if="!msg.is_read" class="unread-dot"></view>
</view>
</view>
=======
<view class="header">
<text class="header-title">消息</text>
<text class="header-subtitle">与客户的聊天记录</text>
@@ -82,7 +41,6 @@
</view>
<view class="safe-bottom"></view>
>>>>>>> local-backup-root-cyj
</scroll-view>
</view>
</template>
@@ -108,10 +66,7 @@
avatar: string
lastMessage: string
lastTime: string
<<<<<<< HEAD
=======
lastTimeRaw: string
>>>>>>> local-backup-root-cyj
unread: number
userId: string
}
@@ -119,11 +74,6 @@
export default {
data() {
return {
<<<<<<< HEAD
currentTab: 'chat',
messages: [] as MessageType[],
=======
>>>>>>> local-backup-root-cyj
conversations: [] as ConversationType[],
loading: false,
refreshing: false,
@@ -161,48 +111,15 @@
const response = await query.execute()
if (response.error != null || !response.data) {
<<<<<<< HEAD
this.messages = []
=======
>>>>>>> local-backup-root-cyj
this.conversations = []
return
}
const rawData = response.data as any[]
<<<<<<< HEAD
const messagesData: MessageType[] = []
=======
>>>>>>> local-backup-root-cyj
const sessionMap = new Map<string, ConversationType>()
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
<<<<<<< HEAD
const msg: MessageType = {
id: item.getString('id') || '',
session_id: item.getString('session_id') || '',
sender_id: item.getString('sender_id') || '',
receiver_id: item.getString('receiver_id') || '',
content: item.getString('content') || '',
msg_type: item.getString('msg_type') || 'text',
is_read: item.getBoolean('is_read') || false,
is_from_user: item.getBoolean('is_from_user') || false,
created_at: item.getString('created_at') || ''
}
messagesData.push(msg)
const otherUserId = msg.is_from_user ? msg.receiver_id : msg.sender_id
const sessionId = msg.session_id || otherUserId
if (!sessionMap.has(sessionId)) {
sessionMap.set(sessionId, {
sessionId: sessionId,
name: '客户',
avatar: '',
lastMessage: msg.content,
lastTime: this.formatTime(msg.created_at),
=======
const isFromUser = item.getBoolean('is_from_user') || false
const senderId = item.getString('sender_id') || ''
const receiverId = item.getString('receiver_id') || ''
@@ -227,18 +144,11 @@
lastMessage: content,
lastTime: this.formatTime(createdAt),
lastTimeRaw: createdAt,
>>>>>>> local-backup-root-cyj
unread: 0,
userId: otherUserId
})
}
<<<<<<< HEAD
const conv = sessionMap.get(sessionId)!
conv.lastMessage = msg.content
conv.lastTime = this.formatTime(msg.created_at)
if (!msg.is_read && !msg.is_from_user) {
=======
const conv = sessionMap.get(otherUserId)!
// 更新最后一条消息(按时间最新的)
if (createdAt > conv.lastTimeRaw) {
@@ -248,15 +158,10 @@
}
// 未读消息:消息来自用户且未读
if (!isRead && isFromUser) {
>>>>>>> local-backup-root-cyj
conv.unread++
}
}
<<<<<<< HEAD
this.messages = messagesData
=======
>>>>>>> local-backup-root-cyj
this.conversations = Array.from(sessionMap.values()).sort((a, b) => b.unread - a.unread)
} catch (e) {
@@ -273,33 +178,11 @@
})
},
<<<<<<< HEAD
switchTab(tab: string) {
this.currentTab = tab
},
=======
>>>>>>> local-backup-root-cyj
onRefresh() {
this.refreshing = true
this.loadMessages()
},
<<<<<<< HEAD
viewMessage(msg: MessageType) {
if (!msg.is_read) {
supa.from('ml_chat_messages').update({ is_read: true }).eq('id', msg.id).execute()
msg.is_read = true
}
const otherUserId = msg.is_from_user ? msg.receiver_id : msg.sender_id
uni.navigateTo({
url: `/pages/mall/merchant/chat?user_id=${otherUserId}&session_id=${msg.session_id}`
})
},
=======
>>>>>>> local-backup-root-cyj
formatTime(timeStr: string): string {
if (!timeStr) return ''
const date = new Date(timeStr)
@@ -319,37 +202,6 @@
</script>
<style>
<<<<<<< HEAD
.messages-page { background-color: #f5f5f5; min-height: 100vh; }
.tabs { display: flex; background-color: #fff; padding: 0 20rpx; position: sticky; top: 0; z-index: 10; }
.tab { flex: 1; text-align: center; padding: 24rpx 0; font-size: 28rpx; color: #666; position: relative; }
.tab.active { color: #007AFF; font-weight: bold; }
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40rpx; height: 4rpx; background-color: #007AFF; border-radius: 2rpx; }
.messages-list { padding: 20rpx; height: calc(100vh - 100rpx); }
.loading-container, .empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; }
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
.empty-text, .loading-text { font-size: 28rpx; color: #999; }
.conversation-card { display: flex; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; position: relative; }
.conv-avatar { width: 100rpx; height: 100rpx; border-radius: 12rpx; margin-right: 20rpx; }
.conv-info { flex: 1; }
.conv-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10rpx; }
.conv-name { font-size: 30rpx; color: #333; font-weight: 500; }
.conv-time { font-size: 22rpx; color: #999; }
.conv-preview { font-size: 26rpx; color: #666; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.unread-badge { min-width: 36rpx; height: 36rpx; background-color: #FF3B30; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; padding: 0 10rpx; }
.unread-badge text { font-size: 20rpx; color: #fff; }
.message-card { display: flex; align-items: flex-start; background-color: #fff; border-radius: 16rpx; padding: 24rpx; margin-bottom: 16rpx; position: relative; }
.message-card.unread { background-color: #f0f9ff; }
.message-icon { font-size: 40rpx; margin-right: 20rpx; }
.message-content { flex: 1; }
.message-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10rpx; }
.message-title { font-size: 28rpx; color: #333; font-weight: 500; }
.message-time { font-size: 22rpx; color: #999; }
.message-text { font-size: 26rpx; color: #666; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.unread-dot { position: absolute; top: 30rpx; right: 30rpx; width: 16rpx; height: 16rpx; background-color: #FF3B30; border-radius: 50%; }
=======
.messages-page { background-color: #f5f7fa; min-height: 100vh; display: flex; flex-direction: column; }
.header { background-color: #fff; padding-top: 60rpx; padding-bottom: 24rpx; padding-left: 30rpx; padding-right: 30rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #eee; }
@@ -383,5 +235,4 @@
.conv-arrow { font-size: 40rpx; color: #ccc; margin-left: 10rpx; }
.safe-bottom { height: 30rpx; }
>>>>>>> local-backup-root-cyj
</style>

View File

@@ -1,8 +1,4 @@
<<<<<<< HEAD
<!-- 商家端 - 订单详情页面 -->
=======
<!-- 商家端 - 订单详情页面 -->
>>>>>>> local-backup-root-cyj
<template>
<view class="order-detail-page">
<!-- 订单状态头部 -->
@@ -118,32 +114,20 @@
<!-- 操作按钮 -->
<view class="action-buttons">
<view
<<<<<<< HEAD
v-if="order.order_status === 1"
=======
v-if="order.order_status === 2"
>>>>>>> local-backup-root-cyj
class="action-btn primary"
@click="shipOrder"
>
去发货
</view>
<view
<<<<<<< HEAD
v-if="order.order_status === 2"
=======
v-if="order.order_status === 3"
>>>>>>> local-backup-root-cyj
class="action-btn primary"
@click="viewLogistics"
>
查看物流
</view>
<<<<<<< HEAD
<view
=======
<view
>>>>>>> local-backup-root-cyj
v-if="order.order_status === 3"
class="action-btn primary"
@click="confirmDelivery"
@@ -179,11 +163,7 @@
@change="onLogisticsChange"
>
<view class="picker-value">
<<<<<<< HEAD
{{ selectedLogistics.name || '请选择物流公司' }}
=======
{{ selectedLogistics?.name || '请选择物流公司' }}
>>>>>>> local-backup-root-cyj
</view>
</picker>
</view>
@@ -260,10 +240,6 @@
updated_at: '',
items: [] as OrderItemType[]
},
<<<<<<< HEAD
addressData: {} as AddressType,
=======
addressData: {
recipient_name: '',
phone: '',
@@ -273,7 +249,6 @@
detail_address: ''
} as AddressType,
>>>>>>> local-backup-root-cyj
showShipModal: false,
logisticsCompanies: [
{ name: '顺丰速运', code: 'SF' },
@@ -284,20 +259,11 @@
{ name: 'EMS', code: 'EMS' },
{ name: '京东物流', code: 'JD' }
] as LogisticsType[],
<<<<<<< HEAD
selectedLogistics: {} as LogisticsType,
=======
selectedLogistics: { name: '', code: '' } as LogisticsType,
>>>>>>> local-backup-root-cyj
trackingNumber: ''
}
},
<<<<<<< HEAD
onLoad(options: any) {
const id = options.id as string
if (id) {
=======
onLoad(options: any) { console.log('--- DEBUG ON LOAD ---', options)
let id = ''
if (options['id'] != null) {
@@ -307,28 +273,18 @@
}
if (id !== '') {
>>>>>>> local-backup-root-cyj
this.orderId = id
this.loadOrderDetail()
}
},
methods: {
<<<<<<< HEAD
async loadOrderDetail() {
try {
=======
async loadOrderDetail() { console.log('--- DEBUG LOAD ORDER DETAIL ---', this.orderId); try {
>>>>>>> local-backup-root-cyj
const response = await supa
.from('ml_orders')
.select(`
*,
<<<<<<< HEAD
order_items!inner (
=======
ml_order_items (
>>>>>>> local-backup-root-cyj
id,
order_id,
product_id,
@@ -345,44 +301,12 @@
.single()
.execute()
<<<<<<< HEAD
if (response.error != null) {
=======
if (response.error != null || (response.status ?? 200) >= 400) {
>>>>>>> local-backup-root-cyj
console.error('获取订单详情失败:', response.error)
uni.showToast({ title: '加载失败', icon: 'none' })
return
}
<<<<<<< HEAD
const rawData = response.data as UTSJSONObject
if (rawData == null) return
this.order = {
id: rawData.getString('id') || '',
order_no: rawData.getString('order_no') || '',
user_id: rawData.getString('user_id') || '',
merchant_id: rawData.getString('merchant_id') || '',
order_status: rawData.getNumber('order_status') || 1,
total_amount: rawData.getNumber('total_amount') || 0,
product_amount: rawData.getNumber('product_amount') || 0,
shipping_fee: rawData.getNumber('shipping_fee') || 0,
discount_amount: rawData.getNumber('discount_amount') || 0,
paid_amount: rawData.getNumber('paid_amount') || 0,
shipping_address: rawData.getString('shipping_address') || '{}',
remark: rawData.getString('remark') || '',
shipping_company: rawData.getString('shipping_company') || '',
tracking_number: rawData.getString('tracking_number') || '',
paid_at: rawData.getString('paid_at') || '',
shipped_at: rawData.getString('shipped_at') || '',
created_at: rawData.getString('created_at') || '',
updated_at: rawData.getString('updated_at') || '',
items: []
}
const itemsObj = rawData.get('order_items')
=======
console.log('--- DEBUG RAW ORDER DATA ---', response.data); let realData = response.data;
let isArrLike = false;
if (response.data != null && (response.data as any)['0'] != null) {
@@ -416,23 +340,11 @@
}
const itemsObj = rawData['ml_order_items']
>>>>>>> local-backup-root-cyj
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArray = itemsObj as any[]
for (let i = 0; i < itemsArray.length; i++) {
const orderItem = itemsArray[i] as UTSJSONObject
this.order.items.push({
<<<<<<< HEAD
id: orderItem.getString('id') || '',
order_id: orderItem.getString('order_id') || '',
product_id: orderItem.getString('product_id') || '',
sku_id: orderItem.getString('sku_id') || '',
product_name: orderItem.getString('product_name') || '',
sku_name: orderItem.getString('sku_name') || '',
price: orderItem.getNumber('price') || 0,
quantity: orderItem.getNumber('quantity') || 0,
image_url: orderItem.getString('image_url') || '',
=======
id: String(orderItem['id'] ?? '') || '',
order_id: String(orderItem['order_id'] ?? '') || '',
product_id: String(orderItem['product_id'] ?? '') || '',
@@ -442,7 +354,6 @@
price: Number(orderItem['price'] ?? 0) || 0,
quantity: Number(orderItem['quantity'] ?? 0) || 0,
image_url: String(orderItem['image_url'] ?? '') || '',
>>>>>>> local-backup-root-cyj
sku_snapshot: ''
} as OrderItemType)
}
@@ -550,11 +461,7 @@
},
async confirmShip() {
<<<<<<< HEAD
if (!this.selectedLogistics.name) {
=======
if (this.selectedLogistics == null || !this.selectedLogistics?.name) {
>>>>>>> local-backup-root-cyj
uni.showToast({ title: '请选择物流公司', icon: 'none' })
return
}
@@ -568,39 +475,23 @@
.from('ml_orders')
.update({
order_status: 3,
<<<<<<< HEAD
shipping_company: this.selectedLogistics.name,
tracking_number: this.trackingNumber,
=======
shipping_status: 2,
carrier_name: this.selectedLogistics?.name, tracking_no: this.trackingNumber,
>>>>>>> local-backup-root-cyj
shipped_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.order.id)
.execute()
<<<<<<< HEAD
if (response.error != null) {
uni.showToast({ title: '发货失败', icon: 'none' })
=======
if (response.error != null || (response.status ?? 200) >= 400) {
let msg = response.error?.message ?? (response.data != null ? JSON.stringify(response.data) : '请检查网络或登录状态'); uni.showToast({ title: '发货被拦截: ' + msg, icon: 'none', duration: 4500 }); console.error('SUPABASE API ERR:', response)
>>>>>>> local-backup-root-cyj
return
}
uni.showToast({ title: '发货成功', icon: 'success' })
this.closeShipModal()
this.loadOrderDetail()
<<<<<<< HEAD
} catch (e) {
uni.showToast({ title: '发货失败', icon: 'none' })
}
=======
} catch (e) { uni.showToast({ title: '发货发生异常', icon: 'none' }); console.error(e) }
>>>>>>> local-backup-root-cyj
},
viewLogistics() {
@@ -627,11 +518,7 @@
.eq('id', this.order.id)
.execute()
<<<<<<< HEAD
if (response.error != null) {
=======
if (response.error != null || (response.status ?? 200) >= 400) {
>>>>>>> local-backup-root-cyj
uni.showToast({ title: '操作失败', icon: 'none' })
return
}
@@ -659,11 +546,7 @@
.eq('id', this.order.id)
.execute()
<<<<<<< HEAD
if (response.error != null) {
=======
if (response.error != null || (response.status ?? 200) >= 400) {
>>>>>>> local-backup-root-cyj
uni.showToast({ title: '删除失败', icon: 'none' })
return
}
@@ -975,11 +858,7 @@
display: flex;
align-items: flex-end;
justify-content: center;
<<<<<<< HEAD
z-index: 1000;
=======
z-index: 99;
>>>>>>> local-backup-root-cyj
}
.modal-content {
@@ -987,11 +866,8 @@
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
<<<<<<< HEAD
=======
position: relative;
z-index: 99;
>>>>>>> local-backup-root-cyj
}
.modal-header {
@@ -1065,24 +941,4 @@
color: #007AFF;
font-weight: bold;
}
</style>
<<<<<<< HEAD
=======
>>>>>>> local-backup-root-cyj
</style>

View File

@@ -1,8 +1,4 @@
<<<<<<< HEAD
<!-- 商家端 - 订单管理页面 -->
=======
<!-- 商家端 - 订单管理页面 -->
>>>>>>> local-backup-root-cyj
<template>
<view class="orders-page">
<!-- 标签页切换 -->
@@ -100,22 +96,14 @@
</view>
<view class="order-actions">
<view
<<<<<<< HEAD
v-if="order.order_status === 1"
=======
v-if="order.order_status === 2"
>>>>>>> local-backup-root-cyj
class="action-btn primary"
@click.stop="shipOrder(order)"
>
发货
</view>
<view
<<<<<<< HEAD
v-if="order.order_status === 2"
=======
v-if="order.order_status === 3"
>>>>>>> local-backup-root-cyj
class="action-btn info"
@click.stop="viewLogistics(order)"
>
@@ -159,11 +147,7 @@
@change="onLogisticsChange"
>
<view class="picker-value">
<<<<<<< HEAD
{{ selectedLogistics.name || '请选择物流公司' }}
=======
{{ selectedLogistics?.name || '请选择物流公司' }}
>>>>>>> local-backup-root-cyj
</view>
</picker>
</view>
@@ -238,11 +222,7 @@
{ name: '待发货', status: 2, count: 0 },
{ name: '待收货', status: 3, count: 0 },
{ name: '已完成', status: 4, count: 0 },
<<<<<<< HEAD
{ name: '退款', status: 0, count: 0 }
=======
{ name: '退款', status: 6, count: 0 }
>>>>>>> local-backup-root-cyj
] as TabType[],
currentTab: -2,
searchKeyword: '',
@@ -277,11 +257,7 @@
const statusMap: Record<string, number> = {
'pending': 1,
'shipped': 3,
<<<<<<< HEAD
'refund': 0,
=======
'refund': 6,
>>>>>>> local-backup-root-cyj
'completed': 4
}
this.currentTab = statusMap[type] ?? -2
@@ -325,11 +301,7 @@
.from('ml_orders')
.select(`
*,
<<<<<<< HEAD
order_items!inner (
=======
order_items (
>>>>>>> local-backup-root-cyj
id,
order_id,
product_id,
@@ -348,14 +320,9 @@
.limit(this.limit)
if (this.currentTab !== -2) {
<<<<<<< HEAD
if (this.currentTab === 0) {
query = query.eq('order_status', 0)
=======
if (this.currentTab === 6) {
// 退款状态同时查询 0 和 6
query = query.in('order_status', [0, 6])
>>>>>>> local-backup-root-cyj
} else {
query = query.eq('order_status', this.currentTab)
}
@@ -367,11 +334,7 @@
const response = await query.execute()
<<<<<<< HEAD
if (response.error != null) {
=======
if (response.error != null || (response.status ?? 200) >= 400) {
>>>>>>> local-backup-root-cyj
console.error('获取订单失败:', response.error)
uni.showToast({ title: '加载失败', icon: 'none' })
return
@@ -387,32 +350,20 @@
const ordersData: OrderType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i]
<<<<<<< HEAD
const orderObj = item as UTSJSONObject
=======
const str = JSON.stringify(item)
const orderObj = JSON.parse(str) as UTSJSONObject
>>>>>>> local-backup-root-cyj
const order: OrderType = {
id: orderObj.getString('id') || '',
order_no: orderObj.getString('order_no') || '',
user_id: orderObj.getString('user_id') || '',
merchant_id: orderObj.getString('merchant_id') || '',
<<<<<<< HEAD
order_status: orderObj.getNumber('order_status') || 1,
=======
order_status: orderObj.getNumber('order_status') ?? (orderObj.get('order_status') == null ? 1 : (orderObj.get('order_status') as number)),
>>>>>>> local-backup-root-cyj
total_amount: orderObj.getNumber('total_amount') || 0,
product_amount: orderObj.getNumber('product_amount') || 0,
shipping_fee: orderObj.getNumber('shipping_fee') || 0,
paid_amount: orderObj.getNumber('paid_amount') || 0,
<<<<<<< HEAD
shipping_address: orderObj.getString('shipping_address') || '',
=======
shipping_address: orderObj.get('shipping_address') != null ? (typeof orderObj.get('shipping_address') === 'string' ? orderObj.getString('shipping_address')! : JSON.stringify(orderObj.get('shipping_address'))) : '',
>>>>>>> local-backup-root-cyj
remark: orderObj.getString('remark') || '',
created_at: orderObj.getString('created_at') || '',
updated_at: orderObj.getString('updated_at') || '',
@@ -423,14 +374,10 @@
if (itemsObj != null && Array.isArray(itemsObj)) {
const itemsArray = itemsObj as any[]
for (let j = 0; j < itemsArray.length; j++) {
<<<<<<< HEAD
const orderItem = itemsArray[j] as UTSJSONObject
=======
const rawItem = itemsArray[j]
const itemStr = JSON.stringify(rawItem)
const orderItem = JSON.parse(itemStr) as UTSJSONObject
>>>>>>> local-backup-root-cyj
order.items.push({
id: orderItem.getString('id') || '',
order_id: orderItem.getString('order_id') || '',
@@ -482,10 +429,6 @@
const rawData = response.data as any[]
if (rawData != null) {
for (let i = 0; i < rawData.length; i++) {
<<<<<<< HEAD
const item = rawData[i] as UTSJSONObject
const status = item.getNumber('order_status') || 1
=======
const row = rawData[i]
const istr = JSON.stringify(row)
const item = JSON.parse(istr) as UTSJSONObject
@@ -495,16 +438,11 @@
status = (typeof status_val === 'number') ? (status_val as number) : parseInt(status_val.toString())
}
>>>>>>> local-backup-root-cyj
if (status === 1) counts[1]++
else if (status === 2) counts[2]++
else if (status === 3) counts[3]++
else if (status === 4) counts[4]++
<<<<<<< HEAD
else if (status === 0) counts[0]++
=======
else if (status === 0 || status === 6) counts[0]++
>>>>>>> local-backup-root-cyj
total++
}
}
@@ -574,11 +512,7 @@
},
async confirmShip() {
<<<<<<< HEAD
if (!this.selectedLogistics.name) {
=======
if (this.selectedLogistics == null || !this.selectedLogistics?.name) {
>>>>>>> local-backup-root-cyj
uni.showToast({ title: '请选择物流公司', icon: 'none' })
return
}
@@ -588,22 +522,6 @@
}
try {
<<<<<<< HEAD
const response = await supa
.from('ml_orders')
.update({
order_status: 3,
shipping_company: this.selectedLogistics.name,
tracking_number: this.trackingNumber,
shipped_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.eq('id', this.currentOrder!.id)
.execute()
if (response.error != null) {
uni.showToast({ title: '发货失败', icon: 'none' })
=======
const payloadStr = JSON.stringify({
order_status: 3,
shipping_status: 2,
@@ -627,7 +545,6 @@
msg = rData.getString('message') ?? rData.getString('code') ?? JSON.stringify(rData);
}
if (!msg) msg = '请检查网络或登录状态'; uni.showToast({ title: '发货被拦截: ' + msg, icon: 'none', duration: 4500 }); console.error('SUPABASE API ERR:', response)
>>>>>>> local-backup-root-cyj
return
}
@@ -635,22 +552,12 @@
this.closeShipModal()
this.loadOrders()
this.loadOrderCounts()
<<<<<<< HEAD
} catch (e) {
uni.showToast({ title: '发货失败', icon: 'none' })
}
=======
} catch (e) { uni.showToast({ title: '发货发生异常', icon: 'none' }); console.error(e) }
>>>>>>> local-backup-root-cyj
},
viewLogistics(order: OrderType) {
uni.navigateTo({
<<<<<<< HEAD
url: `/pages/mall/merchant/logistics?orderId=${order.id}`
=======
url: `/pages/mall/consumer/logistics?orderId=${order.id}`
>>>>>>> local-backup-root-cyj
})
},
@@ -667,11 +574,7 @@
.eq('id', order.id)
.execute()
<<<<<<< HEAD
if (response.error != null) {
=======
if (response.error != null || (response.status ?? 200) >= 400) {
>>>>>>> local-backup-root-cyj
uni.showToast({ title: '删除失败', icon: 'none' })
return
}
@@ -692,12 +595,8 @@
if (status === 2) return '待发货'
if (status === 3) return '待收货'
if (status === 4) return '已完成'
<<<<<<< HEAD
if (status === 0) return '退款中'
=======
if (status === 0 || status === 6) return '退款/售后'
if (status === 7) return '退货完成'
>>>>>>> local-backup-root-cyj
if (status === 5 || status === -1) return '已取消'
return '未知'
},
@@ -838,14 +737,9 @@
.order-card {
background-color: #fff;
<<<<<<< HEAD
border-radius: 16rpx;
margin-bottom: 20rpx;
=======
border-radius: 20rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
>>>>>>> local-backup-root-cyj
overflow: hidden;
}
@@ -944,12 +838,6 @@
-webkit-box-orient: vertical;
}
<<<<<<< HEAD
.product-spec {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
=======
.product-name {
font-size: 26rpx;
color: #333;
@@ -970,28 +858,20 @@
padding: 4rpx 12rpx;
border-radius: 4rpx;
align-self: flex-start;
>>>>>>> local-backup-root-cyj
}
.product-right {
display: flex;
flex-direction: column;
align-items: flex-end;
<<<<<<< HEAD
=======
margin-left: 20rpx;
min-width: 120rpx;
>>>>>>> local-backup-root-cyj
}
.product-price {
font-size: 26rpx;
color: #333;
<<<<<<< HEAD
font-weight: 500;
=======
font-weight: bold;
>>>>>>> local-backup-root-cyj
}
.product-quantity {
@@ -1004,14 +884,9 @@
display: flex;
justify-content: space-between;
align-items: center;
<<<<<<< HEAD
padding: 20rpx 24rpx;
border-top: 1rpx solid #f5f5f5;
=======
padding: 24rpx;
border-top: 1rpx solid #f8f8f8;
background-color: #fafafa;
>>>>>>> local-backup-root-cyj
}
.order-amount {
@@ -1025,11 +900,7 @@
}
.amount-value {
<<<<<<< HEAD
font-size: 28rpx;
=======
font-size: 32rpx;
>>>>>>> local-backup-root-cyj
color: #FF3B30;
font-weight: bold;
margin-left: 10rpx;
@@ -1041,11 +912,6 @@
}
.action-btn {
<<<<<<< HEAD
padding: 12rpx 24rpx;
font-size: 24rpx;
border-radius: 28rpx;
=======
min-width: 120rpx;
height: 56rpx;
display: flex;
@@ -1055,24 +921,11 @@
border-radius: 28rpx;
border: 1rpx solid #eee;
padding: 0 20rpx;
>>>>>>> local-backup-root-cyj
}
.action-btn.primary {
background-color: #007AFF;
color: #fff;
<<<<<<< HEAD
}
.action-btn.info {
background-color: #E3F2FD;
color: #1976D2;
}
.action-btn.default {
background-color: #F5F5F5;
color: #666;
=======
border: none;
}
@@ -1086,7 +939,6 @@
background-color: #fff;
color: #666;
border-color: #ddd;
>>>>>>> local-backup-root-cyj
}
.load-more, .no-more {
@@ -1109,11 +961,7 @@
display: flex;
align-items: flex-end;
justify-content: center;
<<<<<<< HEAD
z-index: 1000;
=======
z-index: 99;
>>>>>>> local-backup-root-cyj
}
.modal-content {
@@ -1121,11 +969,8 @@
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
<<<<<<< HEAD
=======
position: relative;
z-index: 99;
>>>>>>> local-backup-root-cyj
}
.modal-header {
@@ -1199,20 +1044,4 @@
color: #007AFF;
font-weight: bold;
}
</style>
<<<<<<< HEAD
=======
>>>>>>> local-backup-root-cyj
</style>

View File

@@ -1,8 +1,4 @@
<<<<<<< HEAD
<!-- 商家端 - 商品管理详情页 -->
=======
<!-- 商家端 - 商品管理详情页 -->
>>>>>>> local-backup-root-cyj
<template>
<view class="product-manage-detail">
<!-- 商品基本信息 -->
@@ -48,13 +44,8 @@
</view>
<view class="info-item">
<text class="info-label">商品状态</text>
<<<<<<< HEAD
<text class="info-value" :class="{ 'status-on': product.status === 1, 'status-off': product.status === 0 }">
{{ product.status === 1 ? '上架' : '下架' }}
=======
<text class="info-value" :class="{ 'status-on': product.status === 1, 'status-off': product.status === 2 || product.status === 0 }">
{{ product.status === 1 ? '上架' : (product.status === 2 || product.status === 0 ? '下架' : '其他') }}
>>>>>>> local-backup-root-cyj
</text>
</view>
</view>
@@ -79,11 +70,7 @@
<view class="sku-details">
<text class="sku-price">¥{{ sku.price }}</text>
<text class="sku-stock">库存: {{ sku.stock }}</text>
<<<<<<< HEAD
<text class="sku-status" :class="{ 'status-on': sku.status === 1, 'status-off': sku.status === 0 }">
=======
<text class="sku-status" :class="{ 'status-on': sku.status === 1, 'status-off': sku.status === 2 || sku.status === 0 }">
>>>>>>> local-backup-root-cyj
{{ sku.status === 1 ? '启用' : '禁用' }}
</text>
</view>
@@ -718,7 +705,3 @@ export default {
color: #fff;
}
</style>
<<<<<<< HEAD
=======
>>>>>>> local-backup-root-cyj

View File

@@ -1,8 +1,4 @@
<<<<<<< HEAD
<!-- 商家端 - 商品编辑页面 -->
=======
 <!-- 商家端 - 商品编辑页面 (已修复缓存) -->
>>>>>>> local-backup-root-cyj
<template>
<view class="product-edit-page">
<!-- 商品基本信息 -->
@@ -135,10 +131,6 @@
</view>
</view>
<<<<<<< HEAD
<view class="form-item">
<text class="label">总库存 *</text>
=======
<view class="form-item">
<text class="label">VIP独立折扣</text>
<switch :checked="product.is_vip_discount" @change="e => { product.is_vip_discount = e.detail.value as boolean }" />
@@ -156,7 +148,6 @@
<view class="form-item">
<text class="label">总库存 *</text>
>>>>>>> local-backup-root-cyj
<input
class="input"
type="number"
@@ -176,8 +167,6 @@
</view>
</view>
<<<<<<< HEAD
=======
<!-- 会员阶梯价 -->
<view class="section">
<view class="section-title">会员等级价格 (选填)</view>
@@ -197,7 +186,6 @@
</view>
</view>
>>>>>>> local-backup-root-cyj
<!-- 商品属性 -->
<view class="section">
<view class="section-title">商品属性</view>
@@ -277,8 +265,6 @@
logo_url: string
}
<<<<<<< HEAD
=======
type MemberLevelType = {
id: string
name: string
@@ -287,7 +273,6 @@
price: string // 绑定输入框用
}
>>>>>>> local-backup-root-cyj
export default {
data() {
return {
@@ -299,10 +284,7 @@
brands: [] as BrandType[],
brandIndex: -1,
selectedBrand: null as BrandType | null,
<<<<<<< HEAD
=======
memberLevels: [] as MemberLevelType[],
>>>>>>> local-backup-root-cyj
product: {
name: '',
subtitle: '',
@@ -316,17 +298,11 @@
total_stock: '',
warning_stock: '10',
unit: '件',
<<<<<<< HEAD
is_hot: false,
is_new: false,
is_featured: false,
=======
is_hot: false,
is_new: false,
is_featured: false,
is_vip_discount: true,
vip_discount_rate: '',
>>>>>>> local-backup-root-cyj
description: ''
},
merchantId: ''
@@ -334,13 +310,6 @@
},
onLoad(options: any) {
<<<<<<< HEAD
const productId = options.productId as string
if (productId) {
this.productId = productId
this.isEdit = true
this.loadProductDetail(productId)
=======
let productId = ''
if (options) {
const keys = Object.keys(options as object)
@@ -371,15 +340,11 @@
this.loadProductDetail(productId)
} else {
uni.setNavigationBarTitle({ title: '添加商品' })
>>>>>>> local-backup-root-cyj
}
this.initMerchantId()
this.loadCategories()
this.loadBrands()
<<<<<<< HEAD
=======
this.loadMemberLevels()
>>>>>>> local-backup-root-cyj
},
methods: {
@@ -387,11 +352,7 @@
try {
const session = supa.getSession()
if (session != null && session.user != null) {
<<<<<<< HEAD
this.merchantId = session.user.getString('id') || ''
=======
this.merchantId = (session.user as any)['id'] != null ? String((session.user as any)['id']) : ''
>>>>>>> local-backup-root-cyj
}
if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || ''
@@ -401,8 +362,6 @@
}
},
<<<<<<< HEAD
=======
async loadMemberLevels() {
try {
const response = await supa
@@ -472,7 +431,6 @@
}
},
>>>>>>> local-backup-root-cyj
async loadCategories() {
try {
const response = await supa
@@ -491,17 +449,10 @@
if (rawData == null) return
for (let i = 0; i < rawData.length; i++) {
<<<<<<< HEAD
const item = rawData[i] as UTSJSONObject
this.categories.push({
id: item.getString('id') || '',
name: item.getString('name') || ''
=======
const item = rawData[i] as any
this.categories.push({
id: item['id'] != null ? String(item['id']) : '',
name: item['name'] != null ? String(item['name']) : ''
>>>>>>> local-backup-root-cyj
} as CategoryType)
}
} catch (e) {
@@ -527,19 +478,11 @@
if (rawData == null) return
for (let i = 0; i < rawData.length; i++) {
<<<<<<< HEAD
const item = rawData[i] as UTSJSONObject
this.brands.push({
id: item.getString('id') || '',
name: item.getString('name') || '',
logo_url: item.getString('logo_url') || ''
=======
const item = rawData[i] as any
this.brands.push({
id: item['id'] != null ? String(item['id']) : '',
name: item['name'] != null ? String(item['name']) : '',
logo_url: item['logo_url'] != null ? String(item['logo_url']) : ''
>>>>>>> local-backup-root-cyj
} as BrandType)
}
} catch (e) {
@@ -549,10 +492,7 @@
async loadProductDetail(productId: string) {
try {
<<<<<<< HEAD
=======
uni.showLoading({ title: '加载商品中...' })
>>>>>>> local-backup-root-cyj
const response = await supa
.from('ml_products')
.select('*')
@@ -560,35 +500,6 @@
.single()
.execute()
<<<<<<< HEAD
if (response.error != null) {
console.error('获取商品详情失败:', response.error)
return
}
const rawData = response.data as UTSJSONObject
if (rawData == null) return
this.product = {
name: rawData.getString('name') || '',
subtitle: rawData.getString('subtitle') || '',
category_id: rawData.getString('category_id') || '',
brand_id: rawData.getString('brand_id') || '',
main_image_url: rawData.getString('main_image_url') || '',
imageList: this.parseImageUrls(rawData.getString('image_urls')),
base_price: rawData.getString('base_price') || '',
market_price: rawData.getString('market_price') || '',
cost_price: rawData.getString('cost_price') || '',
total_stock: rawData.getString('total_stock') || '',
warning_stock: rawData.getString('warning_stock') || '10',
unit: rawData.getString('unit') || '件',
is_hot: rawData.getBoolean('is_hot') || false,
is_new: rawData.getBoolean('is_new') || false,
is_featured: rawData.getBoolean('is_featured') || false,
description: rawData.getString('description') || ''
}
=======
uni.hideLoading()
if (response.error != null) {
console.error('获取详情失败:', response.error)
@@ -628,7 +539,6 @@
this.product.vip_discount_rate = getStr('vip_discount_rate')
this.product.description = getStr('description')
>>>>>>> local-backup-root-cyj
if (this.product.category_id) {
this.categoryIndex = this.categories.findIndex(c => c.id === this.product.category_id)
if (this.categoryIndex >= 0) {
@@ -643,13 +553,9 @@
}
}
} catch (e) {
<<<<<<< HEAD
console.error('获取商品详情异常:', e)
=======
uni.hideLoading()
console.error('获取商品详情异常:', e)
uni.showToast({ title: '加载异常: ' + String(e), icon: 'none', duration: 3000 })
>>>>>>> local-backup-root-cyj
}
},
@@ -702,88 +608,6 @@
this.product.imageList.splice(index, 1)
},
<<<<<<< HEAD
async saveProduct() {
if (!this.product.name) {
uni.showToast({ title: '请输入商品名称', icon: 'none' })
return
}
if (!this.product.category_id) {
uni.showToast({ title: '请选择商品分类', icon: 'none' })
return
}
if (!this.product.base_price) {
uni.showToast({ title: '请输入销售价', icon: 'none' })
return
}
if (!this.product.total_stock) {
uni.showToast({ title: '请输入总库存', icon: 'none' })
return
}
uni.showLoading({ title: '保存中...' })
try {
const imageUrlsStr = JSON.stringify(this.product.imageList)
const productData = {
merchant_id: this.merchantId,
name: this.product.name,
subtitle: this.product.subtitle,
category_id: this.product.category_id,
brand_id: this.product.brand_id || null,
main_image_url: this.product.main_image_url,
image_urls: imageUrlsStr,
base_price: parseFloat(this.product.base_price),
market_price: this.product.market_price ? parseFloat(this.product.market_price) : null,
cost_price: this.product.cost_price ? parseFloat(this.product.cost_price) : null,
total_stock: parseInt(this.product.total_stock),
warning_stock: parseInt(this.product.warning_stock) || 10,
unit: this.product.unit,
is_hot: this.product.is_hot,
is_new: this.product.is_new,
is_featured: this.product.is_featured,
description: this.product.description,
status: 1,
updated_at: new Date().toISOString()
}
let response
if (this.isEdit) {
response = await supa
.from('ml_products')
.update(productData)
.eq('id', this.productId)
.execute()
} else {
productData['created_at'] = new Date().toISOString()
response = await supa
.from('ml_products')
.insert(productData)
.execute()
}
uni.hideLoading()
if (response.error != null) {
console.error('保存商品失败:', response.error)
uni.showToast({ title: '保存失败', icon: 'none' })
return
}
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (e) {
uni.hideLoading()
console.error('保存商品异常:', e)
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
}
}
=======
async uploadImageToSupa(localPath: string): Promise<string> {
if (localPath.startsWith('http://') || localPath.startsWith('https://')) {
return localPath
@@ -979,7 +803,6 @@
}
}
}
>>>>>>> local-backup-root-cyj
</script>
<style>
@@ -1004,8 +827,6 @@
border-bottom: 1rpx solid #f5f5f5;
}
<<<<<<< HEAD
=======
.section-desc {
font-size: 24rpx;
color: #999;
@@ -1013,7 +834,6 @@
margin-bottom: 30rpx;
}
>>>>>>> local-backup-root-cyj
.form-item {
margin-bottom: 30rpx;
}
@@ -1170,9 +990,3 @@
color: #fff;
}
</style>
<<<<<<< HEAD
=======
>>>>>>> local-backup-root-cyj

View File

@@ -1,8 +1,4 @@
<<<<<<< HEAD
<!-- 商家端 - 商品管理列表页面 -->
=======
<!-- 商家端 - 商品管理列表页面 -->
>>>>>>> local-backup-root-cyj
<template>
<view class="products-page">
<!-- 搜索栏 -->
@@ -88,20 +84,12 @@
</text>
</view>
<text class="product-subtitle">{{ product.subtitle || '暂无描述' }}</text>
<<<<<<< HEAD
<view class="product-tags" v-if="product.tags">
<text v-if="product.is_hot" class="tag hot">热</text>
<text v-if="product.is_new" class="tag new">新</text>
<text v-if="product.is_featured" class="tag recommend">荐</text>
</view>
=======
<view class="product-tags">
<text v-if="product.is_hot" class="tag hot">热</text>
<text v-if="product.is_new" class="tag new">新</text>
<text v-if="product.is_featured" class="tag recommend">荐</text>
<text v-if="product.is_vip_discount" class="tag vip">VIP</text>
</view>
>>>>>>> local-backup-root-cyj
<view class="product-stats">
<view class="price-row">
<text class="current-price">¥{{ product.base_price }}</text>
@@ -235,11 +223,7 @@
if (this.currentFilter === 'onsale') {
query = query.eq('status', 1)
} else if (this.currentFilter === 'offsale') {
<<<<<<< HEAD
query = query.eq('status', 0)
=======
query = query.eq('status', 2)
>>>>>>> local-backup-root-cyj
} else if (this.currentFilter === 'low_stock') {
query = query.lte('total_stock', this.lowStockThreshold).gte('total_stock', 0)
}
@@ -276,11 +260,7 @@
market_price: prodObj.getNumber('market_price') || 0,
total_stock: prodObj.getNumber('total_stock') || 0,
sale_count: prodObj.getNumber('sale_count') || 0,
<<<<<<< HEAD
status: prodObj.getNumber('status') || 0,
=======
status: prodObj.getNumber('status') || 1,
>>>>>>> local-backup-root-cyj
is_hot: prodObj.getBoolean('is_hot') || false,
is_new: prodObj.getBoolean('is_new') || false,
is_featured: prodObj.getBoolean('is_featured') || false,
@@ -353,11 +333,7 @@
},
async toggleStatus(product: ProductType) {
<<<<<<< HEAD
const newStatus = product.status === 1 ? 0 : 1
=======
const newStatus = product.status === 1 ? 2 : 1
>>>>>>> local-backup-root-cyj
const actionText = newStatus === 1 ? '上架' : '下架'
uni.showModal({
@@ -420,21 +396,13 @@
getStatusClass(status: number): string {
if (status === 1) return 'status-onsale'
<<<<<<< HEAD
if (status === 0) return 'status-offsale'
=======
if (status === 2 || status === 0) return 'status-offsale'
>>>>>>> local-backup-root-cyj
return 'status-pending'
},
getStatusText(status: number): string {
if (status === 1) return '在售'
<<<<<<< HEAD
if (status === 0) return '已下架'
=======
if (status === 2 || status === 0) return '已下架'
>>>>>>> local-backup-root-cyj
return '待审核'
}
}
@@ -634,12 +602,6 @@
color: #fff;
}
<<<<<<< HEAD
.tag.recommend {
background-color: #9C27B0;
color: #fff;
}
=======
.tag.recommend {
background-color: #9C27B0;
color: #fff;
@@ -650,7 +612,6 @@
color: #333;
font-weight: bold;
}
>>>>>>> local-backup-root-cyj
.product-stats {
display: flex;
@@ -756,9 +717,3 @@
font-weight: bold;
}
</style>
<<<<<<< HEAD
=======
>>>>>>> local-backup-root-cyj

View File

@@ -135,9 +135,6 @@
}
})
},
<<<<<<< HEAD
=======
async uploadImageToSupa(localPath: string): Promise<string> {
if (localPath.startsWith('http://') || localPath.startsWith('https://')) {
@@ -173,22 +170,12 @@
}
},
>>>>>>> local-backup-root-cyj
async saveShop() {
if (!this.shop.shop_name) {
uni.showToast({ title: '请输入店铺名称', icon: 'none' })
return
}
<<<<<<< HEAD
uni.showLoading({ title: '保存中...' })
try {
const shopData = {
shop_name: this.shop.shop_name,
shop_logo: this.shop.shop_logo,
shop_banner: this.shop.shop_banner,
=======
uni.showLoading({ title: '正在上传图片...' })
try {
@@ -208,7 +195,6 @@
shop_name: this.shop.shop_name,
shop_logo: finalLogo,
shop_banner: finalBanner,
>>>>>>> local-backup-root-cyj
description: this.shop.description,
contact_name: this.shop.contact_name,
contact_phone: this.shop.contact_phone,

View File

@@ -1,4 +1,4 @@
import { rpcOrValue } from '@/services/analytics/rpc.uts'
import { supabase } from '@/components/supadb/aksupainstance.uts'
import supa from '@/components/supadb/aksupainstance.uts'
export type ProductReviewItem = {
@@ -26,21 +26,106 @@ export type ProductReviewQuery = {
pageSize?: number
}
// 直接查询 ml_product_reviewsJOIN ml_products 和 ak_users 获取商品名/图片/用户名
// 使用 supabase.select + Content-Range 实现服务端分页,无需 RPC
export async function fetchAdminProductReviews(query?: ProductReviewQuery): Promise<{ total: number; items: Array<ProductReviewItem> }> {
const payload = {
p_search_product: query?.searchProduct ?? null,
p_search_user: query?.searchUser ?? null,
p_status: query?.status ?? null,
p_start_time: query?.startTime ?? null,
p_end_time: query?.endTime ?? null,
p_page: query?.page ?? 1,
p_page_size: query?.pageSize ?? 20
} as any
const page = query?.page ?? 1
const pageSize = query?.pageSize ?? 20
const offset = (page - 1) * pageSize
const res = await rpcOrValue('rpc_admin_get_product_reviews', payload as any)
const arr = Array.isArray(res) ? (res as Array<any>) : ([] as Array<any>)
const total = arr.length > 0 ? parseInt(String(arr[0]?.total_count ?? '0')) : 0
return { total, items: arr as Array<ProductReviewItem> }
// 构建 PostgREST filter 字符串
const filters: string[] = []
if (query?.status != null) filters.push(`status=eq.${query.status}`)
if (query?.startTime != null && query.startTime !== '') filters.push(`created_at=gte.${query.startTime}`)
if (query?.endTime != null && query.endTime !== '') filters.push(`created_at=lte.${query.endTime}`)
// offset 注入PostgREST 识别为 SQL OFFSET避免发送 Range 头)
if (offset > 0) filters.push(`offset=${offset}`)
const filterStr = filters.length > 0 ? filters.join('&') : null
// 查询评价表,并内联关联商品和用户信息
const res = await supabase.select(
'ml_product_reviews',
filterStr,
{
columns: 'id, product_id, user_id, rating, content, merchant_reply, status, created_at, product:ml_products!ml_product_reviews_product_id_fkey(name, main_image_url), reviewer:ak_users!ml_product_reviews_user_id_fkey(username)',
limit: pageSize,
order: 'created_at.desc',
count: 'exact'
}
)
if (res.status < 200 || res.status >= 300 || res.data == null) {
console.error('fetchAdminProductReviews 查询失败, status:', res.status)
return { total: 0, items: [] as Array<ProductReviewItem> }
}
// 从 Content-Range 解析总行数
let totalCount = 0
const hdrs = res.headers
if (hdrs != null) {
let cr: string | null = null
if (typeof (hdrs as any).get === 'function') {
cr = (hdrs as any).get('content-range') as string | null
}
if (cr == null) cr = (hdrs as UTSJSONObject)['content-range'] as string | null
if (cr != null) {
const m = /\/(\d+)$/.exec(cr)
if (m != null) totalCount = parseInt(m[1] ?? '0')
}
}
if (totalCount === 0) {
totalCount = offset + (Array.isArray(res.data) ? (res.data as any[]).length : 0)
}
// 字段映射:把 JOIN 结果摊平为 ProductReviewItem
const rawRows = res.data as any[]
const items: Array<ProductReviewItem> = []
// 客户端 searchProduct / searchUser 过滤PostgREST embedded filter 兼容性问题时的兜底)
const spLower = (query?.searchProduct ?? '').toLowerCase()
const suLower = (query?.searchUser ?? '').toLowerCase()
for (let i = 0; i < rawRows.length; i++) {
const row = rawRows[i] as UTSJSONObject
const productRaw = row.get('product')
const reviewerRaw = row.get('reviewer')
let productName = ''
let productImage: string | null = null
if (productRaw != null) {
const p = (productRaw instanceof UTSJSONObject ? productRaw : JSON.parse(JSON.stringify(productRaw))) as UTSJSONObject
productName = p.getString('name') ?? ''
productImage = p.getString('main_image_url') ?? null
}
let username: string | null = null
if (reviewerRaw != null) {
const u = (reviewerRaw instanceof UTSJSONObject ? reviewerRaw : JSON.parse(JSON.stringify(reviewerRaw))) as UTSJSONObject
username = u.getString('username') ?? null
}
// 客户端过滤searchProduct / searchUser
if (spLower !== '' && !productName.toLowerCase().includes(spLower)) continue
if (suLower !== '' && (username ?? '').toLowerCase().includes(suLower) === false) continue
items.push({
id: row.getString('id') ?? '',
product_id: row.getString('product_id') ?? '',
product_name: productName,
product_image: productImage,
user_id: row.getString('user_id') ?? '',
username: username,
rating: row.getNumber('rating') ?? 0,
content: row.getString('content') ?? null,
merchant_reply: row.getString('merchant_reply') ?? null,
status: row.getNumber('status') ?? 1,
created_at: row.getString('created_at') ?? '',
total_count: totalCount
} as ProductReviewItem)
}
return { total: totalCount, items }
}
export async function approveProductReview(id: string): Promise<boolean> {