新增摸大红业务并修复后台链接按钮白字
支持分类筛选、搜索、下单支付建群与固定二维码;合并迁移种子数据;后台表格编辑/详情链接恢复主题色可见。
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import {
|
||||
createAdminMohongCategory,
|
||||
deleteAdminMohongCategory,
|
||||
fetchAdminMohongCategories,
|
||||
updateAdminMohongCategory,
|
||||
type MohongCategory,
|
||||
} from '@/features/mohong/api/mohong'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const loading = ref(false)
|
||||
const items = ref<MohongCategory[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
sort_order: 0,
|
||||
status: 'enabled',
|
||||
})
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
items.value = await fetchAdminMohongCategories()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '加载失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
Object.assign(form, { name: '', code: '', sort_order: 0, status: 'enabled' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(item: MohongCategory) {
|
||||
editingId.value = item.id
|
||||
Object.assign(form, {
|
||||
name: item.name,
|
||||
code: item.code || '',
|
||||
sort_order: item.sort_order,
|
||||
status: item.status,
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.name.trim()) {
|
||||
ElMessage.warning('请填写分类名称')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await updateAdminMohongCategory(editingId.value, { ...form })
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await createAdminMohongCategory({ ...form })
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item: MohongCategory) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除分类「${item.name}」?其下商品将变为未分类。`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await deleteAdminMohongCategory(item.id)
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error(readError(error, '删除失败'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>摸大红分类</h2>
|
||||
<p>大红 / 四格大红 / 炫彩等分类,前台左侧筛选使用</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建分类</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="items" border stripe>
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column prop="code" label="编码" width="140" />
|
||||
<el-table-column prop="sort_order" label="排序" width="90" />
|
||||
<el-table-column label="商品数" width="90">
|
||||
<template #default="{ row }">{{ row.product_count }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.status === 'enabled' ? '启用' : '停用' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingId ? '编辑分类' : '新建分类'"
|
||||
width="480px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="名称" required>
|
||||
<el-input v-model="form.name" maxlength="64" />
|
||||
</el-form-item>
|
||||
<el-form-item label="编码">
|
||||
<el-input v-model="form.code" maxlength="64" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort_order" />
|
||||
<span class="hint">越小越靠前</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width: 160px">
|
||||
<el-option label="启用" value="enabled" />
|
||||
<el-option label="停用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-page {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-head h2 {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: #6b7a90;
|
||||
font-size: 13px;
|
||||
}
|
||||
.hint {
|
||||
margin-left: 8px;
|
||||
color: #8a94a6;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchAdminMohongConfig,
|
||||
updateAdminMohongConfig,
|
||||
type MohongConfig,
|
||||
} from '@/features/mohong/api/mohong'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const form = reactive<MohongConfig>({
|
||||
default_qrcode_url: '',
|
||||
group_welcome_text: '',
|
||||
order_copy_template: '',
|
||||
})
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const config = await fetchAdminMohongConfig()
|
||||
Object.assign(form, config)
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '加载配置失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
const config = await updateAdminMohongConfig({
|
||||
default_qrcode_url: form.default_qrcode_url,
|
||||
group_welcome_text: form.group_welcome_text,
|
||||
order_copy_template: form.order_copy_template,
|
||||
})
|
||||
Object.assign(form, config)
|
||||
ElMessage.success('已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadQrcode(file: File) {
|
||||
try {
|
||||
const uploaded = await uploadAdminFile(file, 'mohong')
|
||||
form.default_qrcode_url = uploaded.url
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '上传失败'))
|
||||
}
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="admin-page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>摸大红配置</h2>
|
||||
<p>全局默认固定二维码、群欢迎语、订单复制模板。商品可覆盖默认二维码。</p>
|
||||
</div>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存配置</el-button>
|
||||
</div>
|
||||
|
||||
<el-form label-width="140px" class="config-form">
|
||||
<el-form-item label="默认固定二维码">
|
||||
<div class="qr-row">
|
||||
<el-image
|
||||
v-if="form.default_qrcode_url"
|
||||
:src="form.default_qrcode_url"
|
||||
fit="contain"
|
||||
style="width: 160px; height: 160px; border-radius: 12px; background: #f7f9fc"
|
||||
/>
|
||||
<div class="qr-actions">
|
||||
<el-upload :show-file-list="false" accept="image/*" :before-upload="uploadQrcode">
|
||||
<el-button :icon="Upload">上传二维码</el-button>
|
||||
</el-upload>
|
||||
<el-input v-model="form.default_qrcode_url" placeholder="或直接填写图片 URL" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="群欢迎语">
|
||||
<el-input v-model="form.group_welcome_text" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="复制文案模板">
|
||||
<el-input v-model="form.order_copy_template" type="textarea" :rows="8" />
|
||||
<div class="hint">
|
||||
可用变量:{{order_no}} {{created_at}}
|
||||
{{product_title}} {{quantity}} {{amount}}
|
||||
{{buyer_name}} {{buyer_phone}} {{unit}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-page {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-head h2 {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: #6b7a90;
|
||||
font-size: 13px;
|
||||
}
|
||||
.config-form {
|
||||
max-width: 820px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #eef1f5;
|
||||
}
|
||||
.qr-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
.qr-actions {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
color: #8a94a6;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,244 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import {
|
||||
cancelAdminMohongOrder,
|
||||
completeAdminMohongOrder,
|
||||
fetchAdminMohongOrders,
|
||||
mohongOrderStatusLabel,
|
||||
type MohongOrder,
|
||||
} from '@/features/mohong/api/mohong'
|
||||
import { formatCent } from '@/shared/utils/money'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const items = ref<MohongOrder[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const keyword = ref('')
|
||||
const status = ref('')
|
||||
const detail = ref<MohongOrder | null>(null)
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await fetchAdminMohongOrders({
|
||||
keyword: keyword.value || undefined,
|
||||
status: status.value || undefined,
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
items.value = result.items || []
|
||||
total.value = result.total || 0
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '加载失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleComplete(row: MohongOrder) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认将订单 ${row.order_no} 标记为已完成?`, '提示', {
|
||||
type: 'warning',
|
||||
})
|
||||
await completeAdminMohongOrder(row.id)
|
||||
ElMessage.success('已完成')
|
||||
await load()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(row: MohongOrder) {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt('请输入取消原因', '取消订单', {
|
||||
inputPlaceholder: '可选',
|
||||
confirmButtonText: '确认取消',
|
||||
cancelButtonText: '返回',
|
||||
})
|
||||
await cancelAdminMohongOrder(row.id, value || '后台取消')
|
||||
ElMessage.success('已取消')
|
||||
await load()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function copyText(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success('已复制')
|
||||
} catch {
|
||||
ElMessage.error('复制失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>摸大红订单</h2>
|
||||
<p>查看订单、完成履约、复制用户订单信息</p>
|
||||
</div>
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<el-input v-model="keyword" clearable placeholder="订单号" style="width: 220px" @keyup.enter="load" />
|
||||
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
|
||||
<el-option label="待支付" value="pending_payment" />
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click=";(page = 1), load()">查询</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="items" border stripe>
|
||||
<el-table-column prop="order_no" label="订单号" min-width="170" />
|
||||
<el-table-column prop="product_title" label="商品" min-width="140" />
|
||||
<el-table-column label="数量" width="70" prop="quantity" />
|
||||
<el-table-column label="金额" width="100">
|
||||
<template #default="{ row }">¥{{ row.amount || formatCent(row.amount_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="买家" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.buyer_nickname || '-' }}
|
||||
<div class="sub">{{ row.buyer_phone || '' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">{{ mohongOrderStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下单时间" min-width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="detail = row">详情</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'paid'"
|
||||
link
|
||||
type="success"
|
||||
@click="handleComplete(row)"
|
||||
>
|
||||
完成
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'pending_payment' || row.status === 'paid'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleCancel(row)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<AdminTablePagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@update:current-page="page = $event"
|
||||
@update:page-size="pageSize = $event"
|
||||
@page-change="load"
|
||||
/>
|
||||
|
||||
<el-drawer
|
||||
:model-value="Boolean(detail)"
|
||||
size="420px"
|
||||
title="订单详情"
|
||||
destroy-on-close
|
||||
@update:model-value="(v: boolean) => !v && (detail = null)"
|
||||
>
|
||||
<template v-if="detail">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="订单号">{{ detail.order_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
{{ mohongOrderStatusLabel(detail.status) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="商品">{{ detail.product_title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">{{ detail.quantity }}</el-descriptions-item>
|
||||
<el-descriptions-item label="金额">
|
||||
¥{{ detail.amount || formatCent(detail.amount_cent) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="买家">
|
||||
{{ detail.buyer_nickname }} {{ detail.buyer_phone }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会话ID">
|
||||
{{ detail.conversation_id || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="detail.copy_text" class="copy-box">
|
||||
<div class="copy-head">
|
||||
<strong>复制文案</strong>
|
||||
<el-button size="small" @click="copyText(detail.copy_text)">复制</el-button>
|
||||
</div>
|
||||
<pre>{{ detail.copy_text }}</pre>
|
||||
</div>
|
||||
<div v-if="detail.qrcode_url_snapshot" class="qr-box">
|
||||
<strong>二维码快照</strong>
|
||||
<el-image :src="detail.qrcode_url_snapshot" fit="contain" style="width: 180px; height: 180px" />
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-page {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.page-head h2 {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.page-head p,
|
||||
.sub {
|
||||
margin: 0;
|
||||
color: #6b7a90;
|
||||
font-size: 12px;
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.copy-box,
|
||||
.qr-box {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.copy-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.copy-box pre {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: #f7f9fc;
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.qr-box {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,418 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Upload } from '@element-plus/icons-vue'
|
||||
import {
|
||||
createAdminMohongProduct,
|
||||
deleteAdminMohongProduct,
|
||||
fetchAdminMohongCategories,
|
||||
fetchAdminMohongProducts,
|
||||
mohongProductStatusLabel,
|
||||
updateAdminMohongProduct,
|
||||
type MohongCategory,
|
||||
type MohongProduct,
|
||||
} from '@/features/mohong/api/mohong'
|
||||
import { uploadAdminFile } from '@/shared/api/files'
|
||||
import { formatCent, yuanToCent } from '@/shared/utils/money'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const items = ref<MohongProduct[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const keyword = ref('')
|
||||
const status = ref('')
|
||||
const categoryId = ref<number | undefined>()
|
||||
const categories = ref<MohongCategory[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const form = reactive({
|
||||
category_id: undefined as number | undefined,
|
||||
title: '',
|
||||
cover_url: '',
|
||||
image_urls: [] as string[],
|
||||
description: '',
|
||||
price_yuan: 0,
|
||||
original_price_yuan: 0,
|
||||
unit: '份',
|
||||
stock: -1,
|
||||
sort_order: 0,
|
||||
status: 'draft',
|
||||
qrcode_image_url: '',
|
||||
use_custom_qrcode: false,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCategories()
|
||||
await load()
|
||||
})
|
||||
|
||||
async function loadCategories() {
|
||||
try {
|
||||
categories.value = await fetchAdminMohongCategories()
|
||||
} catch {
|
||||
categories.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await fetchAdminMohongProducts({
|
||||
keyword: keyword.value || undefined,
|
||||
status: status.value || undefined,
|
||||
category_id: categoryId.value,
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
})
|
||||
items.value = result.items || []
|
||||
total.value = result.total || 0
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '加载失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
Object.assign(form, {
|
||||
category_id: categoryId.value,
|
||||
title: '',
|
||||
cover_url: '',
|
||||
image_urls: [],
|
||||
description: '',
|
||||
price_yuan: 0,
|
||||
original_price_yuan: 0,
|
||||
unit: '份',
|
||||
stock: -1,
|
||||
sort_order: 0,
|
||||
status: 'on_sale',
|
||||
qrcode_image_url: '',
|
||||
use_custom_qrcode: false,
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(item: MohongProduct) {
|
||||
editingId.value = item.id
|
||||
Object.assign(form, {
|
||||
category_id: item.category_id || undefined,
|
||||
title: item.title,
|
||||
cover_url: item.cover_url,
|
||||
image_urls: [...(item.image_urls || [])],
|
||||
description: item.description,
|
||||
price_yuan: item.price_cent / 100,
|
||||
original_price_yuan: (item.original_price_cent || 0) / 100,
|
||||
unit: item.unit || '份',
|
||||
stock: item.stock,
|
||||
sort_order: item.sort_order,
|
||||
status: item.status,
|
||||
qrcode_image_url: item.qrcode_image_url || '',
|
||||
use_custom_qrcode: Boolean(item.qrcode_image_url),
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.title.trim()) {
|
||||
ElMessage.warning('请填写标题')
|
||||
return
|
||||
}
|
||||
if (form.price_yuan <= 0) {
|
||||
ElMessage.warning('请填写有效价格')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
title: form.title.trim(),
|
||||
cover_url: form.cover_url,
|
||||
image_urls: form.image_urls,
|
||||
description: form.description,
|
||||
price_cent: yuanToCent(form.price_yuan),
|
||||
original_price_cent: yuanToCent(form.original_price_yuan),
|
||||
unit: form.unit || '份',
|
||||
stock: form.stock,
|
||||
sort_order: form.sort_order,
|
||||
status: form.status,
|
||||
qrcode_image_url: form.use_custom_qrcode ? form.qrcode_image_url : '',
|
||||
}
|
||||
if (form.category_id) {
|
||||
payload.category_id = form.category_id
|
||||
} else if (editingId.value) {
|
||||
payload.clear_category = true
|
||||
}
|
||||
if (editingId.value) {
|
||||
await updateAdminMohongProduct(editingId.value, payload)
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await createAdminMohongProduct(
|
||||
payload as Partial<MohongProduct> & { title: string; price_cent: number }
|
||||
)
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item: MohongProduct) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除商品「${item.title}」?`, '提示', { type: 'warning' })
|
||||
await deleteAdminMohongProduct(item.id)
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error(readError(error, '删除失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadImage(file: File, target: 'cover' | 'gallery' | 'qrcode') {
|
||||
try {
|
||||
const uploaded = await uploadAdminFile(file, 'mohong')
|
||||
if (target === 'cover') form.cover_url = uploaded.url
|
||||
else if (target === 'qrcode') form.qrcode_image_url = uploaded.url
|
||||
else form.image_urls.push(uploaded.url)
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '上传失败'))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function removeGallery(url: string) {
|
||||
form.image_urls = form.image_urls.filter(item => item !== url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>摸大红商品</h2>
|
||||
<p>管理商品图片、价格、库存与专属二维码</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<el-input v-model="keyword" clearable placeholder="搜索标题" style="width: 220px" @keyup.enter="load" />
|
||||
<el-select v-model="categoryId" clearable placeholder="分类" style="width: 140px">
|
||||
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="上架中" value="on_sale" />
|
||||
<el-option label="已下架" value="off_sale" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click=";(page = 1), load()">查询</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="items" border stripe>
|
||||
<el-table-column label="封面" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.cover_url"
|
||||
:src="row.cover_url"
|
||||
style="width: 48px; height: 48px; border-radius: 8px"
|
||||
fit="cover"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="标题" min-width="160" />
|
||||
<el-table-column label="分类" width="110">
|
||||
<template #default="{ row }">{{ row.category_name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="100">
|
||||
<template #default="{ row }">¥{{ row.price || formatCent(row.price_cent) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">{{ row.stock < 0 ? '不限' : row.stock }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">{{ mohongProductStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序" width="80" />
|
||||
<el-table-column label="专属码" width="90">
|
||||
<template #default="{ row }">{{ row.qrcode_image_url ? '是' : '默认' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<AdminTablePagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@update:current-page="page = $event"
|
||||
@update:page-size="pageSize = $event"
|
||||
@page-change="load"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingId ? '编辑商品' : '新建商品'"
|
||||
width="640px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="分类">
|
||||
<el-select v-model="form.category_id" clearable placeholder="选择分类" style="width: 100%">
|
||||
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" required>
|
||||
<el-input v-model="form.title" maxlength="128" />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面">
|
||||
<div class="upload-row">
|
||||
<el-image
|
||||
v-if="form.cover_url"
|
||||
:src="form.cover_url"
|
||||
style="width: 80px; height: 80px; border-radius: 8px"
|
||||
fit="cover"
|
||||
/>
|
||||
<el-upload :show-file-list="false" accept="image/*" :before-upload="(f: File) => uploadImage(f, 'cover')">
|
||||
<el-button :icon="Upload">上传封面</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="详情图">
|
||||
<div class="gallery">
|
||||
<div v-for="url in form.image_urls" :key="url" class="gallery-item">
|
||||
<el-image :src="url" fit="cover" />
|
||||
<button type="button" @click="removeGallery(url)">×</button>
|
||||
</div>
|
||||
<el-upload :show-file-list="false" accept="image/*" :before-upload="(f: File) => uploadImage(f, 'gallery')">
|
||||
<el-button :icon="Upload">添加图片</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="4" />
|
||||
</el-form-item>
|
||||
<el-form-item label="售价(元)" required>
|
||||
<el-input-number v-model="form.price_yuan" :min="0.1" :step="0.1" :precision="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原价(元)">
|
||||
<el-input-number v-model="form.original_price_yuan" :min="0" :step="0.1" :precision="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位">
|
||||
<el-input v-model="form.unit" style="width: 120px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="库存">
|
||||
<el-input-number v-model="form.stock" :min="-1" />
|
||||
<span class="hint">-1 表示不限库存</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort_order" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width: 160px">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="上架" value="on_sale" />
|
||||
<el-option label="下架" value="off_sale" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="专属二维码">
|
||||
<el-switch v-model="form.use_custom_qrcode" active-text="覆盖默认" inactive-text="用默认" />
|
||||
<div v-if="form.use_custom_qrcode" class="upload-row" style="margin-top: 8px">
|
||||
<el-image
|
||||
v-if="form.qrcode_image_url"
|
||||
:src="form.qrcode_image_url"
|
||||
style="width: 100px; height: 100px; border-radius: 8px"
|
||||
fit="contain"
|
||||
/>
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:before-upload="(f: File) => uploadImage(f, 'qrcode')"
|
||||
>
|
||||
<el-button :icon="Upload">上传二维码</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-page {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.page-head h2 {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: #6b7a90;
|
||||
font-size: 13px;
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.upload-row,
|
||||
.gallery {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.gallery-item {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
.gallery-item :deep(.el-image) {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.gallery-item button {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hint {
|
||||
margin-left: 8px;
|
||||
color: #8a94a6;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user