接入商品评论数据

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', id: 'shop_manage',
title: '店铺管理', title: '我的店铺',
path: '/pages/mall/admin/shop/manage', path: '/pages/mall/admin/shop/manage',
componentKey: 'ShopManage', componentKey: 'ShopManage',
parentId: 'shop', parentId: 'shop',

View File

@@ -5,30 +5,33 @@
<view class="search-row"> <view class="search-row">
<view class="search-item"> <view class="search-item">
<text class="label">评价时间:</text> <text class="label">评价时间:</text>
<view class="mock-date-range"> <view class="date-range-row">
<text class="emoji">📅</text> <input class="mock-input date-input" v-model="startTime" placeholder="开始 2025-01-01" />
<text class="txt">开始日期 - 结束日期</text> <text class="date-sep">~</text>
<input class="mock-input date-input" v-model="endTime" placeholder="结束 2025-12-31" />
</view> </view>
</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"> <view class="search-item">
<text class="label">审核状态:</text> <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> </view>
<view class="search-row mt-16"> <view class="search-row mt-16">
<view class="search-item"> <view class="search-item">
<text class="label">商品信息:</text> <text class="label">商品信息:</text>
<input class="mock-input" placeholder="请输入商品信息" /> <input class="mock-input" v-model="searchProduct" placeholder="请输入商品信息" />
</view> </view>
<view class="search-item"> <view class="search-item">
<text class="label">用户名称:</text> <text class="label">用户名称:</text>
<input class="mock-input" placeholder="请输入" /> <input class="mock-input" v-model="searchUser" placeholder="请输入" />
</view> </view>
<button class="btn-primary">查询</button> <button class="btn-primary" @click="onSearch">查询</button>
<button class="btn-white" @click="onReset">重置</button>
</view> </view>
</view> </view>
@@ -40,53 +43,75 @@
<!-- 3. 数据表格 --> <!-- 3. 数据表格 -->
<view class="list-card"> <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="table-v5">
<view class="th-row"> <view class="th-row">
<view class="th col-check"><text>□</text></view> <view class="th col-check"><text>□</text></view>
<view class="th col-id"><text>评论ID</text></view> <view class="th col-id"><text>评论ID</text></view>
<view class="th col-product"><text>商品信息</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-user"><text>用户名称</text></view>
<view class="th col-score"><text>评分</text></view> <view class="th col-score"><text>评分</text></view>
<view class="th col-content"><text>评价内容</text></view> <view class="th col-content"><text>评价内容</text></view>
<view class="th col-reply"><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-time"><text>评价时间</text></view>
<view class="th col-op"><text>操作</text></view> <view class="th col-op"><text>操作</text></view>
</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-check"><text>□</text></view>
<view class="td col-id"><text>{{ item.id }}</text></view> <view class="td col-id">
<view class="td col-product"> <text class="id-txt">{{ item.id.substring(0, 8) }}...</text>
<image class="p-img" :src="item.image" mode="aspectFill" /> </view>
<text class="p-name-txt">{{ item.productName }}</text> <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>
<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"> <view class="td col-status">
<text class="status-tag" :class="item.status === 1 ? 'pass' : 'wait'"> <text class="status-tag" :class="getStatusClass(item.status)">
{{ item.status === 1 ? '通过' : '待审核' }} {{ getStatusText(item.status) }}
</text> </text>
</view> </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"> <view class="td col-op">
<text class="op-link">通过</text> <text v-if="item.status !== 1" class="op-link" @click="handleApprove(item.id)">通过</text>
<text class="op-link">驳回</text> <text v-if="item.status === 1" class="op-link warn" @click="handleReject(item.id)">驳回</text>
<text class="op-link">回复</text> <text class="op-link" @click="openReplyModal(item.id, item.merchant_reply)">回复</text>
<text class="op-link red">删除</text> <text class="op-link red" @click="handleDelete(item.id)">删除</text>
</view> </view>
</view> </view>
</view> </view>
<!-- 分页 --> <!-- 分页 -->
<CommonPagination <CommonPagination
v-if="true" v-if="total > 0"
:total="total" :total="total"
:loading="false" :loading="loading"
:currentPage="currentPage" :currentPage="currentPage"
:pageSize="pageSize" :pageSize="pageSize"
:pageSizeOptionLabels="pageSizeOptionLabels" :pageSizeOptionLabels="pageSizeOptionLabels"
@@ -100,81 +125,232 @@
@jump-page="handleJumpPage" @jump-page="handleJumpPage"
/> />
</view> </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> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import { ref, computed } from 'vue' import { ref, computed, onMounted } from 'vue'
import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue' import CommonPagination from '@/components/CommonPagination/CommonPagination.uvue'
import {
fetchAdminProductReviews,
approveProductReview,
rejectProductReview,
replyProductReview,
deleteProductReview,
type ProductReviewItem
} from '@/services/admin/productReviewService.uts'
const replyList = ref([ // ========== 搜索筛选状态 ==========
{ const searchProduct = ref('')
id: 1069, const searchUser = ref('')
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', const startTime = ref('')
productName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060', const endTime = ref('')
spec: 'XL,卡其', // 审核状态选项index 0 → 全部(null), 1 → 正常(1), 2 → 已删除(2), 3 → 已隐藏(3)
username: 'demo998', const statusLabels = ['全部', '正常', '已删除', '已隐藏']
score: 3.5, const statusPickerIndex = ref(0)
content: '22',
reply: '', function onStatusChange(e : any) {
status: 0, statusPickerIndex.value = Number(e.detail.value)
time: '2025-02-19 14:56:43' }
},
{ // ========== 数据状态 ==========
id: 1059, const loading = ref(false)
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', const fetchError = ref('')
productName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060', const reviewList = ref<ProductReviewItem[]>([])
spec: 'XL,卡其',
username: '你好呀', // ========== 数据加载(服务端分页,按需请求) ==========
score: 3.5, async function loadReviews(page : number) {
content: '的', if (loading.value) return
reply: '', loading.value = true
status: 0, fetchError.value = ''
time: '2025-01-07 15:35:36' try {
}, // index 0 → null不过滤其余直接映射到真实 status 值 1/2/3
{ const selectedStatus : number | null = statusPickerIndex.value === 0 ? null : statusPickerIndex.value
id: 980, const result = await fetchAdminProductReviews({
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500', searchProduct: searchProduct.value !== '' ? searchProduct.value : null,
productName: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060', searchUser: searchUser.value !== '' ? searchUser.value : null,
spec: 'XL,卡其', status: selectedStatus,
username: 'wx209638', startTime: startTime.value !== '' ? startTime.value : null,
score: 5, endTime: endTime.value !== '' ? endTime.value : null,
content: '好', page: page,
reply: '', pageSize: pageSize.value
status: 1, })
time: '2024-09-12 14:20:12' 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 currentPage = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
const jumpPageInput = ref('') const jumpPageInput = ref('')
const pageSizeOptions = [10, 20, 30, 50] 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 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 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 visiblePages = computed((): number[] => {
const t = totalPage.value; const cur = currentPage.value 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 <= 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] 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] return [1, -1, cur - 1, cur, cur + 1, -1, t]
}) })
const handlePageChange = (p: number) => { currentPage.value = p } const handlePageChange = (p : number) => {
const handlePageSizeChange = (e: any) => { if (p < 1 || p > totalPage.value) return
loadReviews(p)
}
const handlePageSizeChange = (e : any) => {
const idx = Number(e.detail.value) const idx = Number(e.detail.value)
pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0] pageSize.value = pageSizeOptions[idx] ?? pageSizeOptions[0]
currentPage.value = 1 loadReviews(1)
} }
const handleJumpPage = () => { const handleJumpPage = () => {
const p = parseInt(jumpPageInput.value) 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 ========== // ========== END PAGINATION STATE ==========
</script> </script>
@@ -208,11 +384,9 @@ const handleJumpPage = () => {
.label { font-size: 14px; color: #606266; width: 80px; text-align: right; } .label { font-size: 14px; color: #606266; width: 80px; text-align: right; }
} }
.mock-date-range { .date-range-row {
width: 280px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; display: flex; flex-direction: row; align-items: center; gap: 8px;
display: flex; flex-direction: row; align-items: center; padding: 0 12px; gap: 8px; .date-sep { font-size: 13px; color: #606266; }
.emoji { font-size: 14px; }
.txt { font-size: 13px; color: #c0c4cc; }
} }
.mock-select { .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; 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-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-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 { .action-bar {
margin-bottom: 20px; margin-bottom: 20px;
@@ -242,6 +419,16 @@ const handleJumpPage = () => {
padding: var(--admin-card-padding); 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%; } .table-v5 { width: 100%; }
.th-row { .th-row {
@@ -266,10 +453,16 @@ const handleJumpPage = () => {
display: flex; align-items: center; justify-content: center; 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-check { width: 50px; }
.col-id { width: 80px; } .col-id { width: 100px; }
.col-product { flex: 1.5; justify-content: flex-start; gap: 12px; } .col-product { flex: 1.5; justify-content: flex-start; gap: 12px; }
.col-spec { width: 100px; }
.col-user { width: 120px; } .col-user { width: 120px; }
.col-score { width: 80px; } .col-score { width: 80px; }
.col-content { flex: 1; } .col-content { flex: 1; }
@@ -279,16 +472,51 @@ const handleJumpPage = () => {
.col-op { width: 180px; display: flex; flex-direction: row; } .col-op { width: 180px; display: flex; flex-direction: row; }
.p-img { width: 40px; height: 40px; border-radius: 4px; } .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; } .p-name-txt { font-size: 13px; line-height: 1.4; color: #1890ff; }
.id-txt { font-size: 12px; color: #909399; }
.blue-link { color: #1890ff; } .blue-link { color: #1890ff; }
.status-tag { .status-tag {
padding: 2px 8px; border-radius: 2px; font-size: 12px; padding: 2px 8px; border-radius: 2px; font-size: 12px;
&.pass { background: rgba(82, 196, 26, 0.1); color: #52c41a; } &.pass { background: rgba(82, 196, 26, 0.1); color: #52c41a; }
&.wait { background: rgba(250, 173, 20, 0.1); color: #faad14; } &.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; } } .op-link {
/* 分页区域已迁至 CommonPagination 组件 */ 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> </style>

View File

@@ -1,44 +1,12 @@
<!-- 商家端 - 聊天页面 --> <!-- 商家端 - 聊天页面 -->
<template> <template>
<view class="chat-page"> <view class="chat-page">
<<<<<<< HEAD
<view class="chat-header">
=======
<!-- 聊天头部 --> <!-- 聊天头部 -->
<view class="chat-header" :style="{ paddingTop: navPaddingTop }"> <view class="chat-header" :style="{ paddingTop: navPaddingTop }">
>>>>>>> local-backup-root-cyj
<view class="header-back" @click="goBack"> <view class="header-back" @click="goBack">
<text class="back-icon"></text> <text class="back-icon"></text>
</view> </view>
<view class="header-info"> <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"> <view class="header-info-text-wrapper">
<text class="chat-title">{{ chatTitle }}</text> <text class="chat-title">{{ chatTitle }}</text>
<text class="chat-status">在线</text> <text class="chat-status">在线</text>
@@ -103,20 +71,11 @@
:src="shopAvatar" :src="shopAvatar"
mode="aspectFill" mode="aspectFill"
/> />
>>>>>>> local-backup-root-cyj
</view> </view>
</view> </view>
</view> </view>
</scroll-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="chat-input">
<view class="input-tools"> <view class="input-tools">
@@ -156,23 +115,16 @@
</text> </text>
</view> </view>
</scroll-view> </scroll-view>
>>>>>>> local-backup-root-cyj
</view> </view>
</template> </template>
<script lang="uts"> <script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts' import supa from '@/components/supadb/aksupainstance.uts'
<<<<<<< HEAD
type ChatMessageType = {
id: string
=======
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts' import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
type ChatMessageType = { type ChatMessageType = {
id: string id: string
viewId: string viewId: string
>>>>>>> local-backup-root-cyj
session_id: string session_id: string
sender_id: string sender_id: string
receiver_id: string receiver_id: string
@@ -181,10 +133,7 @@
is_read: boolean is_read: boolean
is_from_user: boolean is_from_user: boolean
created_at: string created_at: string
<<<<<<< HEAD
=======
time: string time: string
>>>>>>> local-backup-root-cyj
} }
export default { export default {
@@ -196,9 +145,6 @@
inputText: '', inputText: '',
chatMessages: [] as ChatMessageType[], chatMessages: [] as ChatMessageType[],
scrollToView: '', scrollToView: '',
<<<<<<< HEAD
merchantId: ''
=======
merchantId: '', merchantId: '',
userAvatar: '/static/images/default-avatar.png', userAvatar: '/static/images/default-avatar.png',
shopAvatar: '/static/images/default-shop.png', shopAvatar: '/static/images/default-shop.png',
@@ -212,19 +158,15 @@
computed: { computed: {
emojiList(): string[] { emojiList(): string[] {
return ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐'] return ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
>>>>>>> local-backup-root-cyj
} }
}, },
onLoad(options: any) { onLoad(options: any) {
<<<<<<< HEAD
=======
const sysInfo = uni.getSystemInfoSync() const sysInfo = uni.getSystemInfoSync()
const statusBarH = sysInfo.statusBarHeight const statusBarH = sysInfo.statusBarHeight
this.navPaddingTop = (statusBarH + 10) + 'px' this.navPaddingTop = (statusBarH + 10) + 'px'
console.log('chat page onLoad options:', options) console.log('chat page onLoad options:', options)
>>>>>>> local-backup-root-cyj
if (options.session_id) { if (options.session_id) {
this.sessionId = options.session_id this.sessionId = options.session_id
} }
@@ -238,9 +180,6 @@
}, },
onShow() { onShow() {
<<<<<<< HEAD
this.loadChatMessages()
=======
console.log('chat page onShow, chatUserId:', this.chatUserId, 'merchantId:', this.merchantId) console.log('chat page onShow, chatUserId:', this.chatUserId, 'merchantId:', this.merchantId)
if (this.merchantId) { if (this.merchantId) {
this.loadChatMessages() this.loadChatMessages()
@@ -257,61 +196,12 @@
if (this.realtimeChannel != null) { if (this.realtimeChannel != null) {
supa.removeChannel(this.realtimeChannel!) supa.removeChannel(this.realtimeChannel!)
} }
>>>>>>> local-backup-root-cyj
}, },
methods: { methods: {
async initMerchantId() { async initMerchantId() {
try { try {
const session = supa.getSession() 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) { if (session != null && session.user != null) {
this.merchantId = session.user.getString('id') || '' this.merchantId = session.user.getString('id') || ''
} }
@@ -405,15 +295,12 @@
} else { } else {
console.log('没有找到聊天记录') console.log('没有找到聊天记录')
this.chatMessages = [] this.chatMessages = []
>>>>>>> local-backup-root-cyj
} }
} catch (e) { } catch (e) {
console.error('加载聊天记录失败:', e) console.error('加载聊天记录失败:', e)
} }
}, },
<<<<<<< HEAD
=======
setupRealtimeSubscription(): void { setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...') console.log('开始建立聊天实时订阅...')
@@ -492,25 +379,12 @@
}, 100) }, 100)
}, },
>>>>>>> local-backup-root-cyj
async markAsRead() { async markAsRead() {
try { try {
await supa await supa
.from('ml_chat_messages') .from('ml_chat_messages')
.update({ is_read: true }) .update({ is_read: true })
.eq('receiver_id', this.merchantId) .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('sender_id', this.chatUserId)
.eq('is_read', false) .eq('is_read', false)
.execute() .execute()
@@ -525,7 +399,6 @@
this.inputText = '' this.inputText = ''
this.showEmoji = false this.showEmoji = false
>>>>>>> local-backup-root-cyj
try { try {
const newMessage = { const newMessage = {
@@ -543,26 +416,6 @@
.insert([newMessage]) .insert([newMessage])
.execute() .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) { if (response.error != null) {
console.error('发送消息失败:', response.error) console.error('发送消息失败:', response.error)
uni.showToast({ title: '发送失败', icon: 'none' }) uni.showToast({ title: '发送失败', icon: 'none' })
@@ -621,40 +474,12 @@
goBack() { goBack() {
uni.navigateBack() uni.navigateBack()
>>>>>>> local-backup-root-cyj
} }
} }
} }
</script> </script>
<style> <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-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; } .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 { padding: 0 !important; background-color: transparent !important; }
.image-bubble .message-time { margin-top: 5px; text-align: right; } .image-bubble .message-time { margin-top: 5px; text-align: right; }
.message-image { width: 150px; border-radius: 8px; } .message-image { width: 150px; border-radius: 8px; }
>>>>>>> local-backup-root-cyj
</style> </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> <template>
<view class="merchant-container"> <view class="merchant-container">
<scroll-view scroll-y class="main-scroll" :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh"> <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 class="safe-bottom"></view>
</view> </view>
</scroll-view> </scroll-view>
>>>>>>> local-backup-root-cyj
</view> </view>
</template> </template>
@@ -459,9 +320,6 @@
low_stock: 0, low_stock: 0,
pending_reviews: 0 pending_reviews: 0
} as PendingCountsType, } as PendingCountsType,
<<<<<<< HEAD
recentOrders: [] as OrderType[]
=======
recentOrders: [] as OrderType[], recentOrders: [] as OrderType[],
unreadCount: 0, unreadCount: 0,
refreshing: false refreshing: false
@@ -472,7 +330,6 @@
currentDate(): string { currentDate(): string {
const now = new Date() const now = new Date()
return `${now.getMonth() + 1}月${now.getDate()}日` return `${now.getMonth() + 1}月${now.getDate()}日`
>>>>>>> local-backup-root-cyj
} }
}, },
@@ -482,37 +339,16 @@
onShow() { onShow() {
if (this.merchantId) { 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.loadAllData()
this.startRealtimeSubscription() this.startRealtimeSubscription()
} else { } else {
setTimeout(() => { setTimeout(() => {
this.loadAllData() this.loadAllData()
this.startRealtimeSubscription() this.startRealtimeSubscription()
>>>>>>> local-backup-root-cyj
}, 500) }, 500)
} }
}, },
<<<<<<< HEAD
methods: {
formatNumber(value: number | null): string {
if (value == null) return '0.00'
return value.toFixed(2)
},
=======
onHide() { onHide() {
this.stopRealtimeSubscription() this.stopRealtimeSubscription()
}, },
@@ -522,7 +358,6 @@
}, },
methods: { methods: {
>>>>>>> local-backup-root-cyj
async initMerchantId() { async initMerchantId() {
try { try {
const session = supa.getSession() const session = supa.getSession()
@@ -537,8 +372,6 @@
} }
}, },
<<<<<<< HEAD
=======
startRealtimeSubscription() { startRealtimeSubscription() {
if (!this.merchantId) return if (!this.merchantId) return
@@ -586,7 +419,6 @@
return value.toFixed(2) return value.toFixed(2)
}, },
>>>>>>> local-backup-root-cyj
async loadMerchantData() { async loadMerchantData() {
try { try {
const response = await supa const response = await supa
@@ -596,12 +428,8 @@
.limit(1) .limit(1)
.execute() .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) { console.error('ml_shops请求500报错', response.error) }
if (response.error != null || !response.data || (response.data as any[]).length === 0) { if (response.error != null || !response.data || (response.data as any[]).length === 0) {
>>>>>>> local-backup-root-cyj
this.shopInfo = { this.shopInfo = {
id: null, id: null,
merchant_id: this.merchantId, merchant_id: this.merchantId,
@@ -620,11 +448,7 @@
const rawData = (response.data as any[])[0] as UTSJSONObject const rawData = (response.data as any[])[0] as UTSJSONObject
this.shopInfo = { this.shopInfo = {
<<<<<<< HEAD
id: rawData.getString('id') || null,
=======
id: rawData.getString('id') || null, id: rawData.getString('id') || null,
>>>>>>> local-backup-root-cyj
merchant_id: rawData.getString('merchant_id') || null, merchant_id: rawData.getString('merchant_id') || null,
shop_name: rawData.getString('shop_name') || '我的店铺', shop_name: rawData.getString('shop_name') || '我的店铺',
shop_logo: rawData.getString('shop_logo') || null, shop_logo: rawData.getString('shop_logo') || null,
@@ -636,8 +460,6 @@
total_sales: rawData.getNumber('total_sales') || 0, total_sales: rawData.getNumber('total_sales') || 0,
status: rawData.getNumber('status') || 1 status: rawData.getNumber('status') || 1
} }
<<<<<<< HEAD
=======
// 重新动态查询并计算该店铺下所有商品的真实销量总和 // 重新动态查询并计算该店铺下所有商品的真实销量总和
try { try {
@@ -672,7 +494,6 @@
console.error('获取店铺真实销量失败:', e) console.error('获取店铺真实销量失败:', e)
} }
>>>>>>> local-backup-root-cyj
} catch (e) { } catch (e) {
console.error('加载店铺信息失败:', e) console.error('加载店铺信息失败:', e)
} }
@@ -680,21 +501,6 @@
async loadTodayStats() { async loadTodayStats() {
try { 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. 获取所有订单 // 1. 获取所有订单
const response = await supa const response = await supa
.from('ml_orders') .from('ml_orders')
@@ -716,18 +522,12 @@
const now = new Date() const now = new Date()
// 获取今日0点的毫秒数 (本地时间) // 获取今日0点的毫秒数 (本地时间)
const todayStartMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() const todayStartMs = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
>>>>>>> local-backup-root-cyj
const rawData = response.data as any[] const rawData = response.data as any[]
if (rawData != null) { if (rawData != null) {
for (let i = 0; i < rawData.length; i++) { for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject const item = rawData[i] as UTSJSONObject
const status = item.getNumber('order_status') const status = item.getNumber('order_status')
<<<<<<< HEAD
if (status >= 2) {
totalOrders++
totalSales += item.getNumber('total_amount') || 0
=======
// 有效订单(已支付、已发货、已完成) >= 2 // 有效订单(已支付、已发货、已完成) >= 2
// 如果是退款(0)或取消(5),可能不计入今日销售额,这里按需调整 // 如果是退款(0)或取消(5),可能不计入今日销售额,这里按需调整
@@ -753,18 +553,10 @@
todaySales += item.getNumber('total_amount') || 0 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) let currentShopSales = Number(this.shopInfo.total_sales || 0)
if (allTimeSalesVolume > currentShopSales) { if (allTimeSalesVolume > currentShopSales) {
@@ -776,7 +568,6 @@
sales: todaySales, sales: todaySales,
visitors: Math.floor(todayOrders * (2.5 + Math.random())) + 5, // 模拟访客数 visitors: Math.floor(todayOrders * (2.5 + Math.random())) + 5, // 模拟访客数
conversion: todayOrders > 0 ? (12 + Math.floor(Math.random() * 8)) : 0 // 模拟转化率 conversion: todayOrders > 0 ? (12 + Math.floor(Math.random() * 8)) : 0 // 模拟转化率
>>>>>>> local-backup-root-cyj
} }
} catch (e) { } catch (e) {
console.error('获取今日统计异常:', e) console.error('获取今日统计异常:', e)
@@ -792,39 +583,24 @@
.eq('order_status', 2) .eq('order_status', 2)
.execute() .execute()
<<<<<<< HEAD
const refundRes = await supa
=======
if (pendingShipmentRes.error != null) { console.error('pendingShipment报错', pendingShipmentRes.error) } if (pendingShipmentRes.error != null) { console.error('pendingShipment报错', pendingShipmentRes.error) }
const refundRes = await supa const refundRes = await supa
>>>>>>> local-backup-root-cyj
.from('ml_orders') .from('ml_orders')
.select('id', { count: 'exact' }) .select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId) .eq('merchant_id', this.merchantId)
.eq('order_status', 0) .eq('order_status', 0)
.execute() .execute()
<<<<<<< HEAD
const lowStockRes = await supa
=======
if (refundRes.error != null) { console.error('refundRes报错', refundRes.error) } if (refundRes.error != null) { console.error('refundRes报错', refundRes.error) }
const lowStockRes = await supa const lowStockRes = await supa
>>>>>>> local-backup-root-cyj
.from('ml_products') .from('ml_products')
.select('id', { count: 'exact' }) .select('id', { count: 'exact' })
.eq('merchant_id', this.merchantId) .eq('merchant_id', this.merchantId)
.lte('total_stock', 10) .lte('total_stock', 10)
<<<<<<< HEAD
.gte('total_stock', 0)
.execute()
this.pendingCounts = {
=======
.execute() .execute()
if (lowStockRes.error != null) { console.error('lowStockRes报错', lowStockRes.error) } if (lowStockRes.error != null) { console.error('lowStockRes报错', lowStockRes.error) }
this.pendingCounts = { this.pendingCounts = {
>>>>>>> local-backup-root-cyj
pending_shipment: pendingShipmentRes.total || 0, pending_shipment: pendingShipmentRes.total || 0,
refund_requests: refundRes.total || 0, refund_requests: refundRes.total || 0,
low_stock: lowStockRes.total || 0, low_stock: lowStockRes.total || 0,
@@ -841,11 +617,7 @@
.from('ml_orders') .from('ml_orders')
.select(` .select(`
*, *,
<<<<<<< HEAD
order_items!inner (
=======
order_items ( order_items (
>>>>>>> local-backup-root-cyj
id, id,
product_id, product_id,
product_name, product_name,
@@ -860,15 +632,8 @@
.limit(5) .limit(5)
.execute() .execute()
<<<<<<< HEAD
if (response.error != null || !response.data) {
this.recentOrders = []
return
}
=======
if (response.error != null) { console.error('recentOrders报错', response.error) } if (response.error != null) { console.error('recentOrders报错', response.error) }
if (response.error != null || !response.data) { this.recentOrders = []; return; } if (response.error != null || !response.data) { this.recentOrders = []; return; }
>>>>>>> local-backup-root-cyj
const rawData = response.data as any[] const rawData = response.data as any[]
const ordersData: OrderType[] = [] const ordersData: OrderType[] = []
@@ -909,33 +674,6 @@
this.recentOrders = ordersData this.recentOrders = ordersData
} catch (e) { } 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()}) console.error('加载最新订单异常:', e); uni.showModal({title: '最新订单报错', content: e.toString()})
} }
}, },
@@ -979,7 +717,6 @@
if (status === 4) return '已完成' if (status === 4) return '已完成'
if (status === 0) return '退款中' if (status === 0) return '退款中'
return '未知' return '未知'
>>>>>>> local-backup-root-cyj
}, },
formatTime(timeStr: string): string { formatTime(timeStr: string): string {
@@ -988,70 +725,6 @@
const now = new Date() const now = new Date()
const diff = now.getTime() - date.getTime() const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / (1000 * 60)) 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 < 60) return `${minutes}分钟前`
if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前` if (minutes < 1440) return `${Math.floor(minutes / 60)}小时前`
@@ -1104,339 +777,12 @@
goToOrderDetail(orderId: string) { goToOrderDetail(orderId: string) {
uni.navigateTo({ url: `/pages/mall/merchant/order-detail?id=${orderId}` }) uni.navigateTo({ url: `/pages/mall/merchant/order-detail?id=${orderId}` })
>>>>>>> local-backup-root-cyj
} }
} }
} }
</script> </script>
<style> <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; } .merchant-container { background-color: #f5f7fa; min-height: 100vh; }
.main-scroll { 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="label">当前库存</text>
<text class="value">{{ currentProduct?.total_stock }}</text> <text class="value">{{ currentProduct?.total_stock }}</text>
</view> </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="adjust-type">
<view class="type-btn" :class="{ active: adjustType === 'set' }" @click="adjustType = 'set'">直接设为</view> <view class="type-btn" :class="{ active: adjustType === 'set' }" @click="adjustType = 'set'">直接设为</view>
@@ -77,16 +72,11 @@
<view class="form-item"> <view class="form-item">
<text class="label">备注 (可选)</text> <text class="label">备注 (可选)</text>
<input class="input" v-model="stockRemark" placeholder="如:入库、损耗等"/> <input class="input" v-model="stockRemark" placeholder="如:入库、损耗等"/>
>>>>>>> local-backup-root-cyj
</view> </view>
</view> </view>
<view class="modal-footer"> <view class="modal-footer">
<view class="modal-btn cancel" @click="closeStockModal">取消</view> <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> <view class="modal-btn confirm" @click="saveStock">确认提交</view>
>>>>>>> local-backup-root-cyj
</view> </view>
</view> </view>
</view> </view>
@@ -118,13 +108,9 @@
stats: { totalProducts: 0, lowStock: 0, outOfStock: 0 }, stats: { totalProducts: 0, lowStock: 0, outOfStock: 0 },
showStockModal: false, showStockModal: false,
currentProduct: null as ProductType | null, currentProduct: null as ProductType | null,
<<<<<<< HEAD
newStock: ''
=======
newStock: '', newStock: '',
adjustType: 'set', // 'set', 'add', 'sub' adjustType: 'set', // 'set', 'add', 'sub'
stockRemark: '' stockRemark: ''
>>>>>>> local-backup-root-cyj
} }
}, },
@@ -133,10 +119,7 @@
}, },
onShow() { onShow() {
<<<<<<< HEAD
=======
this.page = 1 this.page = 1
>>>>>>> local-backup-root-cyj
this.loadProducts() this.loadProducts()
this.loadStats() this.loadStats()
}, },
@@ -145,31 +128,16 @@
async initMerchantId() { async initMerchantId() {
try { try {
const session = supa.getSession() const session = supa.getSession()
<<<<<<< HEAD
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
=======
if (session != null && session.user != null) { if (session != null && session.user != null) {
this.merchantId = session.user.getString('id') || '' this.merchantId = session.user.getString('id') || ''
} }
if (!this.merchantId) { if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || '' this.merchantId = uni.getStorageSync('user_id') || ''
} }
>>>>>>> local-backup-root-cyj
} catch (e) {} } catch (e) {}
}, },
async loadProducts() { 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 if (this.loading && this.page === 1) return
this.loading = true this.loading = true
@@ -191,41 +159,19 @@
if (response.error != null) { if (response.error != null) {
console.error('加载商品失败:', response.error) console.error('加载商品失败:', response.error)
>>>>>>> local-backup-root-cyj
return return
} }
const rawData = response.data as any[] 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 if (!rawData) return
const productsData: ProductType[] = [] const productsData: ProductType[] = []
for (let i = 0; i < rawData.length; i++) { for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject const item = rawData[i] as UTSJSONObject
>>>>>>> local-backup-root-cyj
productsData.push({ productsData.push({
id: item.getString('id') || '', id: item.getString('id') || '',
name: item.getString('name') || '', name: item.getString('name') || '',
main_image_url: item.getString('main_image_url') || '', 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, total_stock: item.getNumber('total_stock') || 0,
warning_stock: item.getNumber('warning_stock') || 10 warning_stock: item.getNumber('warning_stock') || 10
} as ProductType) } as ProductType)
@@ -238,7 +184,6 @@
} }
this.hasMore = rawData.length === this.limit this.hasMore = rawData.length === this.limit
>>>>>>> local-backup-root-cyj
} catch (e) { } catch (e) {
console.error('加载失败:', e) console.error('加载失败:', e)
} finally { } finally {
@@ -288,13 +233,9 @@
editStock(product: ProductType) { editStock(product: ProductType) {
this.currentProduct = product this.currentProduct = product
<<<<<<< HEAD
this.newStock = String(product.total_stock)
=======
this.newStock = '' this.newStock = ''
this.adjustType = 'set' this.adjustType = 'set'
this.stockRemark = '' this.stockRemark = ''
>>>>>>> local-backup-root-cyj
this.showStockModal = true this.showStockModal = true
}, },
@@ -305,15 +246,6 @@
}, },
async saveStock() { 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) const val = parseInt(this.newStock)
if (isNaN(val)) { if (isNaN(val)) {
uni.showToast({ title: '请输入有效数值', icon: 'none' }) uni.showToast({ title: '请输入有效数值', icon: 'none' })
@@ -345,21 +277,12 @@
}) })
.eq('id', this.currentProduct!.id) .eq('id', this.currentProduct!.id)
.execute() .execute()
>>>>>>> local-backup-root-cyj
if (response.error != null) { if (response.error != null) {
uni.showToast({ title: '保存失败', icon: 'none' }) uni.showToast({ title: '保存失败', icon: 'none' })
return 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' }) uni.showToast({ title: '更新成功', icon: 'success' })
this.closeStockModal() this.closeStockModal()
this.page = 1 this.page = 1
@@ -369,7 +292,6 @@
uni.showToast({ title: '操作异常', icon: 'none' }) uni.showToast({ title: '操作异常', icon: 'none' })
} finally { } finally {
uni.hideLoading() 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; } .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-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; } .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; } .modal-body { padding: 30rpx; }
.adjust-type { display: flex; justify-content: space-between; margin-bottom: 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 { 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; } .type-btn.active { background-color: #E3F2FD; color: #007AFF; border-color: #007AFF; }
>>>>>>> local-backup-root-cyj
.form-item { margin-bottom: 20rpx; } .form-item { margin-bottom: 20rpx; }
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; } .form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
.form-item .value { font-size: 28rpx; color: #333; } .form-item .value { font-size: 28rpx; color: #333; }

View File

@@ -1,47 +1,6 @@
<!-- 商家端 - 消息中心页面 --> <!-- 商家端 - 消息中心页面 -->
<template> <template>
<view class="messages-page"> <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"> <view class="header">
<text class="header-title">消息</text> <text class="header-title">消息</text>
<text class="header-subtitle">与客户的聊天记录</text> <text class="header-subtitle">与客户的聊天记录</text>
@@ -82,7 +41,6 @@
</view> </view>
<view class="safe-bottom"></view> <view class="safe-bottom"></view>
>>>>>>> local-backup-root-cyj
</scroll-view> </scroll-view>
</view> </view>
</template> </template>
@@ -108,10 +66,7 @@
avatar: string avatar: string
lastMessage: string lastMessage: string
lastTime: string lastTime: string
<<<<<<< HEAD
=======
lastTimeRaw: string lastTimeRaw: string
>>>>>>> local-backup-root-cyj
unread: number unread: number
userId: string userId: string
} }
@@ -119,11 +74,6 @@
export default { export default {
data() { data() {
return { return {
<<<<<<< HEAD
currentTab: 'chat',
messages: [] as MessageType[],
=======
>>>>>>> local-backup-root-cyj
conversations: [] as ConversationType[], conversations: [] as ConversationType[],
loading: false, loading: false,
refreshing: false, refreshing: false,
@@ -161,48 +111,15 @@
const response = await query.execute() const response = await query.execute()
if (response.error != null || !response.data) { if (response.error != null || !response.data) {
<<<<<<< HEAD
this.messages = []
=======
>>>>>>> local-backup-root-cyj
this.conversations = [] this.conversations = []
return return
} }
const rawData = response.data as any[] const rawData = response.data as any[]
<<<<<<< HEAD
const messagesData: MessageType[] = []
=======
>>>>>>> local-backup-root-cyj
const sessionMap = new Map<string, ConversationType>() const sessionMap = new Map<string, ConversationType>()
for (let i = 0; i < rawData.length; i++) { for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject 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 isFromUser = item.getBoolean('is_from_user') || false
const senderId = item.getString('sender_id') || '' const senderId = item.getString('sender_id') || ''
const receiverId = item.getString('receiver_id') || '' const receiverId = item.getString('receiver_id') || ''
@@ -227,18 +144,11 @@
lastMessage: content, lastMessage: content,
lastTime: this.formatTime(createdAt), lastTime: this.formatTime(createdAt),
lastTimeRaw: createdAt, lastTimeRaw: createdAt,
>>>>>>> local-backup-root-cyj
unread: 0, unread: 0,
userId: otherUserId 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)! const conv = sessionMap.get(otherUserId)!
// 更新最后一条消息(按时间最新的) // 更新最后一条消息(按时间最新的)
if (createdAt > conv.lastTimeRaw) { if (createdAt > conv.lastTimeRaw) {
@@ -248,15 +158,10 @@
} }
// 未读消息:消息来自用户且未读 // 未读消息:消息来自用户且未读
if (!isRead && isFromUser) { if (!isRead && isFromUser) {
>>>>>>> local-backup-root-cyj
conv.unread++ conv.unread++
} }
} }
<<<<<<< HEAD
this.messages = messagesData
=======
>>>>>>> local-backup-root-cyj
this.conversations = Array.from(sessionMap.values()).sort((a, b) => b.unread - a.unread) this.conversations = Array.from(sessionMap.values()).sort((a, b) => b.unread - a.unread)
} catch (e) { } catch (e) {
@@ -273,33 +178,11 @@
}) })
}, },
<<<<<<< HEAD
switchTab(tab: string) {
this.currentTab = tab
},
=======
>>>>>>> local-backup-root-cyj
onRefresh() { onRefresh() {
this.refreshing = true this.refreshing = true
this.loadMessages() 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 { formatTime(timeStr: string): string {
if (!timeStr) return '' if (!timeStr) return ''
const date = new Date(timeStr) const date = new Date(timeStr)
@@ -319,37 +202,6 @@
</script> </script>
<style> <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; } .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; } .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; } .conv-arrow { font-size: 40rpx; color: #ccc; margin-left: 10rpx; }
.safe-bottom { height: 30rpx; } .safe-bottom { height: 30rpx; }
>>>>>>> local-backup-root-cyj
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -135,9 +135,6 @@
} }
}) })
}, },
<<<<<<< HEAD
=======
async uploadImageToSupa(localPath: string): Promise<string> { async uploadImageToSupa(localPath: string): Promise<string> {
if (localPath.startsWith('http://') || localPath.startsWith('https://')) { if (localPath.startsWith('http://') || localPath.startsWith('https://')) {
@@ -173,22 +170,12 @@
} }
}, },
>>>>>>> local-backup-root-cyj
async saveShop() { async saveShop() {
if (!this.shop.shop_name) { if (!this.shop.shop_name) {
uni.showToast({ title: '请输入店铺名称', icon: 'none' }) uni.showToast({ title: '请输入店铺名称', icon: 'none' })
return 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: '正在上传图片...' }) uni.showLoading({ title: '正在上传图片...' })
try { try {
@@ -208,7 +195,6 @@
shop_name: this.shop.shop_name, shop_name: this.shop.shop_name,
shop_logo: finalLogo, shop_logo: finalLogo,
shop_banner: finalBanner, shop_banner: finalBanner,
>>>>>>> local-backup-root-cyj
description: this.shop.description, description: this.shop.description,
contact_name: this.shop.contact_name, contact_name: this.shop.contact_name,
contact_phone: this.shop.contact_phone, 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' import supa from '@/components/supadb/aksupainstance.uts'
export type ProductReviewItem = { export type ProductReviewItem = {
@@ -26,21 +26,106 @@ export type ProductReviewQuery = {
pageSize?: number 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> }> { export async function fetchAdminProductReviews(query?: ProductReviewQuery): Promise<{ total: number; items: Array<ProductReviewItem> }> {
const payload = { const page = query?.page ?? 1
p_search_product: query?.searchProduct ?? null, const pageSize = query?.pageSize ?? 20
p_search_user: query?.searchUser ?? null, const offset = (page - 1) * pageSize
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 res = await rpcOrValue('rpc_admin_get_product_reviews', payload as any) // 构建 PostgREST filter 字符串
const arr = Array.isArray(res) ? (res as Array<any>) : ([] as Array<any>) const filters: string[] = []
const total = arr.length > 0 ? parseInt(String(arr[0]?.total_count ?? '0')) : 0 if (query?.status != null) filters.push(`status=eq.${query.status}`)
return { total, items: arr as Array<ProductReviewItem> } 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> { export async function approveProductReview(id: string): Promise<boolean> {