feat: remove shop feature (Phase 2)

Delete all shop domain classes, controllers, schema builders, templates,
JS/CSS, admin SPA pages, and shop-only payment classes (Shipping,
AfterSale, Discount). Remove shop-only DB table definitions (product_variant,
after_sale, order_shipping, card_key) from migrator and add their DROP
to uninstall.php. Clean up shared files:

- Remove aksrt_product CPT + taxonomies from Setup.php
- Remove shop rewrite rules/query vars/template redirect from SetupPart2.php
- Remove shop widget areas from SetupPart1.php
- Remove shop enqueues from ScriptsPart1b.php
- Remove sepakerator-aksrt-shop from SetupPart2.php
- Remove get_shop_settings facade (delete ForumShop.php, clean DomainGroups.php)
- Remove ORDER_PRODUCT_SHOP constant from DomainModel.php
- Remove Shop::class fallback branches from UserOrders.php, OrderCenterRest.php
- Remove shipping/after-sale branches from OrderRepositoryAdmin.php, PayMenus.php, PayOrders.php
- Remove shop product widgets from widgets-register.php
- Remove cart page from template-registry.php, routing.php, ScriptsPart2.php
- Remove cart UI from sidebar-nav.php, profile-orders.php, sidebar-orders.php
- Clean up order-center.js (remove rebuy/cartUrl/receipt/after-sale handlers)
- Remove aksrt_get_cart_page_url/product_page/product_edit from transactions.php
- Clean up aksrt_product_cat references in Coupon.php, PayCoupons.php

Shared payment/order/credits/content-pay stack fully retained.
This commit is contained in:
2026-08-02 21:25:52 +08:00
parent 2bbd7c01a3
commit 1d2505356e
103 changed files with 46 additions and 21267 deletions
@@ -1,81 +0,0 @@
import { renderFaIcon, schemaPage } from './helpers'
// Shop SPA = config only. Ops moved to native WP menus (Phase 4).
export const shopSection = {
key: 'shop',
label: '商城&商品',
icon: renderFaIcon('fas fa-shopping-cart'),
route: '/shop',
children: [
schemaPage({
key: 'shop-basic',
name: 'ShopBasic',
label: '商城基础',
route: '/shop/basic',
title: '商城基础',
desc: '商城总开关、首页链接、面包屑和 URL 别名设置。',
canonicalDomain: 'shop',
section: 'basic',
}),
schemaPage({
key: 'shop-list',
name: 'ShopList',
label: '商品列表',
route: '/shop/list',
title: '商品列表',
desc: '商品列表排序、分页、卡片样式和销量显示配置。',
canonicalDomain: 'shop',
section: 'list',
}),
schemaPage({
key: 'shop-single',
name: 'ShopSingle',
label: '商品详情',
route: '/shop/single',
title: '商品详情',
desc: '商品详情页布局、相关推荐和底部内容设置。',
canonicalDomain: 'shop',
section: 'single',
}),
schemaPage({
key: 'shop-product-defaults',
name: 'ShopProductDefaults',
label: '商品默认参数',
route: '/shop/product-defaults',
title: '商品默认参数',
desc: '主题级商品参数默认值,作为参数继承链的根。',
canonicalDomain: 'shop',
section: 'product-defaults',
}),
schemaPage({
key: 'shop-contact',
name: 'ShopContact',
label: '客服配置',
route: '/shop/contact',
title: '客服配置',
desc: '商品详情页客服按钮和联系方式配置。',
canonicalDomain: 'shop',
section: 'contact',
}),
schemaPage({
key: 'shop-shipping-config',
name: 'ShopShippingConfig',
label: '物流配置',
route: '/shop/shipping-config',
title: '物流配置',
desc: '快递查询接口、物流公司列表和收货地址设置。',
canonicalDomain: 'shop',
section: 'shipping-config',
}),
schemaPage({
key: 'shop-timing',
name: 'ShopTiming',
label: '时效与售后原因',
route: '/shop/timing',
title: '时效与售后原因',
desc: '确认收货、评价、退货时效和售后原因配置。',
canonicalDomain: 'shop',
section: 'timing',
}),
],
}
@@ -1,304 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="售后工作台" desc="把退款、换货、维修放在订单语境里处理,减少运营跳转。" />
<div class="aksrt-after-sale__stats">
<div v-for="item in statCards" :key="item.key" class="aksrt-after-sale__stat-card">
<div class="aksrt-after-sale__stat-icon" :style="{ background: item.color }">
<i :class="item.icon"></i>
</div>
<div class="aksrt-after-sale__stat-info">
<div class="aksrt-after-sale__stat-value">{{ stats[item.key] || 0 }}</div>
<div class="aksrt-after-sale__stat-label">{{ item.label }}</div>
</div>
</div>
</div>
<n-card size="small" class="aksrt-after-sale__section">
<n-space wrap>
<n-select v-model:value="filters.status" :options="statusOptions" clearable placeholder="售后状态" style="width: 160px" />
<n-select v-model:value="filters.stage" :options="stageOptions" clearable placeholder="处理阶段" style="width: 160px" />
<n-select v-model:value="filters.type" :options="typeOptions" clearable placeholder="售后类型" style="width: 160px" />
<n-input v-model:value="filters.order_number" clearable placeholder="订单号" style="width: 180px" @keyup.enter="loadData(1)" />
<n-input v-model:value="filters.date_from" type="date" style="width: 160px" />
<n-input v-model:value="filters.date_to" type="date" style="width: 160px" />
<n-button type="primary" @click="loadData(1)">查询</n-button>
<n-button @click="resetFilters">重置</n-button>
</n-space>
<div class="aksrt-after-sale__toolbar">
<n-space>
<n-button type="primary" @click="showCreateModal = true">新建售后</n-button>
</n-space>
</div>
</n-card>
<n-card size="small" class="aksrt-after-sale__section">
<n-data-table
:columns="columns"
:data="items"
:loading="loading"
:pagination="pagination"
@update:page="handlePageChange"
/>
</n-card>
<AfterSaleDetailDrawer
v-model:show="detailVisible"
:after-sale-id="detailId"
:status-options="detailStatusOptions"
@updated="loadData(page)"
/>
<div v-if="showCreateModal" class="aksrt-modal-overlay" @click.self="showCreateModal = false">
<div class="aksrt-modal aksrt-after-sale__modal">
<h3>新建售后工单</h3>
<div class="aksrt-form-group">
<label>订单号</label>
<input v-model="createForm.order_number" type="text" class="aksrt-input" placeholder="输入订单号">
</div>
<div class="aksrt-form-group">
<label>售后类型</label>
<select v-model="createForm.type" class="aksrt-select">
<option v-for="item in typeOptions.filter((item) => item.value)" :key="item.value" :value="item.value">{{ item.label }}</option>
</select>
</div>
<div class="aksrt-form-group">
<label>金额</label>
<input v-model.number="createForm.amount" type="number" step="0.01" class="aksrt-input" placeholder="0.00">
</div>
<div class="aksrt-form-group">
<label>原因</label>
<textarea v-model="createForm.reason" class="aksrt-textarea" rows="3" placeholder="售后原因"></textarea>
</div>
<div class="aksrt-form-group">
<label>补充备注</label>
<textarea v-model="createForm.note" class="aksrt-textarea" rows="3" placeholder="补充说明"></textarea>
</div>
<div class="aksrt-modal__actions">
<button class="aksrt-button" @click="showCreateModal = false">取消</button>
<button class="aksrt-button aksrt-button--primary" :disabled="creating" @click="submitCreate">
{{ creating ? '提交中...' : '提交' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, h, onMounted, reactive, ref } from 'vue'
import { NButton, NTag, useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
import AfterSaleDetailDrawer from './components/AfterSaleDetailDrawer.vue'
const message = useMessage()
const loading = ref(false)
const page = ref(1)
const items = ref([])
const stats = ref({})
const detailVisible = ref(false)
const detailId = ref(null)
const statusOptions = ref([])
const stageOptions = ref([])
const typeOptions = ref([])
const showCreateModal = ref(false)
const creating = ref(false)
const filters = reactive({
status: null,
stage: null,
type: null,
order_number: '',
date_from: '',
date_to: '',
})
const createForm = reactive({
order_number: '',
type: 'refund',
amount: 0,
reason: '',
note: '',
})
const pagination = reactive({
page: 1,
pageSize: 20,
itemCount: 0,
})
const statCards = [
{ key: 'pending', label: '待处理', icon: 'fas fa-clock', color: '#f59e0b' },
{ key: 'processing', label: '处理中', icon: 'fas fa-spinner', color: '#2563eb' },
{ key: 'approved', label: '已通过', icon: 'fas fa-check', color: '#16a34a' },
{ key: 'rejected', label: '已拒绝', icon: 'fas fa-times', color: '#dc2626' },
{ key: 'total', label: '总数', icon: 'fas fa-layer-group', color: '#7c3aed' },
]
const detailStatusOptions = computed(() => statusOptions.value.filter((item) => item.value))
const columns = [
{ title: '订单号', key: 'order_number', width: 170, render: (row) => h('code', row.order_number) },
{ title: '售后类型', key: 'type_label', width: 110 },
{ title: '阶段', key: 'stage', width: 120, render: (row) => row.stage?.label || '-' },
{ title: '物流联动', key: 'shipping_link', width: 170, render: (row) => row.order_context?.shipping?.latest?.status_label || row.order_context?.shipping?.latest?.status || '-' },
{ title: '申请人', key: 'applicant', width: 120, render: (row) => row.applicant?.display_name || '-' },
{ title: '金额', key: 'amount', width: 110, render: (row) => `¥${Number(row.amount || 0).toFixed(2)}` },
{ title: '原因', key: 'reason', ellipsis: { tooltip: true } },
{
title: '状态',
key: 'status',
width: 100,
render: (row) => h(NTag, { size: 'small', type: resolveTagType(row.status) }, () => row.status_label || row.status),
},
{ title: '创建时间', key: 'created_at', width: 160 },
{
title: '操作',
key: 'actions',
width: 120,
render: (row) => h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => openDetail(row) }, () => '详情'),
},
]
function resolveTagType(status) {
if (status === 'approved' || status === 'completed') return 'success'
if (status === 'processing') return 'info'
if (status === 'rejected' || status === 'cancelled') return 'error'
return 'warning'
}
async function loadData(nextPage = 1) {
loading.value = true
page.value = nextPage
pagination.page = nextPage
try {
const res = await api.request('/shop/after-sale', {
params: {
page: nextPage,
per_page: pagination.pageSize,
status: filters.status || '',
stage: filters.stage || '',
type: filters.type || '',
order_number: filters.order_number || '',
date_from: filters.date_from || '',
date_to: filters.date_to || '',
},
})
const data = res?.data || {}
items.value = data.items || []
stats.value = data.stats || {}
pagination.itemCount = data.total || 0
statusOptions.value = [{ label: '全部状态', value: null }, ...(data.options?.status || [])]
stageOptions.value = [{ label: '全部阶段', value: null }, ...(data.options?.stage || [])]
typeOptions.value = [{ label: '全部类型', value: null }, ...(data.options?.type || [])]
} catch (error) {
message.error(error?.message || '加载售后数据失败')
} finally {
loading.value = false
}
}
function handlePageChange(nextPage) {
loadData(nextPage)
}
function openDetail(row) {
detailId.value = row.id
detailVisible.value = true
}
function resetFilters() {
filters.status = null
filters.stage = null
filters.type = null
filters.order_number = ''
filters.date_from = ''
filters.date_to = ''
loadData(1)
}
async function submitCreate() {
if (!createForm.order_number || !createForm.reason) {
message.warning('请填写订单号和售后原因')
return
}
creating.value = true
try {
await api.request('/shop/after-sale', {
method: 'POST',
data: { ...createForm },
})
message.success('售后工单已创建')
showCreateModal.value = false
createForm.order_number = ''
createForm.type = 'refund'
createForm.amount = 0
createForm.reason = ''
createForm.note = ''
loadData(1)
} catch (error) {
message.error(error?.message || '创建售后工单失败')
} finally {
creating.value = false
}
}
onMounted(() => loadData(1))
</script>
<style scoped>
.aksrt-after-sale__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.aksrt-after-sale__stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
}
.aksrt-after-sale__stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.aksrt-after-sale__stat-value {
font-size: 22px;
font-weight: 700;
color: #111827;
}
.aksrt-after-sale__stat-label {
font-size: 12px;
color: #6b7280;
}
.aksrt-after-sale__section {
margin-bottom: 16px;
}
.aksrt-after-sale__toolbar {
margin-top: 14px;
}
.aksrt-after-sale__modal {
width: min(520px, calc(100vw - 32px));
}
</style>
-215
View File
@@ -1,215 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="优惠券管理" desc="创建和管理优惠券,支持百分比和固定金额两种类型。" />
<!-- 操作栏 -->
<n-card size="small" class="aksrt-shop-coupons__section">
<n-space justify="space-between">
<n-space>
<n-input v-model:value="search" placeholder="搜索优惠码" clearable style="width:200px;" @clear="loadCoupons" @keyup.enter="loadCoupons">
<template #prefix><FaIcon icon="fas fa-search" :size="12" /></template>
</n-input>
<n-select v-model:value="statusFilter" :options="statusOptions" placeholder="状态" clearable style="width:120px;" @update:value="loadCoupons" />
</n-space>
<n-button type="primary" @click="showCreate = true">
<template #icon><FaIcon icon="fas fa-plus" :size="12" /></template>
新增优惠券
</n-button>
</n-space>
</n-card>
<!-- 优惠券列表 -->
<n-card size="small" class="aksrt-shop-coupons__section">
<n-data-table
:columns="columns"
:data="coupons"
:loading="loading"
:bordered="false"
size="small"
:pagination="pagination"
@update:page="handlePageChange"
/>
</n-card>
<!-- 创建弹窗 -->
<n-modal :show="showCreate" @update:show="showCreate = $event" preset="card" title="新增优惠券" style="width:480px;" closable>
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
<n-form-item label="优惠码" path="code">
<n-input v-model:value="form.code" placeholder="如 SAVE20" />
</n-form-item>
<n-form-item label="类型" path="type">
<n-select v-model:value="form.type" :options="typeOptions" />
</n-form-item>
<n-form-item :label="form.type === 'percent' ? '折扣比例 (%)' : '减免金额 (元)'" path="value">
<n-input-number v-model:value="form.value" :min="0" :max="form.type === 'percent' ? 100 : 99999" style="width:100%;" />
</n-form-item>
<n-form-item label="最低消费 (元)">
<n-input-number v-model:value="form.min_amount" :min="0" style="width:100%;" />
</n-form-item>
<n-form-item label="使用次数上限">
<n-input-number v-model:value="form.usage_limit" :min="0" style="width:100%;" />
<template #feedback>0 表示不限制</template>
</n-form-item>
<n-form-item label="过期时间">
<n-input v-model:value="form.expires_at" type="datetime-local" />
<template #feedback>留空表示永不过期</template>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="showCreate = false">取消</n-button>
<n-button type="primary" :loading="saving" @click="handleCreate">创建</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, h } from 'vue'
import { NTag, NButton, useMessage, useDialog } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import FaIcon from '@/components/form/FaIcon.vue'
import api from '@/api/index.js'
const message = useMessage()
const dialog = useDialog()
const loading = ref(false)
const saving = ref(false)
const coupons = ref([])
const search = ref('')
const statusFilter = ref(null)
const showCreate = ref(false)
const formRef = ref(null)
const form = reactive({
code: '',
type: 'fixed',
value: 10,
min_amount: 0,
usage_limit: 0,
expires_at: '',
})
const rules = {
code: { required: true, message: '请输入优惠码', trigger: 'blur' },
value: { type: 'number', required: true, message: '请输入数值', trigger: 'blur' },
}
const statusOptions = [
{ label: '有效', value: 'active' },
{ label: '已禁用', value: 'disabled' },
]
const typeOptions = [
{ label: '固定金额', value: 'fixed' },
{ label: '百分比折扣', value: 'percent' },
]
const pagination = reactive({ page: 1, pageSize: 20, itemCount: 0, showSizePicker: true, pageSizes: [10, 20, 50] })
const columns = [
{ title: '优惠码', key: 'code', width: 140 },
{
title: '类型',
key: 'type',
width: 100,
render: (row) => h(NTag, { size: 'small', type: row.type === 'percent' ? 'info' : 'success' }, () => row.type === 'percent' ? `${row.value}%` : `¥${row.value}`),
},
{ title: '最低消费', key: 'min_amount', width: 100, render: (row) => row.min_amount > 0 ? `¥${row.min_amount}` : '—' },
{ title: '已用/上限', key: 'usage', width: 100, render: (row) => `${row.used_count}/${row.usage_limit || '∞'}` },
{
title: '状态',
key: 'status',
width: 80,
render: (row) => h(NTag, { size: 'small', type: row.status === 'active' ? 'success' : 'default' }, () => row.status === 'active' ? '有效' : '已禁用'),
},
{ title: '过期时间', key: 'expires_at', width: 160, render: (row) => row.expires_at || '永不过期' },
{
title: '操作',
key: 'actions',
width: 100,
render: (row) => {
if (row.status !== 'active') return h('span', { style: 'color:#aaa;font-size:12px;' }, '—')
return h(NButton, { size: 'tiny', quaternary: true, type: 'error', onClick: () => handleDisable(row) }, () => '禁用')
},
},
]
async function loadCoupons() {
loading.value = true
try {
const params = new URLSearchParams({
page: String(pagination.page),
per_page: String(pagination.pageSize),
})
if (search.value) params.set('search', search.value)
if (statusFilter.value) params.set('status', statusFilter.value)
const res = await api.request('/shop/coupons?' + params.toString())
if (res?.data) {
coupons.value = res.data.items || []
pagination.itemCount = res.data.total || 0
}
} catch {
// endpoint may not exist yet
} finally {
loading.value = false
}
}
function handlePageChange(page) {
pagination.page = page
loadCoupons()
}
async function handleCreate() {
try {
await formRef.value?.validate()
} catch {
return
}
saving.value = true
try {
await api.request('/shop/coupons', {
method: 'POST',
body: JSON.stringify({ ...form }),
})
message.success('优惠券已创建')
showCreate.value = false
loadCoupons()
} catch {
message.error('创建失败')
} finally {
saving.value = false
}
}
function handleDisable(row) {
dialog.warning({
title: '确认禁用',
content: `确定要禁用优惠码「${row.code}」吗?`,
positiveText: '禁用',
negativeText: '取消',
onPositiveClick: async () => {
try {
await api.request(`/shop/coupons/${row.id}/disable`, { method: 'POST' })
message.success('优惠券已禁用')
loadCoupons()
} catch {
message.error('操作失败')
}
},
})
}
onMounted(loadCoupons)
</script>
<style scoped>
.aksrt-shop-coupons__section {
margin-bottom: 16px;
}
</style>
@@ -1,180 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="商城&商品" desc="商品管理、订单系统、库存管理等电商功能。" />
<div class="aksrt-shop-dashboard">
<!-- 统计卡片 -->
<div class="aksrt-shop-dashboard__stats">
<div class="aksrt-shop-stat-card">
<div class="aksrt-shop-stat-card__icon"><i class="fas fa-box"></i></div>
<div class="aksrt-shop-stat-card__info">
<span class="aksrt-shop-stat-card__value">{{ overview.product_count }}</span>
<span class="aksrt-shop-stat-card__label">商品数量</span>
</div>
</div>
<div class="aksrt-shop-stat-card">
<div class="aksrt-shop-stat-card__icon"><i class="fas fa-file-invoice"></i></div>
<div class="aksrt-shop-stat-card__info">
<span class="aksrt-shop-stat-card__value">{{ overview.order_count }}</span>
<span class="aksrt-shop-stat-card__label">订单数量</span>
</div>
</div>
<div class="aksrt-shop-stat-card">
<div class="aksrt-shop-stat-card__icon"><i class="fas fa-coins"></i></div>
<div class="aksrt-shop-stat-card__info">
<span class="aksrt-shop-stat-card__value">{{ revenueDisplay }}</span>
<span class="aksrt-shop-stat-card__label">累计收入 (元)</span>
</div>
</div>
<div class="aksrt-shop-stat-card">
<div class="aksrt-shop-stat-card__icon"><i class="fas fa-clock"></i></div>
<div class="aksrt-shop-stat-card__info">
<span class="aksrt-shop-stat-card__value">{{ overview.order_pending }}</span>
<span class="aksrt-shop-stat-card__label">待处理订单</span>
</div>
</div>
</div>
<!-- 最近商品 -->
<n-card class="aksrt-section-card" title="最近商品">
<n-data-table
v-if="products.length"
:columns="productColumns"
:data="products"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无商品" />
</n-card>
<!-- 最近订单 -->
<n-card class="aksrt-section-card" title="最近订单" style="margin-top:16px;">
<n-data-table
v-if="orders.length"
:columns="orderColumns"
:data="orders"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无订单" />
</n-card>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, h } from 'vue'
import { NTag } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
const overview = ref({ product_count: 0, order_count: 0, order_pending: 0, revenue_total: 0 })
const products = ref([])
const orders = ref([])
const revenueDisplay = computed(() => (overview.value.revenue_total || 0).toFixed(2))
const statusMap = {
created: { label: '已创建', type: 'default' },
pending: { label: '待支付', type: 'warning' },
paid: { label: '已支付', type: 'success' },
delivered: { label: '已发货', type: 'info' },
completed: { label: '已完成', type: 'success' },
cancelled: { label: '已取消', type: 'default' },
expired: { label: '已过期', type: 'default' },
refunding: { label: '退款中', type: 'warning' },
refunded: { label: '已退款', type: 'error' },
}
const productColumns = [
{ title: '商品名称', key: 'title', ellipsis: { tooltip: true } },
{ title: '价格', key: 'price', width: 100, render: (row) => '¥' + (row.price || 0).toFixed(2) },
{ title: '库存', key: 'stock', width: 80 },
{ title: '销量', key: 'sales', width: 80 },
{ title: '创建时间', key: 'created', width: 160 },
]
const orderColumns = [
{ title: '订单号', key: 'order_number', width: 180, ellipsis: { tooltip: true } },
{ title: '产品类型', key: 'product_type', width: 100 },
{ title: '金额', key: 'amount', width: 100, render: (row) => '¥' + (row.amount || 0).toFixed(2) },
{
title: '状态',
key: 'status',
width: 100,
render: (row) => {
const s = statusMap[row.status] || { label: row.status, type: 'default' }
return h(NTag, { size: 'small', type: s.type }, () => s.label)
},
},
{ title: '创建时间', key: 'created_at', width: 160 },
]
onMounted(async () => {
try {
const [overviewRes, productsRes, ordersRes] = await Promise.all([
api.request('/shop/overview'),
api.request('/shop/products?per_page=5'),
api.request('/shop/orders?per_page=5'),
])
if (overviewRes?.success && overviewRes.data) {
overview.value = { ...overview.value, ...overviewRes.data }
}
if (productsRes?.success && productsRes.data) {
products.value = productsRes.data.items || []
}
if (ordersRes?.success && ordersRes.data) {
orders.value = ordersRes.data.items || []
}
} catch {
// endpoints may not be available yet
}
})
</script>
<style scoped>
.aksrt-shop-dashboard__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.aksrt-shop-stat-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #fff;
border-radius: 8px;
border: 1px solid #e8e8e8;
}
.aksrt-shop-stat-card__icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: #f0f5ff;
color: #1890ff;
font-size: 20px;
}
.aksrt-shop-stat-card__info {
display: flex;
flex-direction: column;
}
.aksrt-shop-stat-card__value {
font-size: 24px;
font-weight: 600;
color: #1a1a2e;
}
.aksrt-shop-stat-card__label {
font-size: 13px;
color: #7a7f8a;
}
</style>
-173
View File
@@ -1,173 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="订单管理" desc="查看和管理所有交易订单。" />
<!-- 筛选栏 -->
<n-card size="small" class="aksrt-shop__section">
<n-space>
<n-input v-model:value="search" placeholder="搜索订单号" clearable style="width: 200px;" @clear="loadOrders" @keyup.enter="loadOrders">
<template #prefix><FaIcon icon="fas fa-search" :size="12" /></template>
</n-input>
<n-select v-model:value="statusFilter" :options="statusOptions" placeholder="订单状态" clearable style="width: 140px;" @update:value="loadOrders" />
</n-space>
</n-card>
<!-- 订单列表 -->
<n-card size="small" class="aksrt-shop__section">
<n-data-table
:columns="columns"
:data="orders"
:loading="loading"
:bordered="false"
size="small"
:pagination="pagination"
@update:page="handlePageChange"
/>
</n-card>
<!-- 订单详情抽屉 -->
<OrderDetailDrawer
v-model:show="detailVisible"
:order-number="detailOrderNumber"
@updated="loadOrders"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, h } from 'vue'
import { NTag, NButton } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import FaIcon from '@/components/form/FaIcon.vue'
import OrderDetailDrawer from './components/OrderDetailDrawer.vue'
import api from '@/api/index.js'
const loading = ref(false)
const orders = ref([])
const search = ref('')
const statusFilter = ref(null)
const detailVisible = ref(false)
const detailOrderNumber = ref('')
const statusOptions = [
{ label: '待支付', value: 'pending' },
{ label: '已支付', value: 'paid' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' },
{ label: '已退款', value: 'refunded' },
]
const statusMap = {
created: { label: '已创建', type: 'default' },
pending: { label: '待支付', type: 'warning' },
paid: { label: '已支付', type: 'success' },
delivered: { label: '已发货', type: 'info' },
completed: { label: '已完成', type: 'success' },
cancelled: { label: '已取消', type: 'default' },
expired: { label: '已过期', type: 'default' },
refunding: { label: '退款中', type: 'warning' },
refunded: { label: '已退款', type: 'error' },
}
const pagination = reactive({ page: 1, pageSize: 20, itemCount: 0, showSizePicker: true, pageSizes: [10, 20, 50] })
const columns = [
{
title: '订单号',
key: 'order_number',
width: 180,
ellipsis: { tooltip: true },
render: (row) => h('a', {
href: 'javascript:;',
style: 'color:#1890ff;cursor:pointer;',
onClick: () => openDetail(row),
}, row.order_number),
},
{ title: '商品', key: 'product_name', ellipsis: { tooltip: true } },
{ title: '金额', key: 'amount', width: 100, render: (row) => '¥' + (row.amount || 0).toFixed(2) },
{
title: '状态',
key: 'status',
width: 100,
render: (row) => {
const s = statusMap[row.status] || { label: row.status, type: 'default' }
return h(NTag, { size: 'small', type: s.type }, () => s.label)
},
},
{ title: '买家', key: 'buyer_name', width: 120 },
{ title: '创建时间', key: 'created_at', width: 160 },
{
title: '操作',
key: 'actions',
width: 160,
render: (row) => {
const btns = []
btns.push(h(NButton, { size: 'tiny', quaternary: true, type: 'info', onClick: () => openDetail(row) }, () => '详情'))
if (row.status === 'pending') {
btns.push(h(NButton, { size: 'tiny', quaternary: true, type: 'error', onClick: () => handleCancel(row) }, () => '取消'))
}
if (row.status === 'paid') {
btns.push(h(NButton, { size: 'tiny', quaternary: true, type: 'info', onClick: () => handleDeliver(row) }, () => '发货'))
}
return h('div', { style: 'display:flex;gap:4px;' }, btns)
},
},
]
async function loadOrders() {
loading.value = true
try {
const params = new URLSearchParams({
per_page: String(pagination.pageSize),
page: String(pagination.page),
})
if (search.value) params.set('search', search.value)
if (statusFilter.value) params.set('status', statusFilter.value)
const res = await api.request('/shop/orders?' + params.toString())
if (res?.data) {
orders.value = res.data.items || []
pagination.itemCount = res.data.total || 0
}
} catch {
// Endpoint may not exist yet
} finally {
loading.value = false
}
}
function handlePageChange(page) {
pagination.page = page
loadOrders()
}
function openDetail(row) {
detailOrderNumber.value = row.order_number || ''
detailVisible.value = true
}
// Canonical: use order_number for status updates
async function updateOrderStatus(row, newStatus) {
try {
await api.request('/shop/orders/update-status', {
method: 'POST',
body: JSON.stringify({ order_number: row.order_number, status: newStatus }),
})
loadOrders()
} catch {
// silent
}
}
function handleCancel(row) { updateOrderStatus(row, 'cancelled') }
function handleDeliver(row) { updateOrderStatus(row, 'delivered') }
onMounted(loadOrders)
</script>
<style scoped>
.aksrt-shop__section {
margin-bottom: 16px;
}
</style>
@@ -1,147 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="商品管理" desc="管理所有商品的上架、编辑和库存。" />
<!-- 操作栏 -->
<n-card size="small" class="aksrt-shop__section">
<n-space justify="space-between">
<n-space>
<n-input v-model:value="search" placeholder="搜索商品名称" clearable style="width: 240px;" @clear="loadProducts" @keyup.enter="loadProducts">
<template #prefix><FaIcon icon="fas fa-search" :size="12" /></template>
</n-input>
<n-select v-model:value="statusFilter" :options="statusOptions" placeholder="状态筛选" clearable style="width: 140px;" @update:value="loadProducts" />
</n-space>
<n-button type="primary" @click="openCreate">
<template #icon><FaIcon icon="fas fa-plus" :size="12" /></template>
新增商品
</n-button>
</n-space>
</n-card>
<!-- 商品列表 -->
<n-card size="small" class="aksrt-shop__section">
<n-data-table
:columns="columns"
:data="products"
:loading="loading"
:bordered="false"
size="small"
:pagination="pagination"
@update:page="handlePageChange"
/>
</n-card>
<!-- 商品编辑抽屉 -->
<ProductEditorDrawer
v-model:show="drawerVisible"
:product-id="editingProductId"
@saved="loadProducts"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, h } from 'vue'
import { NTag, NSpace, NButton } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import FaIcon from '@/components/form/FaIcon.vue'
import ProductEditorDrawer from './components/ProductEditorDrawer.vue'
import api from '@/api/index.js'
const loading = ref(false)
const products = ref([])
const search = ref('')
const statusFilter = ref(null)
const drawerVisible = ref(false)
const editingProductId = ref(null)
const statusOptions = [
{ label: '已上架', value: 'publish' },
{ label: '已下架', value: 'draft' },
{ label: '全部', value: '' },
]
const pagination = reactive({ page: 1, pageSize: 20, itemCount: 0, showSizePicker: true, pageSizes: [10, 20, 50] })
const columns = [
{ title: '商品名称', key: 'title', ellipsis: { tooltip: true } },
{ title: '价格', key: 'price', width: 100, render: (row) => '¥' + (row.price || 0).toFixed(2) },
{ title: '库存', key: 'stock', width: 80 },
{ title: '销量', key: 'sales', width: 80 },
{
title: '状态',
key: 'status',
width: 80,
render: (row) => h(NTag, { size: 'small', type: row.status === 'publish' ? 'success' : 'default' }, () => row.status === 'publish' ? '已上架' : '已下架'),
},
{ title: '创建时间', key: 'created', width: 160 },
{
title: '操作',
key: 'actions',
width: 140,
render: (row) => h(NSpace, { size: 'small' }, () => [
h(NButton, { size: 'tiny', quaternary: true, type: 'info', onClick: () => openEdit(row) }, () => '编辑'),
h(NButton, { size: 'tiny', quaternary: true, type: row.status === 'publish' ? 'warning' : 'success', onClick: () => handleToggle(row) }, () => row.status === 'publish' ? '下架' : '上架'),
]),
},
]
async function loadProducts() {
loading.value = true
try {
const params = new URLSearchParams({
per_page: String(pagination.pageSize),
page: String(pagination.page),
})
if (search.value) params.set('search', search.value)
if (statusFilter.value) params.set('status', statusFilter.value)
const res = await api.request('/shop/products?' + params.toString())
if (res?.data) {
products.value = res.data.items || []
pagination.itemCount = res.data.total || 0
}
} catch {
// Endpoint may not exist yet
} finally {
loading.value = false
}
}
function handlePageChange(page) {
pagination.page = page
loadProducts()
}
function openCreate() {
editingProductId.value = null
drawerVisible.value = true
}
function openEdit(row) {
editingProductId.value = row.id
drawerVisible.value = true
}
async function handleToggle(row) {
const newStatus = row.status === 'publish' ? 'draft' : 'publish'
try {
await api.request('/shop/products/' + row.id, {
method: 'POST',
body: JSON.stringify({ status: newStatus }),
})
loadProducts()
} catch {
// silent
}
}
onMounted(loadProducts)
</script>
<style scoped>
.aksrt-shop__section {
margin-bottom: 16px;
}
</style>
@@ -1,364 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="物流工作台" desc="围绕订单上下文处理发货、跟踪、签收和异常退回。" />
<div class="aksrt-shipping__stats">
<div v-for="item in statCards" :key="item.key" class="aksrt-shipping__stat-card">
<div class="aksrt-shipping__stat-icon" :style="{ background: item.color }">
<i :class="item.icon"></i>
</div>
<div class="aksrt-shipping__stat-info">
<div class="aksrt-shipping__stat-value">{{ stats[item.key] || 0 }}</div>
<div class="aksrt-shipping__stat-label">{{ item.label }}</div>
</div>
</div>
</div>
<n-card size="small" class="aksrt-shipping__section">
<n-space wrap>
<n-select v-model:value="filters.status" :options="statusOptions" clearable placeholder="物流状态" style="width: 150px" />
<n-input v-model:value="filters.order_number" clearable placeholder="订单号" style="width: 180px" @keyup.enter="loadData(1)" />
<n-input v-model:value="filters.tracking_no" clearable placeholder="物流单号" style="width: 180px" @keyup.enter="loadData(1)" />
<n-input v-model:value="filters.date_from" type="date" style="width: 160px" />
<n-input v-model:value="filters.date_to" type="date" style="width: 160px" />
<n-button type="primary" @click="loadData(1)">查询</n-button>
<n-button @click="resetFilters">重置</n-button>
</n-space>
<div class="aksrt-shipping__toolbar">
<n-space>
<n-button type="primary" @click="showCreateModal = true">新建发货</n-button>
<n-button :disabled="selectedRowKeys.length === 0" @click="showBatchDialog = true">批量更新状态</n-button>
<n-popover trigger="click" placement="bottom-start">
<template #trigger>
<n-button>列控制</n-button>
</template>
<n-checkbox-group v-model:value="visibleColumnKeys">
<n-space vertical size="small">
<n-checkbox v-for="item in columnOptions" :key="item.key" :value="item.key">
{{ item.label }}
</n-checkbox>
</n-space>
</n-checkbox-group>
</n-popover>
</n-space>
</div>
</n-card>
<n-card size="small" class="aksrt-shipping__section">
<n-data-table
:columns="columns"
:data="items"
:loading="loading"
:row-key="(row) => row.id"
:pagination="pagination"
@update:page="handlePageChange"
@update:checked-row-keys="selectedRowKeys = $event"
/>
</n-card>
<ShippingDetailDrawer
v-model:show="detailVisible"
:shipping-id="detailId"
@updated="loadData(page)"
/>
<ShippingBatchActionDialog
v-model:show="showBatchDialog"
:ids="selectedRowKeys"
:status-options="batchStatusOptions"
@updated="handleBatchUpdated"
/>
<div v-if="showCreateModal" class="aksrt-modal-overlay" @click.self="showCreateModal = false">
<div class="aksrt-modal aksrt-shipping__modal">
<h3>新建发货记录</h3>
<div class="aksrt-form-group">
<label>订单号</label>
<input v-model="createForm.order_number" type="text" class="aksrt-input" placeholder="输入订单号">
</div>
<div class="aksrt-form-group">
<label>物流公司</label>
<select v-model="createForm.carrier" class="aksrt-select">
<option value="">请选择</option>
<option v-for="item in carrierOptions" :key="item.value" :value="item.value">{{ item.label }}</option>
</select>
</div>
<div class="aksrt-form-group">
<label>物流单号</label>
<input v-model="createForm.tracking_no" type="text" class="aksrt-input" placeholder="输入物流单号">
</div>
<div class="aksrt-form-group">
<label>备注</label>
<textarea v-model="createForm.note" class="aksrt-textarea" rows="3" placeholder="发货备注"></textarea>
</div>
<div class="aksrt-modal__actions">
<button class="aksrt-button" @click="showCreateModal = false">取消</button>
<button class="aksrt-button aksrt-button--primary" :disabled="creating" @click="submitCreate">
{{ creating ? '提交中...' : '提交' }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, h, onMounted, reactive, ref } from 'vue'
import { NButton, NTag, useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
import ShippingDetailDrawer from './components/ShippingDetailDrawer.vue'
import ShippingBatchActionDialog from './components/ShippingBatchActionDialog.vue'
const message = useMessage()
const loading = ref(false)
const page = ref(1)
const items = ref([])
const stats = ref({})
const detailVisible = ref(false)
const detailId = ref(null)
const selectedRowKeys = ref([])
const showBatchDialog = ref(false)
const showCreateModal = ref(false)
const creating = ref(false)
const carrierOptions = ref([])
const statusOptions = ref([])
const filters = reactive({
status: null,
order_number: '',
tracking_no: '',
date_from: '',
date_to: '',
})
const createForm = reactive({
order_number: '',
carrier: '',
tracking_no: '',
note: '',
})
const pagination = reactive({
page: 1,
pageSize: 20,
itemCount: 0,
})
const statCards = [
{ key: 'shipped', label: '已发货', icon: 'fas fa-box', color: '#0f766e' },
{ key: 'in_transit', label: '运输中', icon: 'fas fa-truck', color: '#2563eb' },
{ key: 'delivered', label: '已签收', icon: 'fas fa-check-circle', color: '#16a34a' },
{ key: 'returned', label: '已退回', icon: 'fas fa-undo', color: '#dc2626' },
{ key: 'total', label: '总数', icon: 'fas fa-layer-group', color: '#7c3aed' },
]
const batchStatusOptions = computed(() => statusOptions.value.filter((item) => item.value !== 'shipped'))
const columnOptions = [
{ key: 'order_number', label: '订单号' },
{ key: 'carrier_label', label: '物流公司' },
{ key: 'tracking_no', label: '物流单号' },
{ key: 'status_label', label: '状态' },
{ key: 'operator', label: '处理人' },
{ key: 'address', label: '收货信息' },
{ key: 'created_at', label: '创建时间' },
]
const visibleColumnKeys = ref(columnOptions.map((item) => item.key))
const baseColumns = [
{ title: '订单号', key: 'order_number', width: 170, render: (row) => h('code', row.order_number) },
{ title: '物流公司', key: 'carrier_label', width: 120 },
{ title: '物流单号', key: 'tracking_no', width: 180, render: (row) => h('code', row.tracking_no) },
{
title: '状态',
key: 'status_label',
width: 100,
render: (row) => h(NTag, { size: 'small', type: resolveTagType(row.status) }, () => row.status_label || row.status),
},
{ title: '处理人', key: 'operator', width: 120, render: (row) => row.operator?.display_name || '-' },
{ title: '收货信息', key: 'address', ellipsis: { tooltip: true }, render: (row) => row.order_context?.address?.full_text || '-' },
{ title: '创建时间', key: 'created_at', width: 160 },
]
const columns = computed(() => {
const visible = new Set(visibleColumnKeys.value)
const filteredColumns = baseColumns.filter((column) => visible.has(column.key))
return [
{ type: 'selection', width: 40 },
...filteredColumns,
{
title: '操作',
key: 'actions',
width: 160,
render: (row) => h('div', { class: 'aksrt-shipping__action-buttons' }, [
h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => openDetail(row) }, () => '详情'),
h(NButton, { size: 'small', quaternary: true, onClick: () => copyAddress(row) }, () => '复制地址'),
]),
},
]
})
function resolveTagType(status) {
if (status === 'delivered') return 'success'
if (status === 'in_transit') return 'info'
if (status === 'returned') return 'error'
return 'warning'
}
async function loadData(nextPage = 1) {
loading.value = true
page.value = nextPage
pagination.page = nextPage
try {
const res = await api.request('/shop/shipping', {
params: {
page: nextPage,
per_page: pagination.pageSize,
status: filters.status || '',
order_number: filters.order_number || '',
tracking_no: filters.tracking_no || '',
date_from: filters.date_from || '',
date_to: filters.date_to || '',
},
})
const data = res?.data || {}
items.value = data.items || []
stats.value = data.stats || {}
pagination.itemCount = data.total || 0
carrierOptions.value = data.options?.carrier || []
statusOptions.value = [{ label: '全部状态', value: null }, ...(data.options?.status || [])]
} catch (error) {
message.error(error?.message || '加载物流数据失败')
} finally {
loading.value = false
}
}
function handlePageChange(nextPage) {
loadData(nextPage)
}
function openDetail(row) {
detailId.value = row.id
detailVisible.value = true
}
function handleBatchUpdated() {
selectedRowKeys.value = []
loadData(page.value)
}
function resetFilters() {
filters.status = null
filters.order_number = ''
filters.tracking_no = ''
filters.date_from = ''
filters.date_to = ''
loadData(1)
}
async function copyAddress(row) {
const text = row.order_context?.address?.full_text || ''
if (!text) {
message.warning('当前记录没有地址信息')
return
}
try {
await navigator.clipboard.writeText(text)
message.success('地址已复制')
} catch {
message.warning('复制失败,请手动复制')
}
}
async function submitCreate() {
if (!createForm.order_number || !createForm.carrier || !createForm.tracking_no) {
message.warning('请填写订单号、物流公司和物流单号')
return
}
creating.value = true
try {
await api.request('/shop/shipping', {
method: 'POST',
data: { ...createForm },
})
message.success('发货记录已创建')
showCreateModal.value = false
createForm.order_number = ''
createForm.carrier = ''
createForm.tracking_no = ''
createForm.note = ''
loadData(1)
} catch (error) {
message.error(error?.message || '创建发货记录失败')
} finally {
creating.value = false
}
}
onMounted(() => loadData(1))
</script>
<style scoped>
.aksrt-shipping__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.aksrt-shipping__stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
}
.aksrt-shipping__stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.aksrt-shipping__stat-value {
font-size: 22px;
font-weight: 700;
color: #111827;
}
.aksrt-shipping__stat-label {
font-size: 12px;
color: #6b7280;
}
.aksrt-shipping__section {
margin-bottom: 16px;
}
.aksrt-shipping__toolbar {
margin-top: 14px;
}
.aksrt-shipping__action-buttons {
display: flex;
gap: 4px;
}
.aksrt-shipping__modal {
width: min(520px, calc(100vw - 32px));
}
</style>
@@ -1,138 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="提现管理" desc="审核和管理创作者提现申请。" />
<!-- 筛选栏 -->
<n-card size="small" class="aksrt-shop__section">
<n-space>
<n-select v-model:value="statusFilter" :options="statusOptions" placeholder="提现状态" clearable style="width: 140px;" @update:value="loadWithdrawals" />
</n-space>
</n-card>
<!-- 提现列表 -->
<n-card size="small" class="aksrt-shop__section">
<n-data-table
:columns="columns"
:data="withdrawals"
:loading="loading"
:bordered="false"
size="small"
:pagination="pagination"
@update:page="handlePageChange"
/>
</n-card>
<!-- 审核弹窗 -->
<WithdrawalReviewModal
v-model:show="reviewVisible"
:withdrawal="reviewItem"
@updated="loadWithdrawals"
/>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, h } from 'vue'
import { NTag, NButton } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import WithdrawalReviewModal from './components/WithdrawalReviewModal.vue'
import api from '@/api/index.js'
const loading = ref(false)
const withdrawals = ref([])
const statusFilter = ref(null)
const reviewVisible = ref(false)
const reviewItem = ref({})
const statusOptions = [
{ label: '待审核', value: 'pending' },
{ label: '已批准', value: 'approved' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已打款', value: 'completed' },
]
const statusMap = {
pending: { label: '待审核', type: 'warning' },
approved: { label: '已批准', type: 'info' },
processing: { label: '处理中', type: 'info' },
completed: { label: '已打款', type: 'success' },
rejected: { label: '已拒绝', type: 'error' },
}
// Unified pagination: page + per_page → items + total + page + total_pages
const pagination = reactive({
page: 1,
pageSize: 20,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50],
})
const columns = [
{ title: '申请人', key: 'user_name', width: 120 },
{ title: '金额', key: 'amount', width: 120, render: (row) => '¥' + (row.amount || 0).toFixed(2) },
{ title: '提现方式', key: 'method', width: 100 },
{ title: '收款账号', key: 'account_info', width: 160, ellipsis: { tooltip: true } },
{
title: '状态',
key: 'status',
width: 100,
render: (row) => {
const s = statusMap[row.status] || { label: row.status, type: 'default' }
return h(NTag, { size: 'small', type: s.type }, () => s.label)
},
},
{ title: '申请时间', key: 'created_at', width: 160 },
{
title: '操作',
key: 'actions',
width: 120,
render: (row) => {
if (row.status !== 'pending') {
return h('span', { style: 'color:#aaa;font-size:12px;' }, '—')
}
return h(NButton, { size: 'tiny', quaternary: true, type: 'info', onClick: () => openReview(row) }, () => '审核')
},
},
]
async function loadWithdrawals() {
loading.value = true
try {
const params = new URLSearchParams({
per_page: String(pagination.pageSize),
page: String(pagination.page),
})
if (statusFilter.value) params.set('status', statusFilter.value)
const res = await api.request('/payment/withdrawals?' + params.toString())
if (res?.data) {
withdrawals.value = res.data.items || []
pagination.itemCount = res.data.total || 0
}
} catch {
// Endpoint may not exist yet
} finally {
loading.value = false
}
}
function handlePageChange(page) {
pagination.page = page
loadWithdrawals()
}
function openReview(row) {
reviewItem.value = row
reviewVisible.value = true
}
onMounted(loadWithdrawals)
</script>
<style scoped>
.aksrt-shop__section {
margin-bottom: 16px;
}
</style>
@@ -1,283 +0,0 @@
<template>
<n-drawer :show="show" :width="640" @update:show="$emit('update:show', $event)">
<n-drawer-content title="售后详情" closable>
<n-spin :show="loading">
<!-- 售后状态头 -->
<div class="aksrt-aftersale__status-header" :class="'aksrt-aftersale__status-header--' + (detail.record?.status || 'default')">
<n-tag :type="statusType" size="large">{{ statusLabel }}</n-tag>
<span class="aksrt-aftersale__order-no">订单号:{{ detail.record?.order_number || '-' }}</span>
</div>
<!-- 售后信息 -->
<n-descriptions bordered :column="2" size="small" label-placement="left" style="margin-top:16px;">
<n-descriptions-item label="售后类型">{{ detail.record?.type_label || detail.record?.type || '-' }}</n-descriptions-item>
<n-descriptions-item label="申请阶段">
<n-tag size="small">{{ detail.record?.stage?.label || '-' }}</n-tag>
</n-descriptions-item>
<n-descriptions-item label="申请金额">
<n-text type="error" strong>¥{{ Number(detail.record?.amount || 0).toFixed(2) }}</n-text>
</n-descriptions-item>
<n-descriptions-item label="退款金额">
<n-text type="error" strong>¥{{ Number(detail.record?.refund_amount || 0).toFixed(2) }}</n-text>
</n-descriptions-item>
<n-descriptions-item label="申请原因" :span="2">{{ detail.record?.reason || '-' }}</n-descriptions-item>
<n-descriptions-item label="申请人">{{ detail.record?.applicant?.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="申请时间">{{ detail.record?.created_at || '-' }}</n-descriptions-item>
<n-descriptions-item label="处理人">{{ detail.record?.operator?.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="处理时间">{{ detail.record?.processed_at || '-' }}</n-descriptions-item>
<n-descriptions-item v-if="detail.record?.operator_note" label="处理备注" :span="2">{{ detail.record.operator_note }}</n-descriptions-item>
</n-descriptions>
<!-- 退款渠道 -->
<n-card v-if="detail.record?.refund_channel" size="small" title="退款渠道" :bordered="false" style="margin-top:16px;">
<n-descriptions :column="2" size="small" label-placement="left">
<n-descriptions-item label="退款方式">{{ detail.record.refund_channel.method_label || detail.record.refund_channel.method || '-' }}</n-descriptions-item>
<n-descriptions-item label="退款状态">
<n-tag :type="detail.record.refund_channel.status === 'completed' ? 'success' : 'warning'" size="small">
{{ detail.record.refund_channel.status_label || detail.record.refund_channel.status || '-' }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="退款单号">{{ detail.record.refund_channel.refund_id || '-' }}</n-descriptions-item>
<n-descriptions-item label="退款时间">{{ detail.record.refund_channel.completed_at || '-' }}</n-descriptions-item>
</n-descriptions>
</n-card>
<!-- 退货地址 -->
<n-card v-if="detail.record?.return_address" size="small" title="退货地址" :bordered="false" style="margin-top:12px;">
<n-descriptions :column="1" size="small" label-placement="left">
<n-descriptions-item label="收货人">{{ detail.record.return_address.name || '-' }}</n-descriptions-item>
<n-descriptions-item label="电话">{{ detail.record.return_address.phone || '-' }}</n-descriptions-item>
<n-descriptions-item label="地址">{{ detail.record.return_address.full_address || '-' }}</n-descriptions-item>
</n-descriptions>
<n-button size="small" @click="copyReturnAddress" style="margin-top:8px;">
<i class="fas fa-copy" style="margin-right:4px;" /> 复制地址
</n-button>
</n-card>
<!-- 回寄物流 -->
<n-card v-if="detail.record?.return_shipping" size="small" title="回寄物流" :bordered="false" style="margin-top:12px;">
<n-descriptions :column="2" size="small" label-placement="left">
<n-descriptions-item label="物流公司">{{ detail.record.return_shipping.company || '-' }}</n-descriptions-item>
<n-descriptions-item label="物流单号">{{ detail.record.return_shipping.tracking_number || '-' }}</n-descriptions-item>
<n-descriptions-item label="回寄时间">{{ detail.record.return_shipping.shipped_at || '-' }}</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag size="small" :type="detail.record.return_shipping.status === 'received' ? 'success' : 'info'">
{{ detail.record.return_shipping.status_label || detail.record.return_shipping.status || '-' }}
</n-tag>
</n-descriptions-item>
</n-descriptions>
<n-timeline v-if="detail.record.return_shipping.tracking_detail?.length" style="margin-top:12px;">
<n-timeline-item
v-for="(item, i) in detail.record.return_shipping.tracking_detail"
:key="i"
:title="item.description"
:content="item.time"
:type="i === 0 ? 'success' : 'default'"
/>
</n-timeline>
</n-card>
<!-- 售后联动 -->
<n-card size="small" title="售后联动" :bordered="false" style="margin-top:12px;">
<div class="aksrt-aftersale__context-item">
关联物流:
<span>
{{ detail.record?.order_context?.shipping?.latest?.status_label || detail.record?.order_context?.shipping?.latest?.status || '暂无物流记录' }}
<template v-if="detail.record?.order_context?.shipping?.latest?.tracking_no">
/ {{ detail.record.order_context.shipping.latest.carrier_label || '-' }}
/ {{ detail.record.order_context.shipping.latest.tracking_no }}
</template>
</span>
</div>
<div v-if="detail.record?.order_context?.address?.full_text" class="aksrt-aftersale__context-item">
回寄参考地址:
<span>{{ detail.record.order_context.address.full_text }}</span>
</div>
</n-card>
<!-- 状态更新 -->
<n-card size="small" title="状态更新" :bordered="false" style="margin-top:16px;">
<n-space vertical>
<n-input v-model:value="note" type="textarea" :rows="2" placeholder="处理备注" />
<n-space>
<n-select v-model:value="nextStatus" :options="actionOptions" placeholder="选择状态" style="width:160px;" />
<n-button type="primary" :disabled="!nextStatus" :loading="updating" @click="handleStatusUpdate">更新状态</n-button>
</n-space>
</n-space>
</n-card>
<!-- 操作时间线 -->
<div v-if="detail.timeline?.length" class="aksrt-aftersale__timeline">
<div class="aksrt-aftersale__section-title">操作时间线</div>
<n-timeline size="small">
<n-timeline-item
v-for="(item, index) in detail.timeline"
:key="index"
:time="item.time"
:title="item.label"
:content="item.detail || ''"
:type="item.type === 'refund' ? 'warning' : item.type === 'shipping' ? 'info' : 'success'"
/>
</n-timeline>
</div>
</n-spin>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api/index.js'
const props = defineProps({
show: { type: Boolean, default: false },
afterSaleId: { type: [Number, String], default: null },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const loading = ref(false)
const updating = ref(false)
const detail = ref({ record: null, timeline: [] })
const nextStatus = ref(null)
const note = ref('')
const statusMap = {
pending: { label: '待处理', type: 'warning' },
processing: { label: '处理中', type: 'info' },
approved: { label: '已通过', type: 'success' },
rejected: { label: '已拒绝', type: 'error' },
completed: { label: '已完成', type: 'success' },
cancelled: { label: '已取消', type: 'default' },
}
const statusLabel = computed(() => {
const s = detail.value?.record?.status
return (statusMap[s] || { label: s || '-' }).label
})
const statusType = computed(() => {
const s = detail.value?.record?.status
return (statusMap[s] || { type: 'default' }).type
})
const actionOptions = computed(() => {
const status = detail.value?.record?.status
if (status === 'pending') {
return [
{ label: '处理中', value: 'processing' },
{ label: '已通过', value: 'approved' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已取消', value: 'cancelled' },
]
}
if (status === 'processing') {
return [
{ label: '已通过', value: 'approved' },
{ label: '已拒绝', value: 'rejected' },
{ label: '已取消', value: 'cancelled' },
]
}
if (status === 'approved') {
return [
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' },
]
}
return []
})
watch(() => props.show, async (visible) => {
if (visible && props.afterSaleId) {
await loadDetail()
} else if (!visible) {
detail.value = { record: null, timeline: [] }
nextStatus.value = null
note.value = ''
}
})
async function loadDetail() {
loading.value = true
try {
const res = await api.request(`/shop/after-sale/${props.afterSaleId}`)
detail.value = res?.data || { record: null, timeline: [] }
} catch (error) {
message.error(error?.message || '加载售后详情失败')
} finally {
loading.value = false
}
}
async function handleStatusUpdate() {
if (!nextStatus.value) return
updating.value = true
try {
await api.request(`/shop/after-sale/${props.afterSaleId}/update-status`, {
method: 'POST',
data: { status: nextStatus.value, operator_note: note.value },
})
message.success('售后状态已更新')
nextStatus.value = null
note.value = ''
await loadDetail()
emit('updated')
} catch (error) {
message.error(error?.message || '状态更新失败')
} finally {
updating.value = false
}
}
async function copyReturnAddress() {
const addr = detail.value?.record?.return_address
if (!addr) return
const text = `${addr.name} ${addr.phone} ${addr.full_address}`
try {
await navigator.clipboard.writeText(text)
message.success('退货地址已复制')
} catch {
message.warning('复制失败')
}
}
</script>
<style scoped>
.aksrt-aftersale__status-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 8px;
background: var(--surface-2);
}
.aksrt-aftersale__status-header--pending { background: #fff7e6; }
.aksrt-aftersale__status-header--processing { background: #e6f7ff; }
.aksrt-aftersale__status-header--approved { background: #f6ffed; }
.aksrt-aftersale__status-header--rejected { background: #fff1f0; }
.aksrt-aftersale__status-header--completed { background: #f6ffed; }
.aksrt-aftersale__status-header--cancelled { background: #f5f5f5; }
.aksrt-aftersale__order-no {
font-size: 13px;
color: var(--color-ink-3);
}
.aksrt-aftersale__context-item {
margin-top: 8px;
color: var(--color-ink-2);
line-height: 1.6;
}
.aksrt-aftersale__timeline {
margin-top: 18px;
}
.aksrt-aftersale__section-title {
margin-bottom: 8px;
font-weight: 600;
color: var(--color-ink);
}
</style>
@@ -1,107 +0,0 @@
<template>
<div class="aksrt-order-context">
<n-card size="small" title="订单上下文" :bordered="false">
<template v-if="context?.exists">
<n-descriptions bordered :column="1" size="small" label-placement="left">
<n-descriptions-item label="订单号">{{ context.summary?.order_number || '-' }}</n-descriptions-item>
<n-descriptions-item label="订单状态">{{ context.summary?.status_label || context.summary?.status || '-' }}</n-descriptions-item>
<n-descriptions-item label="买家">
{{ context.buyer?.display_name || '-' }}
<span v-if="context.buyer?.user_login" class="aksrt-order-context__muted">({{ context.buyer.user_login }})</span>
</n-descriptions-item>
<n-descriptions-item v-if="context.buyer?.user_email" label="联系邮箱">{{ context.buyer.user_email }}</n-descriptions-item>
<n-descriptions-item label="支付方式">{{ context.payment_method?.label || context.payment_method?.value || '-' }}</n-descriptions-item>
<n-descriptions-item label="配送方式">{{ context.shipping_method?.label || context.shipping_method?.value || '-' }}</n-descriptions-item>
<n-descriptions-item label="收货地址">{{ context.address?.full_text || '-' }}</n-descriptions-item>
<n-descriptions-item v-if="context.coupon?.code || context.coupon?.label" label="优惠信息">
{{ context.coupon?.label || context.coupon?.code }}
<span v-if="context.coupon?.discount" class="aksrt-order-context__muted">减免 ¥{{ formatAmount(context.coupon.discount) }}</span>
</n-descriptions-item>
<n-descriptions-item label="金额">
¥{{ formatAmount(context.pricing?.amount) }}
<span class="aksrt-order-context__muted">
小计 ¥{{ formatAmount(context.pricing?.subtotal) }} / 运费 ¥{{ formatAmount(context.pricing?.shipping_fee) }} / 优惠 ¥{{ formatAmount(context.pricing?.discount) }}
</span>
</n-descriptions-item>
<n-descriptions-item label="物流联动">
<template v-if="context.shipping?.latest?.status_label || context.shipping?.count">
{{ context.shipping?.latest?.status_label || context.shipping?.latest?.status || '-' }}
<span v-if="context.shipping?.latest?.tracking_no" class="aksrt-order-context__muted">
{{ context.shipping.latest.carrier_label || context.shipping.latest.carrier || '' }} / {{ context.shipping.latest.tracking_no }}
</span>
</template>
<template v-else>-</template>
</n-descriptions-item>
<n-descriptions-item label="售后联动">
<template v-if="context.after_sale?.latest?.status_label || context.after_sale?.count">
{{ context.after_sale?.latest?.status_label || context.after_sale?.latest?.status || '-' }}
<span v-if="context.after_sale?.latest?.stage?.label" class="aksrt-order-context__muted">
{{ context.after_sale.latest.stage.label }}
</span>
</template>
<template v-else>-</template>
</n-descriptions-item>
<n-descriptions-item v-if="context.customer_note" label="用户备注">{{ context.customer_note }}</n-descriptions-item>
</n-descriptions>
<div v-if="Array.isArray(context.items) && context.items.length" class="aksrt-order-context__items">
<div v-for="(item, index) in context.items" :key="index" class="aksrt-order-context__item">
<div class="aksrt-order-context__item-title">{{ item.product_name || `商品 #${item.product_id || index + 1}` }}</div>
<div class="aksrt-order-context__item-meta">数量 {{ item.quantity || 1 }} / 小计 ¥{{ formatAmount(item.line_total) }}</div>
</div>
</div>
</template>
<n-empty v-else description="未找到订单上下文" size="small" />
</n-card>
</div>
</template>
<script setup>
defineProps({
context: {
type: Object,
default: () => ({}),
},
})
function formatAmount(value) {
return Number(value || 0).toFixed(2)
}
</script>
<style scoped>
.aksrt-order-context {
margin-top: 16px;
}
.aksrt-order-context__muted {
display: inline-block;
margin-left: 8px;
color: #8c8c8c;
font-size: 12px;
}
.aksrt-order-context__items {
display: grid;
gap: 8px;
margin-top: 12px;
}
.aksrt-order-context__item {
padding: 10px 12px;
border: 1px solid #edf2f7;
border-radius: 10px;
background: #f8fafc;
}
.aksrt-order-context__item-title {
font-weight: 600;
color: #1f2937;
}
.aksrt-order-context__item-meta {
margin-top: 4px;
color: #6b7280;
font-size: 12px;
}
</style>
@@ -1,479 +0,0 @@
<template>
<n-drawer :show="show" :width="640" @update:show="$emit('update:show', $event)">
<n-drawer-content title="订单详情" closable>
<n-spin :show="loading">
<!-- 订单状态头 -->
<div class="aksrt-order-drawer__status-header" :class="'aksrt-order-drawer__status-header--' + order.status">
<n-tag :type="statusType" size="large">{{ statusLabel }}</n-tag>
<span class="aksrt-order-drawer__order-no">订单号:{{ order.order_number || '-' }}</span>
</div>
<!-- 核心信息 -->
<n-descriptions bordered :column="2" label-placement="left" size="small" style="margin-top:16px;">
<n-descriptions-item label="商品" :span="2">{{ order.product_name || order.product_type || '-' }}</n-descriptions-item>
<n-descriptions-item label="订单金额">
<n-text type="error" strong>¥{{ (order.amount || 0).toFixed(2) }}</n-text>
</n-descriptions-item>
<n-descriptions-item label="支付方式">{{ order.payment_method?.label || order.payment_method || '-' }}</n-descriptions-item>
<n-descriptions-item label="小计">¥{{ (order.subtotal || 0).toFixed(2) }}</n-descriptions-item>
<n-descriptions-item label="运费">¥{{ (order.shipping_fee || 0).toFixed(2) }}</n-descriptions-item>
<n-descriptions-item label="优惠">¥{{ (order.discount || 0).toFixed(2) }}</n-descriptions-item>
<n-descriptions-item v-if="order.payment_transaction_id" label="交易号" :span="2">{{ order.payment_transaction_id }}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ order.created_at || '-' }}</n-descriptions-item>
<n-descriptions-item v-if="order.paid_at" label="支付时间">{{ order.paid_at }}</n-descriptions-item>
<n-descriptions-item v-if="order.completed_at" label="完成时间">{{ order.completed_at }}</n-descriptions-item>
<n-descriptions-item v-if="order.note" label="备注" :span="2">{{ order.note }}</n-descriptions-item>
</n-descriptions>
<!-- 买家信息 -->
<n-card size="small" title="买家信息" :bordered="false" style="margin-top:16px;">
<n-descriptions :column="2" label-placement="left" size="small">
<n-descriptions-item label="用户 ID">{{ order.user_id || '-' }}</n-descriptions-item>
<n-descriptions-item label="用户名">{{ buyer.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="邮箱">{{ buyer.email || '-' }}</n-descriptions-item>
<n-descriptions-item label="手机">{{ buyer.phone || '-' }}</n-descriptions-item>
</n-descriptions>
</n-card>
<!-- 收货地址 -->
<n-card v-if="address" size="small" title="收货地址" :bordered="false" style="margin-top:12px;">
<n-descriptions :column="1" label-placement="left" size="small">
<n-descriptions-item label="收货人">{{ address.name || '-' }}</n-descriptions-item>
<n-descriptions-item label="电话">{{ address.phone || '-' }}</n-descriptions-item>
<n-descriptions-item label="地址">{{ address.full_address || '-' }}</n-descriptions-item>
</n-descriptions>
</n-card>
<!-- 物流信息 -->
<n-card v-if="shipping" size="small" title="物流信息" :bordered="false" style="margin-top:12px;">
<n-descriptions :column="2" label-placement="left" size="small">
<n-descriptions-item label="物流公司">{{ shipping.company || '-' }}</n-descriptions-item>
<n-descriptions-item label="物流单号">{{ shipping.tracking_number || '-' }}</n-descriptions-item>
<n-descriptions-item label="发货时间">{{ shipping.shipped_at || '-' }}</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag :type="shipping.status === 'delivered' ? 'success' : 'info'" size="small">
{{ shipping.status_label || shipping.status || '-' }}
</n-tag>
</n-descriptions-item>
</n-descriptions>
<n-timeline v-if="shipping.tracking_detail?.length" style="margin-top:12px;">
<n-timeline-item
v-for="(item, i) in shipping.tracking_detail"
:key="i"
:title="item.description"
:content="item.time"
:type="i === 0 ? 'success' : 'default'"
/>
</n-timeline>
</n-card>
<!-- 售后信息 -->
<n-card v-if="afterSale" size="small" title="售后信息" :bordered="false" style="margin-top:12px;">
<n-descriptions :column="2" label-placement="left" size="small">
<n-descriptions-item label="售后类型">{{ afterSale.type_label || afterSale.type || '-' }}</n-descriptions-item>
<n-descriptions-item label="售后状态">
<n-tag :type="afterSale.status === 'completed' ? 'success' : 'warning'" size="small">
{{ afterSale.status_label || afterSale.status || '-' }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="退款金额">¥{{ (afterSale.refund_amount || 0).toFixed(2) }}</n-descriptions-item>
<n-descriptions-item label="申请时间">{{ afterSale.created_at || '-' }}</n-descriptions-item>
<n-descriptions-item label="原因" :span="2">{{ afterSale.reason || '-' }}</n-descriptions-item>
</n-descriptions>
</n-card>
<!-- 操作时间线 -->
<n-card v-if="timeline.length" size="small" title="操作时间线" :bordered="false" style="margin-top:16px;">
<n-timeline>
<n-timeline-item
v-for="(entry, index) in timeline"
:key="index"
:type="entry.type === 'shipping' ? 'info' : entry.type === 'after_sale' ? 'warning' : 'success'"
:title="entry.title"
:content="entry.detail || ''"
:time="entry.time"
/>
</n-timeline>
</n-card>
<!-- 操作按钮 -->
<div v-if="order.order_number" class="aksrt-order-drawer__actions">
<n-divider />
<n-space wrap>
<n-button v-if="order.status === 'pending'" type="success" size="small" @click="updateStatus('paid')">
<template #icon><i class="fas fa-check" /></template>
确认支付
</n-button>
<n-button v-if="order.status === 'pending'" type="error" size="small" @click="updateStatus('cancelled')">
<template #icon><i class="fas fa-times" /></template>
取消订单
</n-button>
<n-button v-if="order.status === 'paid'" type="info" size="small" @click="showShipModal = true">
<template #icon><i class="fas fa-truck" /></template>
发货
</n-button>
<n-button v-if="order.status === 'delivered'" type="success" size="small" @click="updateStatus('completed')">
<template #icon><i class="fas fa-check-double" /></template>
确认完成
</n-button>
<n-button v-if="['paid', 'delivered', 'completed'].includes(order.status)" type="warning" size="small" @click="showRefundModal = true">
<template #icon><i class="fas fa-undo" /></template>
申请退款
</n-button>
<n-button v-if="order.status === 'completed' && !afterSale" type="default" size="small" @click="showAfterSaleModal = true">
<template #icon><i class="fas fa-headset" /></template>
创建售后
</n-button>
</n-space>
</div>
</n-spin>
<template #footer>
<n-button @click="$emit('update:show', false)">关闭</n-button>
</template>
</n-drawer-content>
</n-drawer>
<!-- 发货弹窗 -->
<n-modal v-model:show="showShipModal" preset="card" title="发货" style="max-width:480px;" :bordered="false">
<n-form label-placement="top">
<n-form-item label="物流公司">
<n-select v-model:value="shipForm.company" :options="shippingCompanies" placeholder="选择物流公司" />
</n-form-item>
<n-form-item label="物流单号">
<n-input v-model:value="shipForm.tracking_number" placeholder="填写物流单号" />
</n-form-item>
<n-form-item label="备注">
<n-input v-model:value="shipForm.note" type="textarea" :rows="2" placeholder="发货备注(可选)" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="showShipModal = false">取消</n-button>
<n-button type="info" :loading="shipLoading" @click="submitShip">确认发货</n-button>
</n-space>
</template>
</n-modal>
<!-- 退款弹窗 -->
<n-modal v-model:show="showRefundModal" preset="card" title="申请退款" style="max-width:420px;" :bordered="false">
<n-form label-placement="top">
<n-form-item label="退款金额 (元)">
<n-input-number v-model:value="refundAmount" :min="0" :max="order.amount || 0" :precision="2" style="width:100%;" />
<n-text depth="3" style="font-size:12px;margin-top:4px;">订单总额 ¥{{ (order.amount || 0).toFixed(2) }},支持部分退款</n-text>
</n-form-item>
<n-form-item label="退款原因">
<n-input v-model:value="refundReason" type="textarea" :rows="3" placeholder="填写退款原因(可选)" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="showRefundModal = false">取消</n-button>
<n-button type="warning" :loading="refundLoading" @click="submitRefund">确认退款</n-button>
</n-space>
</template>
</n-modal>
<!-- 售后弹窗 -->
<n-modal v-model:show="showAfterSaleModal" preset="card" title="创建售后" style="max-width:480px;" :bordered="false">
<n-form label-placement="top">
<n-form-item label="售后类型">
<n-select v-model:value="afterSaleForm.type" :options="afterSaleTypes" />
</n-form-item>
<n-form-item label="退款金额">
<n-input-number v-model:value="afterSaleForm.refund_amount" :min="0" :max="order.amount || 0" :precision="2" style="width:100%;" />
</n-form-item>
<n-form-item label="原因">
<n-input v-model:value="afterSaleForm.reason" type="textarea" :rows="3" placeholder="售后原因" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="showAfterSaleModal = false">取消</n-button>
<n-button type="warning" :loading="afterSaleLoading" @click="submitAfterSale">提交售后</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api/index.js'
const props = defineProps({
show: { type: Boolean, default: false },
orderNumber: { type: String, default: '' },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const loading = ref(false)
const order = ref({})
const buyer = ref({})
const address = ref(null)
const shipping = ref(null)
const afterSale = ref(null)
const timeline = ref([])
// 发货
const showShipModal = ref(false)
const shipLoading = ref(false)
const shipForm = ref({ company: '', tracking_number: '', note: '' })
const shippingCompanies = [
{ label: '顺丰速运', value: 'sf' },
{ label: '圆通速递', value: 'yt' },
{ label: '中通快递', value: 'zt' },
{ label: '韵达快递', value: 'yd' },
{ label: '申通快递', value: 'sto' },
{ label: '百世快递', value: 'ht' },
{ label: '极兔速递', value: 'jt' },
{ label: '邮政快递', value: 'ems' },
{ label: '其他', value: 'other' },
]
// 退款
const showRefundModal = ref(false)
const refundAmount = ref(0)
const refundReason = ref('')
const refundLoading = ref(false)
// 售后
const showAfterSaleModal = ref(false)
const afterSaleLoading = ref(false)
const afterSaleForm = ref({ type: 'refund', refund_amount: 0, reason: '' })
const afterSaleTypes = [
{ label: '仅退款', value: 'refund' },
{ label: '退货退款', value: 'refund_return' },
{ label: '换货', value: 'exchange' },
{ label: '维修', value: 'repair' },
]
const statusMap = {
created: { label: '已创建', type: 'default' },
pending: { label: '待支付', type: 'warning' },
paid: { label: '已支付', type: 'success' },
delivered: { label: '已发货', type: 'info' },
completed: { label: '已完成', type: 'success' },
cancelled: { label: '已取消', type: 'default' },
expired: { label: '已过期', type: 'default' },
refunding: { label: '退款中', type: 'warning' },
refunded: { label: '已退款', type: 'error' },
}
const statusLabel = computed(() => (statusMap[order.value.status] || { label: order.value.status }).label)
const statusType = computed(() => (statusMap[order.value.status] || { type: 'default' }).type)
watch(() => props.show, async (val) => {
if (val && props.orderNumber) {
await loadOrder()
} else if (!val) {
resetState()
}
})
async function loadOrder() {
loading.value = true
try {
const res = await api.request(`/shop/orders/${props.orderNumber}`)
applyOrderResponse(res)
} catch {
message.error('加载订单详情失败')
} finally {
loading.value = false
}
}
function applyOrderResponse(res) {
if (!res) return
order.value = res.data || {}
const ctx = res.context || {}
buyer.value = ctx.buyer || {}
const addr = ctx.address || null
if (addr && !addr.full_address) {
addr.full_address = addr.full_text || [addr.province, addr.city, addr.district, addr.address].filter(Boolean).join(' ')
}
address.value = addr && Object.keys(addr).length ? addr : null
const shipLatest = ctx.shipping?.latest || null
if (shipLatest && Object.keys(shipLatest).length) {
shipping.value = {
company: shipLatest.carrier_label || shipLatest.carrier || '',
tracking_number: shipLatest.tracking_no || '',
shipped_at: shipLatest.created_at || '',
status: shipLatest.status || '',
status_label: shipLatest.status_label || '',
tracking_detail: shipLatest.tracking_detail || [],
}
} else {
shipping.value = null
}
const asLatest = ctx.after_sale?.latest || null
if (asLatest && Object.keys(asLatest).length) {
afterSale.value = {
type: asLatest.type || '',
type_label: asLatest.type_label || '',
status: asLatest.status || '',
status_label: asLatest.status_label || '',
refund_amount: asLatest.amount ?? asLatest.refund_amount ?? 0,
created_at: asLatest.created_at || '',
reason: asLatest.reason || '',
}
} else {
afterSale.value = null
}
timeline.value = res.timeline || []
if (order.value.amount != null) {
refundAmount.value = order.value.amount || 0
}
}
function resetState() {
order.value = {}
buyer.value = {}
address.value = null
shipping.value = null
afterSale.value = null
timeline.value = []
showRefundModal.value = false
showShipModal.value = false
showAfterSaleModal.value = false
}
async function refreshOrder() {
try {
const res = await api.request(`/shop/orders/${props.orderNumber}`)
applyOrderResponse(res)
} catch { /* silent */ }
}
async function updateStatus(status) {
try {
await api.request('/shop/orders/update-status', {
method: 'POST',
data: { order_number: props.orderNumber, status },
})
message.success('状态已更新')
await refreshOrder()
emit('updated')
} catch {
message.error('操作失败')
}
}
async function submitShip() {
if (!shipForm.value.company || !shipForm.value.tracking_number) {
message.warning('请填写物流公司和物流单号')
return
}
shipLoading.value = true
try {
await api.request('/shop/shipping', {
method: 'POST',
data: {
order_number: props.orderNumber,
carrier: shipForm.value.company,
tracking_no: shipForm.value.tracking_number,
note: shipForm.value.note,
},
})
message.success('发货成功')
showShipModal.value = false
shipForm.value = { company: '', tracking_number: '', note: '' }
await refreshOrder()
emit('updated')
} catch (err) {
message.error(err?.message || '发货失败')
} finally {
shipLoading.value = false
}
}
async function submitRefund() {
refundLoading.value = true
try {
await api.request('/shop/orders/update-status', {
method: 'POST',
data: {
order_number: props.orderNumber,
status: 'refunding',
refund_amount: refundAmount.value,
refund_reason: refundReason.value,
},
})
message.success('退款申请已提交')
showRefundModal.value = false
refundReason.value = ''
await refreshOrder()
emit('updated')
} catch (err) {
message.error(err?.message || '退款操作失败')
} finally {
refundLoading.value = false
}
}
async function submitAfterSale() {
if (!afterSaleForm.value.reason) {
message.warning('请填写售后原因')
return
}
afterSaleLoading.value = true
try {
await api.request('/shop/after-sale', {
method: 'POST',
data: {
order_number: props.orderNumber,
type: afterSaleForm.value.type,
amount: afterSaleForm.value.refund_amount,
reason: afterSaleForm.value.reason,
},
})
message.success('售后申请已提交')
showAfterSaleModal.value = false
afterSaleForm.value = { type: 'refund', refund_amount: 0, reason: '' }
await refreshOrder()
emit('updated')
} catch (err) {
message.error(err?.message || '售后申请失败')
} finally {
afterSaleLoading.value = false
}
}
</script>
<style scoped>
.aksrt-order-drawer__status-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 8px;
background: var(--surface-2);
}
.aksrt-order-drawer__status-header--pending { background: #fff7e6; }
.aksrt-order-drawer__status-header--paid { background: #f6ffed; }
.aksrt-order-drawer__status-header--delivered { background: #e6f7ff; }
.aksrt-order-drawer__status-header--completed { background: #f6ffed; }
.aksrt-order-drawer__status-header--cancelled { background: #f5f5f5; }
.aksrt-order-drawer__status-header--refunding { background: #fff7e6; }
.aksrt-order-drawer__status-header--refunded { background: #fff1f0; }
.aksrt-order-drawer__order-no {
font-size: 13px;
color: var(--color-ink-3);
}
.aksrt-order-drawer__actions {
margin-top: 16px;
}
.aksrt-order-drawer__muted {
font-size: 12px;
color: var(--color-ink-4);
margin-left: 8px;
}
</style>
@@ -1,523 +0,0 @@
<template>
<n-drawer :show="show" :width="720" @update:show="$emit('update:show', $event)">
<n-drawer-content :title="isEdit ? '编辑商品' : '新增商品'" closable>
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
<!-- 基础信息 -->
<n-divider title-placement="left">基础信息</n-divider>
<n-form-item label="商品名称" path="title">
<n-input v-model:value="form.title" placeholder="输入商品名称" />
</n-form-item>
<n-grid :cols="2" :x-gap="12">
<n-gi>
<n-form-item label="价格 (元)" path="price">
<n-input-number v-model:value="form.price" :min="0" :precision="2" style="width:100%;" placeholder="0.00" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="库存" path="stock">
<n-input-number v-model:value="form.stock" :min="0" style="width:100%;" placeholder="0" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="商品简介" path="excerpt">
<n-input v-model:value="form.excerpt" type="textarea" :rows="3" placeholder="简短描述(可选)" />
</n-form-item>
<n-form-item label="商品详情" path="content">
<n-input v-model:value="form.content" type="textarea" :rows="6" placeholder="详细描述(支持 HTML)" />
</n-form-item>
<!-- 商品素材 -->
<n-divider title-placement="left">商品素材</n-divider>
<n-form-item label="主图 URL" path="image">
<n-input v-model:value="form.image" placeholder="https://example.com/image.jpg" />
</n-form-item>
<n-form-item label="图集 URL">
<n-input v-model:value="form.galleryText" type="textarea" :rows="3" placeholder="每行一个图片 URL" />
</n-form-item>
<!-- 分类与标签 -->
<n-divider title-placement="left">分类与标签</n-divider>
<n-form-item label="分类">
<n-space vertical style="width:100%;">
<n-select
v-model:value="form.categories"
:options="categoryOptions"
multiple
filterable
placeholder="选择分类(可多选)"
:loading="loadingTaxonomy"
style="width:100%;"
/>
<n-space>
<n-input
v-model:value="newCategoryName"
placeholder="新建分类名称"
style="min-width:180px;"
@keyup.enter="createCategory"
/>
<n-button :loading="creatingCategory" @click="createCategory">新建分类</n-button>
</n-space>
</n-space>
</n-form-item>
<n-form-item label="标签">
<n-select
v-model:value="form.tags"
:options="tagOptions"
multiple
filterable
tag
placeholder="选择或输入新标签后回车"
:loading="loadingTaxonomy"
@update:value="onTagsUpdate"
/>
</n-form-item>
<!-- 发货与售后 -->
<n-divider title-placement="left">发货与售后</n-divider>
<n-grid :cols="2" :x-gap="12">
<n-gi>
<n-form-item label="发货方式" path="shipping_mode">
<n-select v-model:value="form.shipping_mode" :options="shippingModeOptions" placeholder="选择发货方式" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="额外运费 (元)" path="shipping_fee">
<n-input-number v-model:value="form.shipping_fee" :min="0" :precision="2" style="width:100%;" placeholder="0.00" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="售后说明" path="after_sale_policy">
<n-input v-model:value="form.after_sale_policy" type="textarea" :rows="3" placeholder="售后政策说明(可选)" />
</n-form-item>
<n-form-item label="支持退款">
<n-switch v-model:value="form.support_refund" />
</n-form-item>
<!-- 发布设置 -->
<n-divider title-placement="left">发布设置</n-divider>
<n-form-item label="状态" path="status">
<n-select v-model:value="form.status" :options="statusOptions" />
</n-form-item>
<!-- SKU / 变体(编辑已有商品时可用;新建时先创建再编辑变体) -->
<n-divider title-placement="left">SKU 变体</n-divider>
<n-alert v-if="!isEdit" type="info" :bordered="false" style="margin-bottom:12px;">
新建商品请先保存,再编辑时配置 SKU 变体。
</n-alert>
<template v-else>
<n-space style="margin-bottom:12px;">
<n-button size="small" @click="addVariantRow">
<template #icon><i class="fas fa-plus" /></template>
添加变体
</n-button>
<n-button size="small" type="primary" ghost :loading="savingVariants" @click="saveVariantsOnly">
仅保存变体
</n-button>
<n-text depth="3" style="font-size:12px;">属性格式:名称:值,多项用分号分隔,如 颜色:红;尺寸:L</n-text>
</n-space>
<n-spin :show="loadingVariants">
<n-empty v-if="!variants.length" description="暂无变体,点击「添加变体」" style="margin:12px 0;" />
<div v-for="(row, index) in variants" :key="row._key || row.id || index" class="aksrt-variant-row">
<n-grid :cols="24" :x-gap="8" :y-gap="8">
<n-gi :span="6">
<n-input v-model:value="row.sku" placeholder="SKU" size="small" />
</n-gi>
<n-gi :span="8">
<n-input v-model:value="row.attrsText" placeholder="颜色:红;尺寸:L" size="small" />
</n-gi>
<n-gi :span="4">
<n-input-number v-model:value="row.price" :min="0" :precision="2" size="small" style="width:100%;" placeholder="价格" />
</n-gi>
<n-gi :span="4">
<n-input-number v-model:value="row.stock" :min="0" size="small" style="width:100%;" placeholder="库存" />
</n-gi>
<n-gi :span="2">
<n-button size="small" quaternary type="error" @click="removeVariantRow(index)">
<i class="fas fa-trash" />
</n-button>
</n-gi>
<n-gi :span="22">
<n-input v-model:value="row.image" placeholder="变体图片 URL(可选)" size="small" />
</n-gi>
</n-grid>
</div>
</n-spin>
</template>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="$emit('update:show', false)">取消</n-button>
<n-button type="primary" :loading="saving" @click="handleSave">
{{ isEdit ? '保存修改' : '创建商品' }}
</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import { ref, reactive, watch, computed } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api/index.js'
const props = defineProps({
show: { type: Boolean, default: false },
productId: { type: [Number, String], default: null },
})
const emit = defineEmits(['update:show', 'saved'])
const message = useMessage()
const formRef = ref(null)
const saving = ref(false)
const loadingTaxonomy = ref(false)
const loadingVariants = ref(false)
const savingVariants = ref(false)
const variants = ref([])
let variantKeySeq = 1
const isEdit = computed(() => !!props.productId)
const form = reactive({
title: '',
price: 0,
stock: 0,
excerpt: '',
content: '',
status: 'publish',
image: '',
galleryText: '',
categories: [],
tags: [],
shipping_mode: 'digital',
shipping_fee: 0,
after_sale_policy: '',
support_refund: true,
})
const rules = {
title: { required: true, message: '请输入商品名称', trigger: 'blur' },
price: { type: 'number', required: true, message: '请输入价格', trigger: 'blur' },
}
const statusOptions = [
{ label: '上架', value: 'publish' },
{ label: '下架', value: 'draft' },
]
const shippingModeOptions = [
{ label: '自动发货(虚拟商品)', value: 'digital' },
{ label: '手动发货', value: 'manual' },
{ label: '快递发货', value: 'express' },
]
const categoryOptions = ref([])
const tagOptions = ref([])
const newCategoryName = ref('')
const creatingCategory = ref(false)
const creatingTagNames = new Set()
// Load taxonomy options
async function loadTaxonomy() {
loadingTaxonomy.value = true
try {
const res = await api.request('/shop/products/taxonomy')
if (res?.data) {
categoryOptions.value = (res.data.categories || []).map(c => ({ label: c.name, value: c.id }))
tagOptions.value = (res.data.tags || []).map(t => ({ label: t.name, value: t.name }))
}
} catch {
message.warning('分类标签加载失败,可稍后重试')
} finally {
loadingTaxonomy.value = false
}
}
async function createCategory() {
const name = (newCategoryName.value || '').trim()
if (!name) {
message.warning('请输入分类名称')
return
}
creatingCategory.value = true
try {
const res = await api.request('/shop/products/taxonomy', {
method: 'POST',
data: { taxonomy: 'category', name },
})
const term = res?.data
if (term?.id) {
if (!categoryOptions.value.some(o => o.value === term.id)) {
categoryOptions.value = [...categoryOptions.value, { label: term.name, value: term.id }]
}
if (!form.categories.includes(term.id)) {
form.categories = [...form.categories, term.id]
}
newCategoryName.value = ''
message.success(term.existing ? '分类已存在,已选中' : '分类已创建')
}
} catch (err) {
message.error(err?.message || '创建分类失败')
} finally {
creatingCategory.value = false
}
}
async function ensureTagTerm(name) {
const label = String(name || '').trim()
if (!label || creatingTagNames.has(label)) return
const exists = tagOptions.value.some(o => o.value === label || o.label === label)
if (exists) return
creatingTagNames.add(label)
try {
const res = await api.request('/shop/products/taxonomy', {
method: 'POST',
data: { taxonomy: 'tag', name: label },
})
const term = res?.data
if (term?.name && !tagOptions.value.some(o => o.value === term.name)) {
tagOptions.value = [...tagOptions.value, { label: term.name, value: term.name }]
}
} catch {
// Keep typed tag in form; backend product save still accepts string tags.
} finally {
creatingTagNames.delete(label)
}
}
function onTagsUpdate(values) {
const list = Array.isArray(values) ? values : []
form.tags = list
list.forEach((name) => {
if (!tagOptions.value.some(o => o.value === name)) {
ensureTagTerm(name)
}
})
}
function attributesToText(attrs) {
if (!Array.isArray(attrs) || !attrs.length) return ''
return attrs
.map((item) => {
if (!item || typeof item !== 'object') return ''
const name = String(item.name ?? item.key ?? '').trim()
const value = String(item.value ?? '').trim()
if (!name && !value) return ''
return name ? `${name}:${value}` : value
})
.filter(Boolean)
.join(';')
}
function textToAttributes(text) {
const raw = String(text || '').trim()
if (!raw) return []
return raw
.split(/[;;\n]+/)
.map(part => part.trim())
.filter(Boolean)
.map((part) => {
const sep = part.includes(':') ? ':' : (part.includes(':') ? ':' : null)
if (!sep) return { name: '规格', value: part }
const [name, ...rest] = part.split(sep)
return { name: (name || '规格').trim(), value: rest.join(sep).trim() }
})
.filter(item => item.value !== '')
}
function mapVariantFromApi(row) {
return {
_key: `v_${variantKeySeq++}`,
id: row.id || 0,
sku: row.sku || '',
price: Number(row.price || 0),
stock: Number(row.stock || 0),
image: row.image || '',
attrsText: attributesToText(row.attributes),
}
}
function buildVariantsPayload() {
return variants.value.map((row, index) => ({
sku: String(row.sku || '').trim(),
price: Number(row.price || 0),
stock: Math.max(0, parseInt(row.stock, 10) || 0),
image: String(row.image || '').trim(),
attributes: textToAttributes(row.attrsText),
sort_order: index,
}))
}
function addVariantRow() {
variants.value = [
...variants.value,
{
_key: `v_${variantKeySeq++}`,
id: 0,
sku: '',
price: Number(form.price || 0),
stock: Number(form.stock || 0),
image: '',
attrsText: '',
},
]
}
function removeVariantRow(index) {
variants.value = variants.value.filter((_, i) => i !== index)
}
async function loadVariants(productId) {
if (!productId) {
variants.value = []
return
}
loadingVariants.value = true
try {
const res = await api.request(`/shop/products/${productId}/variants`)
const list = res?.data?.variants || res?.variants || []
variants.value = Array.isArray(list) ? list.map(mapVariantFromApi) : []
} catch {
variants.value = []
message.warning('变体加载失败')
} finally {
loadingVariants.value = false
}
}
async function persistVariants(productId) {
if (!productId) return false
await api.request(`/shop/products/${productId}/variants`, {
method: 'POST',
data: { variants: buildVariantsPayload() },
})
return true
}
async function saveVariantsOnly() {
if (!props.productId) {
message.warning('请先保存商品')
return
}
savingVariants.value = true
try {
await persistVariants(props.productId)
message.success('变体已保存')
await loadVariants(props.productId)
} catch (err) {
message.error(err?.message || '变体保存失败')
} finally {
savingVariants.value = false
}
}
// Load product data when editing
watch(() => props.show, async (val) => {
if (val) {
loadTaxonomy()
if (props.productId) {
try {
const res = await api.request(`/shop/products/${props.productId}`)
if (res?.data) {
const gallery = Array.isArray(res.data.gallery) ? res.data.gallery : []
Object.assign(form, {
title: res.data.title || '',
price: res.data.price || 0,
stock: res.data.stock || 0,
excerpt: res.data.excerpt || '',
content: res.data.content || '',
status: res.data.status || 'publish',
image: res.data.image || '',
galleryText: gallery.join('\n'),
categories: Array.isArray(res.data.categories) ? res.data.categories : [],
tags: Array.isArray(res.data.tags) ? res.data.tags : [],
shipping_mode: res.data.shipping_mode || 'digital',
shipping_fee: res.data.shipping_fee || 0,
after_sale_policy: res.data.after_sale_policy || '',
support_refund: res.data.support_refund !== false,
})
}
await loadVariants(props.productId)
} catch {
message.error('加载商品失败')
}
} else {
Object.assign(form, {
title: '', price: 0, stock: 0, excerpt: '', content: '', status: 'publish',
image: '', galleryText: '', categories: [], tags: [],
shipping_mode: 'digital', shipping_fee: 0, after_sale_policy: '', support_refund: true,
})
variants.value = []
}
}
})
async function handleSave() {
try {
await formRef.value?.validate()
} catch {
return
}
saving.value = true
try {
const payload = {
...form,
gallery: form.galleryText
.split('\n')
.map(s => s.trim())
.filter(Boolean),
}
delete payload.galleryText
if (isEdit.value) {
await api.request(`/shop/products/${props.productId}`, {
method: 'POST',
data: payload,
})
await persistVariants(props.productId)
message.success('商品已更新')
} else {
const res = await api.request('/shop/products', {
method: 'POST',
data: payload,
})
const productId = res?.data?.id || res?.id || null
if (productId && variants.value.length) {
await persistVariants(productId)
}
message.success('商品已创建')
}
emit('update:show', false)
emit('saved')
} catch (err) {
message.error(err?.message || '操作失败')
} finally {
saving.value = false
}
}
</script>
<style scoped>
.aksrt-variant-row {
padding: 10px;
margin-bottom: 8px;
border: 1px solid var(--n-border-color, #e5e7eb);
border-radius: 8px;
background: var(--n-color-modal, #fafafa);
}
</style>
@@ -1,94 +0,0 @@
<template>
<n-modal :show="show" preset="dialog" title="批量更新物流状态" @update:show="$emit('update:show', $event)">
<div class="aksrt-shipping-batch__body">
<div class="aksrt-shipping-batch__summary">已选 {{ ids.length }} 条记录</div>
<n-form label-placement="top">
<n-form-item label="目标状态">
<n-select v-model:value="form.status" :options="statusOptions" placeholder="选择状态" />
</n-form-item>
<n-form-item label="备注">
<n-input v-model:value="form.note" type="textarea" :rows="3" placeholder="批量操作备注" />
</n-form-item>
</n-form>
</div>
<template #action>
<n-space justify="end">
<n-button @click="$emit('update:show', false)">取消</n-button>
<n-button type="primary" :loading="submitting" @click="handleSubmit">提交</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { reactive, watch } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api/index.js'
const props = defineProps({
show: { type: Boolean, default: false },
ids: { type: Array, default: () => [] },
statusOptions: { type: Array, default: () => [] },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const form = reactive({
status: null,
note: '',
})
const submitting = reactive({ value: false })
watch(() => props.show, (visible) => {
if (visible) {
form.status = null
form.note = ''
}
})
async function handleSubmit() {
if (!form.status || !props.ids.length) {
message.warning('请选择目标状态并至少勾选一条记录')
return
}
submitting.value = true
try {
const res = await api.request('/shop/shipping/batch-update-status', {
method: 'POST',
data: {
ids: props.ids,
status: form.status,
note: form.note,
},
})
const updatedIds = Array.isArray(res?.data?.updated_ids) ? res.data.updated_ids : []
const failed = Array.isArray(res?.data?.failed) ? res.data.failed : []
if (failed.length) {
message.warning(`已更新 ${updatedIds.length} 条,另有 ${failed.length} 条因状态限制未执行`)
} else {
message.success(`已批量更新 ${updatedIds.length} 条记录`)
}
emit('update:show', false)
emit('updated')
} catch (error) {
message.error(error?.message || '批量操作失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.aksrt-shipping-batch__body {
padding-top: 8px;
}
.aksrt-shipping-batch__summary {
margin-bottom: 12px;
color: #475569;
font-size: 13px;
}
</style>
@@ -1,237 +0,0 @@
<template>
<n-drawer :show="show" :width="640" @update:show="$emit('update:show', $event)">
<n-drawer-content title="物流详情" closable>
<n-spin :show="loading">
<!-- 物流状态头 -->
<div class="aksrt-shipping__status-header" :class="'aksrt-shipping__status-header--' + (detail.record?.status || 'default')">
<n-tag :type="statusType" size="large">{{ statusLabel }}</n-tag>
<span class="aksrt-shipping__order-no">订单号:{{ detail.record?.order_number || '-' }}</span>
</div>
<!-- 物流信息 -->
<n-descriptions bordered :column="2" size="small" label-placement="left" style="margin-top:16px;">
<n-descriptions-item label="物流公司">{{ detail.record?.carrier_label || detail.record?.carrier || '-' }}</n-descriptions-item>
<n-descriptions-item label="物流单号">
<n-text code>{{ detail.record?.tracking_no || '-' }}</n-text>
<n-button v-if="detail.record?.tracking_no" text size="small" @click="copyTrackingNo" style="margin-left:8px;">
<i class="fas fa-copy" />
</n-button>
</n-descriptions-item>
<n-descriptions-item label="发货时间">{{ detail.record?.shipped_at || '-' }}</n-descriptions-item>
<n-descriptions-item label="处理人">{{ detail.record?.operator?.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="发货类型">
<n-tag size="small" :type="detail.record?.ship_type === 'express' ? 'info' : 'default'">
{{ detail.record?.ship_type_label || '快递发货' }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item label="备注">{{ detail.record?.note || '-' }}</n-descriptions-item>
</n-descriptions>
<!-- 收货地址 -->
<n-card v-if="detail.record?.order_context?.address" size="small" title="收货地址" :bordered="false" style="margin-top:16px;">
<n-descriptions :column="1" size="small" label-placement="left">
<n-descriptions-item label="收货人">{{ detail.record.order_context.address.name || '-' }}</n-descriptions-item>
<n-descriptions-item label="电话">{{ detail.record.order_context.address.phone || '-' }}</n-descriptions-item>
<n-descriptions-item label="地址">{{ detail.record.order_context.address.full_address || detail.record.order_context.address.full_text || '-' }}</n-descriptions-item>
</n-descriptions>
<n-button size="small" @click="copyAddress" style="margin-top:8px;">
<i class="fas fa-copy" style="margin-right:4px;" /> 复制地址
</n-button>
</n-card>
<!-- 物流追踪 -->
<n-card v-if="detail.tracking_detail?.length" size="small" title="物流追踪" :bordered="false" style="margin-top:16px;">
<n-timeline>
<n-timeline-item
v-for="(item, i) in detail.tracking_detail"
:key="i"
:title="item.description || item.status"
:content="item.time"
:type="i === 0 ? 'success' : 'default'"
/>
</n-timeline>
</n-card>
<!-- 状态更新 -->
<n-card size="small" title="状态更新" :bordered="false" style="margin-top:16px;">
<n-space vertical>
<n-input v-model:value="note" type="textarea" :rows="2" placeholder="状态变更备注" />
<n-space>
<n-select v-model:value="nextStatus" :options="actionOptions" placeholder="选择状态" style="width:160px;" />
<n-button type="primary" :disabled="!nextStatus" :loading="updating" @click="handleStatusUpdate">更新状态</n-button>
</n-space>
</n-space>
</n-card>
<!-- 操作时间线 -->
<div v-if="detail.timeline?.length" class="aksrt-shipping__timeline">
<div class="aksrt-shipping__section-title">操作时间线</div>
<n-timeline size="small">
<n-timeline-item
v-for="(item, index) in detail.timeline"
:key="index"
:time="item.time"
:title="item.label"
:content="item.detail || ''"
/>
</n-timeline>
</div>
</n-spin>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api/index.js'
const props = defineProps({
show: { type: Boolean, default: false },
shippingId: { type: [Number, String], default: null },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const loading = ref(false)
const updating = ref(false)
const detail = ref({ record: null, timeline: [], tracking_detail: [] })
const nextStatus = ref(null)
const note = ref('')
const statusMap = {
pending: { label: '待发货', type: 'warning' },
shipped: { label: '已发货', type: 'info' },
in_transit: { label: '运输中', type: 'info' },
delivered: { label: '已签收', type: 'success' },
returned: { label: '已退回', type: 'warning' },
lost: { label: '已丢失', type: 'error' },
}
const statusLabel = computed(() => {
const s = detail.value?.record?.status
return (statusMap[s] || { label: s || '-' }).label
})
const statusType = computed(() => {
const s = detail.value?.record?.status
return (statusMap[s] || { type: 'default' }).type
})
const actionOptions = computed(() => {
const status = detail.value?.record?.status
if (status === 'shipped' || status === 'pending') {
return [
{ label: '运输中', value: 'in_transit' },
{ label: '已签收', value: 'delivered' },
{ label: '已退回', value: 'returned' },
{ label: '已丢失', value: 'lost' },
]
}
if (status === 'in_transit') {
return [
{ label: '已签收', value: 'delivered' },
{ label: '已退回', value: 'returned' },
{ label: '已丢失', value: 'lost' },
]
}
return []
})
watch(() => props.show, async (visible) => {
if (visible && props.shippingId) {
await loadDetail()
} else if (!visible) {
detail.value = { record: null, timeline: [], tracking_detail: [] }
nextStatus.value = null
note.value = ''
}
})
async function loadDetail() {
loading.value = true
try {
const res = await api.request(`/shop/shipping/${props.shippingId}`)
detail.value = res?.data || { record: null, timeline: [], tracking_detail: [] }
} catch (error) {
message.error(error?.message || '加载物流详情失败')
} finally {
loading.value = false
}
}
async function handleStatusUpdate() {
if (!nextStatus.value) return
updating.value = true
try {
await api.request(`/shop/shipping/${props.shippingId}/update-status`, {
method: 'POST',
data: { status: nextStatus.value, note: note.value },
})
message.success('物流状态已更新')
nextStatus.value = null
note.value = ''
await loadDetail()
emit('updated')
} catch (error) {
message.error(error?.message || '状态更新失败')
} finally {
updating.value = false
}
}
async function copyTrackingNo() {
const text = detail.value?.record?.tracking_no || ''
if (!text) return
try {
await navigator.clipboard.writeText(text)
message.success('物流单号已复制')
} catch {
message.warning('复制失败')
}
}
async function copyAddress() {
const addr = detail.value?.record?.order_context?.address
const text = addr?.full_address || addr?.full_text || ''
if (!text) return
try {
await navigator.clipboard.writeText(text)
message.success('地址已复制')
} catch {
message.warning('复制失败')
}
}
</script>
<style scoped>
.aksrt-shipping__status-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 8px;
background: var(--surface-2);
}
.aksrt-shipping__status-header--shipped { background: #e6f7ff; }
.aksrt-shipping__status-header--in_transit { background: #e6f7ff; }
.aksrt-shipping__status-header--delivered { background: #f6ffed; }
.aksrt-shipping__status-header--returned { background: #fff7e6; }
.aksrt-shipping__status-header--lost { background: #fff1f0; }
.aksrt-shipping__order-no {
font-size: 13px;
color: var(--color-ink-3);
}
.aksrt-shipping__timeline {
margin-top: 18px;
}
.aksrt-shipping__section-title {
margin-bottom: 8px;
font-weight: 600;
color: var(--color-ink);
}
</style>
@@ -1,68 +0,0 @@
<template>
<n-modal :show="show" @update:show="$emit('update:show', $event)" preset="card" :title="modalTitle" style="width:480px;" closable>
<n-form label-placement="top">
<n-form-item label="申请人">
<n-input :value="withdrawal.user_name || '-'" disabled />
</n-form-item>
<n-form-item label="提现金额">
<n-input :value="'¥' + (withdrawal.amount || 0).toFixed(2)" disabled />
</n-form-item>
<n-form-item label="提现方式">
<n-input :value="withdrawal.method || '-'" disabled />
</n-form-item>
<n-form-item label="收款账号">
<n-input :value="withdrawal.account || withdrawal.account_info || '-'" disabled />
</n-form-item>
<n-form-item label="审核备注">
<n-input v-model:value="note" type="textarea" :rows="3" placeholder="填写审核说明(可选)" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="$emit('update:show', false)">取消</n-button>
<n-button type="error" :loading="loading" @click="handleAction('rejected')">拒绝</n-button>
<n-button type="success" :loading="loading" @click="handleAction('approved')">批准</n-button>
<n-button type="info" :loading="loading" @click="handleAction('completed')">打款</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api/index.js'
const props = defineProps({
show: { type: Boolean, default: false },
withdrawal: { type: Object, default: () => ({}) },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const loading = ref(false)
const note = ref('')
const modalTitle = computed(() => {
const id = props.withdrawal?.id
return id ? `审核提现 #${id}` : '审核提现'
})
async function handleAction(status) {
loading.value = true
try {
await api.request(`/payment/withdrawals/${props.withdrawal.id}`, {
method: 'POST',
body: JSON.stringify({ status, note: note.value }),
})
message.success('操作成功')
emit('update:show', false)
emit('updated')
} catch {
message.error('操作失败')
} finally {
loading.value = false
}
}
</script>
@@ -1,2 +0,0 @@
// Shop ops Vue pages removed (Phase 4). Config uses SchemaPage.
export const shopPageLoaders = {}
+1 -1
View File
@@ -73,7 +73,7 @@ function createSectionRedirectRoutes() {
/**
* Old SPA ops routes → native WP admin pages (Phase 4 transition).
* Bookmarks under #/shop/products etc. land on NativeOpsRedirect.
* Bookmarks under order-related paths land on NativeOpsRedirect.
*/
const NATIVE_OPS_REDIRECTS = [
{ path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' },
@@ -1,49 +0,0 @@
export const basicSchema = {
key: 'shop.basic',
title: '商城基础',
icon: 'fas fa-store',
description: '商城总开关、首页链接、面包屑和 URL 别名设置。',
defaults: {
shop_s: true,
shop_home_url: '',
shop_breadcrumbs_s: true,
shop_breadcrumbs_home: true,
shop_breadcrumbs_shop_home: false,
shop_breadcrumbs_shop_home_name: '商城首页',
shop_product_rewrite_slug: 'product',
shop_cart_rewrite_slug: 'cart',
shop_rewrite_suffix_html_s: true,
},
groups: [
{
key: 'general',
title: '商城开关',
icon: 'fas fa-power-off',
fields: [
{ key: 'shop_s', label: '启用商城', type: 'switch', default: true },
{ key: 'shop_home_url', label: '商城首页链接', type: 'input', default: '', help: '留空使用默认商城页面', dependsOn: { key: 'shop_s', value: true } },
],
},
{
key: 'breadcrumbs',
title: '面包屑',
icon: 'fas fa-angle-double-right',
fields: [
{ key: 'shop_breadcrumbs_s', label: '显示面包屑', type: 'switch', default: true, dependsOn: { key: 'shop_s', value: true } },
{ key: 'shop_breadcrumbs_home', label: '显示首页链接', type: 'switch', default: true, dependsOn: { key: 'shop_breadcrumbs_s', value: true } },
{ key: 'shop_breadcrumbs_shop_home', label: '显示商城首页', type: 'switch', default: false, dependsOn: { key: 'shop_breadcrumbs_s', value: true } },
{ key: 'shop_breadcrumbs_shop_home_name', label: '商城首页名称', type: 'input', default: '商城首页', dependsOn: { key: 'shop_breadcrumbs_s', value: true } },
],
},
{
key: 'rewrite',
title: 'URL 别名',
icon: 'fas fa-link',
fields: [
{ key: 'shop_product_rewrite_slug', label: '商品 URL 别名', type: 'input', default: 'product', dependsOn: { key: 'shop_s', value: true } },
{ key: 'shop_cart_rewrite_slug', label: '购物车 URL 别名', type: 'input', default: 'cart', dependsOn: { key: 'shop_s', value: true } },
{ key: 'shop_rewrite_suffix_html_s', label: '.html 伪静态后缀', type: 'switch', default: true, dependsOn: { key: 'shop_s', value: true } },
],
},
],
}
@@ -1,32 +0,0 @@
export const contactSchema = {
key: 'shop.contact',
title: '客服配置',
icon: 'fas fa-headset',
description: '商品详情页客服按钮和联系方式配置。',
defaults: {
shop_contact_msg_s: true,
shop_contact_name: '联系客服',
shop_contact_note: '',
shop_contact_more: '',
},
groups: [
{
key: 'button',
title: '客服按钮',
icon: 'fas fa-comment-dots',
fields: [
{ key: 'shop_contact_msg_s', label: '显示客服按钮', type: 'switch', default: true },
{ key: 'shop_contact_name', label: '按钮文案', type: 'input', default: '联系客服', dependsOn: { key: 'shop_contact_msg_s', value: true } },
{ key: 'shop_contact_note', label: '客服备注', type: 'textarea', default: '', dependsOn: { key: 'shop_contact_msg_s', value: true } },
],
},
{
key: 'channels',
title: '联系方式',
icon: 'fas fa-address-book',
fields: [
{ key: 'shop_contact_more', label: '多客服联系方式', type: 'textarea', default: '', help: '每行一个:名称|图标|链接|二维码图片 URL' },
],
},
],
}
@@ -1,21 +0,0 @@
/**
* Shop & Product domain schemas.
*/
import { basicSchema } from './basic.js'
import { listSchema } from './list.js'
import { contactSchema } from './contact.js'
import { singleSchema } from './single.js'
import { productDefaultsSchema } from './product-defaults.js'
import { shippingConfigSchema } from './shipping-config.js'
import { timingSchema } from './timing.js'
export const shopSchemas = {
basic: basicSchema,
list: listSchema,
contact: contactSchema,
single: singleSchema,
productDefaults: productDefaultsSchema,
shippingConfig: shippingConfigSchema,
timing: timingSchema,
}
@@ -1,40 +0,0 @@
export const listSchema = {
key: 'shop.list',
title: '商品列表',
icon: 'fas fa-th-list',
description: '商品列表排序、分页、卡片样式和销量显示配置。',
defaults: {
shop_list_orderby: 'date',
shop_list_count: 12,
shop_list_paging: 'number',
shop_list_ias_s: false,
shop_list_ias_max: 5,
shop_list_style: 'default',
shop_sales_show: 'show',
shop_sales_show_min: 10,
},
groups: [
{
key: 'pagination',
title: '列表分页',
icon: 'fas fa-layer-group',
fields: [
{ key: 'shop_list_orderby', label: '默认排序', type: 'select', default: 'date', options: [{ label: '最新发布', value: 'date' }, { label: '销量最高', value: 'sales' }, { label: '价格最低', value: 'price_asc' }, { label: '价格最高', value: 'price_desc' }, { label: '评价最多', value: 'comments' }] },
{ key: 'shop_list_count', label: '每页数量', type: 'number', default: 12, min: 4, max: 100 },
{ key: 'shop_list_paging', label: '翻页模式', type: 'radio', default: 'number', options: [{ label: '数字翻页', value: 'number' }, { label: 'AJAX 加载', value: 'ajax' }] },
{ key: 'shop_list_ias_s', label: '滚动自动加载', type: 'switch', default: false, dependsOn: { key: 'shop_list_paging', value: 'ajax' } },
{ key: 'shop_list_ias_max', label: '自动加载页数', type: 'number', default: 5, min: 1, max: 50, dependsOn: { key: 'shop_list_ias_s', value: true } },
],
},
{
key: 'style',
title: '列表样式',
icon: 'fas fa-palette',
fields: [
{ key: 'shop_list_style', label: '卡片样式', type: 'select', default: 'default', options: [{ label: '默认', value: 'default' }, { label: '竖向大卡片', value: 'card-lg' }, { label: '横向小卡片', value: 'card-sm' }] },
{ key: 'shop_sales_show', label: '销量显示', type: 'select', default: 'show', options: [{ label: '显示', value: 'show' }, { label: '不显示', value: 'hide' }, { label: '超量显示', value: 'threshold' }] },
{ key: 'shop_sales_show_min', label: '起显阈值', type: 'number', default: 10, min: 0, max: 99999, help: '销量超过此数字才显示', dependsOn: { key: 'shop_sales_show', value: 'threshold' } },
],
},
],
}
@@ -1,54 +0,0 @@
export const productDefaultsSchema = {
key: 'shop.product-defaults',
title: '商品默认参数',
icon: 'fas fa-sliders-h',
description: '主题级商品参数默认值,作为「商品→分类→主题设置」参数继承链的根。',
defaults: {
shop_limit_buy: -1,
shop_shipping_fee: 'free',
shop_guest_buy: false,
shop_email_fill: false,
shop_rebate: true,
shop_after_sale: 'refund_return',
shop_service: '',
shop_product_params_default: '',
},
groups: [
{
key: 'policy',
title: '购买策略',
icon: 'fas fa-shopping-bag',
fields: [
{ key: 'shop_limit_buy', label: '默认限购', type: 'number', default: -1, min: -1, max: 9999, help: '-1 不限,0 禁购,正数为限购数量' },
{ key: 'shop_guest_buy', label: '免登录购买', type: 'switch', default: false },
{ key: 'shop_email_fill', label: '邮箱必留', type: 'switch', default: false },
{ key: 'shop_rebate', label: '允许返佣', type: 'switch', default: true },
],
},
{
key: 'shipping',
title: '运费默认',
icon: 'fas fa-truck',
fields: [
{ key: 'shop_shipping_fee', label: '默认运费类型', type: 'select', default: 'free', options: [{ label: '包邮', value: 'free' }, { label: '固定运费', value: 'fixed' }, { label: '按金额计算', value: 'amount' }] },
],
},
{
key: 'afterSale',
title: '售后与服务',
icon: 'fas fa-undo',
fields: [
{ key: 'shop_after_sale', label: '默认售后政策', type: 'select', default: 'refund_return', options: [{ label: '仅退款', value: 'refund' }, { label: '退货退款', value: 'refund_return' }, { label: '不支持售后', value: 'none' }] },
{ key: 'shop_service', label: '服务保障', type: 'textarea', default: '', help: '每行一项:名称|说明|图标图片 URL' },
],
},
{
key: 'params',
title: '默认参数模板',
icon: 'fas fa-list-alt',
fields: [
{ key: 'shop_product_params_default', label: '商品参数模板', type: 'textarea', default: '', help: '每行一个参数:参数名|参数值,新建商品自动带入' },
],
},
],
}
@@ -1,52 +0,0 @@
export const shippingConfigSchema = {
key: 'shop.shipping-config',
title: '物流配置',
icon: 'fas fa-shipping-fast',
description: '快递查询接口、物流公司列表和收货地址设置。',
defaults: {
shop_manual_address: false,
shop_express_api_sdk: 'kuaidi100',
shop_express_query_interval: 60,
shop_express_kdniao_opt: '',
shop_express_kuaidi100_opt: '',
shop_express_aliyun_opt: '',
shop_shipping_company: '',
},
groups: [
{
key: 'address',
title: '收货地址',
icon: 'fas fa-map-marker-alt',
fields: [
{ key: 'shop_manual_address', label: '手动填写收货地址', type: 'switch', default: false, help: '开启后用户可手动输入地址而非选择地址簿' },
],
},
{
key: 'express',
title: '快递查询',
icon: 'fas fa-search',
fields: [
{ key: 'shop_express_api_sdk', label: '快递查询接口', type: 'select', default: 'kuaidi100', options: [{ label: '快递100', value: 'kuaidi100' }, { label: '阿里云', value: 'aliyun' }, { label: '快递鸟', value: 'kdniao' }] },
{ key: 'shop_express_query_interval', label: '查询刷新间隔(分钟)', type: 'number', default: 60, min: 30, max: 240 },
],
},
{
key: 'credentials',
title: '接口凭据',
icon: 'fas fa-key',
fields: [
{ key: 'shop_express_kdniao_opt', label: '快递鸟凭据', type: 'textarea', default: '', help: 'AppID|APIKey', dependsOn: { key: 'shop_express_api_sdk', value: 'kdniao' } },
{ key: 'shop_express_kuaidi100_opt', label: '快递100凭据', type: 'textarea', default: '', help: 'Key|Customer', dependsOn: { key: 'shop_express_api_sdk', value: 'kuaidi100' } },
{ key: 'shop_express_aliyun_opt', label: '阿里云凭据', type: 'textarea', default: '', help: 'AppCode', dependsOn: { key: 'shop_express_api_sdk', value: 'aliyun' } },
],
},
{
key: 'companies',
title: '物流公司',
icon: 'fas fa-building',
fields: [
{ key: 'shop_shipping_company', label: '物流公司列表', type: 'textarea', default: '', help: '每行一个:编码|名称,如 SF|顺丰速运' },
],
},
],
}
@@ -1,45 +0,0 @@
export const singleSchema = {
key: 'shop.single',
title: '商品详情',
icon: 'fas fa-box-open',
description: '商品详情页布局、相关推荐和底部内容设置。',
defaults: {
shop_content_layout: 'adapt',
shop_content_show_bg: false,
shop_single_tab: '',
shop_content_after: '',
shop_single_related_s: true,
shop_single_related_opt: '',
shop_main_image_default: '',
},
groups: [
{
key: 'layout',
title: '详情页布局',
icon: 'fas fa-columns',
fields: [
{ key: 'shop_content_layout', label: '内容布局', type: 'select', default: 'adapt', options: [{ label: '适应宽度', value: 'adapt' }, { label: '全屏', value: 'full' }, { label: '适应宽度+侧栏', value: 'sidebar' }] },
{ key: 'shop_content_show_bg', label: '详情背景盒子', type: 'switch', default: false },
{ key: 'shop_single_tab', label: '自定义 TAB 栏目', type: 'textarea', default: '', help: '每行一个 TAB:标题|内容 HTML' },
{ key: 'shop_content_after', label: '详情页底部内容', type: 'textarea', default: '', help: '追加在每个商品详情页底部的 HTML 内容' },
],
},
{
key: 'related',
title: '相关推荐',
icon: 'fas fa-th',
fields: [
{ key: 'shop_single_related_s', label: '显示相关推荐', type: 'switch', default: true },
{ key: 'shop_single_related_opt', label: '推荐配置', type: 'textarea', default: '', help: '标题|关联类型(cat/discount/tag)|排序|数量|翻页|样式', dependsOn: { key: 'shop_single_related_s', value: true } },
],
},
{
key: 'image',
title: '商品图片',
icon: 'fas fa-image',
fields: [
{ key: 'shop_main_image_default', label: '默认商品主图', type: 'media', default: '' },
],
},
],
}
@@ -1,54 +0,0 @@
export const timingSchema = {
key: 'shop.timing',
title: '时效与售后原因',
icon: 'fas fa-clock',
description: '确认收货、评价、退货时效和售后原因配置。',
defaults: {
shop_order_receipt_max_day: 15,
shop_comment_max_day: 7,
shop_comment_placeholder: '',
shop_comment_img_num: 3,
shop_after_sale_return_express_max_day: 7,
shop_after_sale_reason_refund: '',
shop_after_sale_reason_return: '',
shop_author_show: true,
},
groups: [
{
key: 'receipt',
title: '确认收货',
icon: 'fas fa-check-circle',
fields: [
{ key: 'shop_order_receipt_max_day', label: '自动确认收货(天)', type: 'number', default: 15, min: 1, max: 60, help: '发货后超过此天数自动确认收货' },
],
},
{
key: 'comment',
title: '评价时效',
icon: 'fas fa-star',
fields: [
{ key: 'shop_comment_max_day', label: '评价时效(天)', type: 'number', default: 7, min: 1, max: 60, help: '确认收货后超过此天数自动好评' },
{ key: 'shop_comment_placeholder', label: '评价占位文案', type: 'input', default: '' },
{ key: 'shop_comment_img_num', label: '评价图片数', type: 'number', default: 3, min: 0, max: 9 },
],
},
{
key: 'afterSale',
title: '售后退货',
icon: 'fas fa-undo-alt',
fields: [
{ key: 'shop_after_sale_return_express_max_day', label: '退货快递时效(天)', type: 'number', default: 7, min: 1, max: 30 },
{ key: 'shop_after_sale_reason_refund', label: '仅退款原因', type: 'textarea', default: '', help: '每行一个可选原因' },
{ key: 'shop_after_sale_reason_return', label: '退货退款原因', type: 'textarea', default: '', help: '每行一个可选原因' },
],
},
{
key: 'author',
title: '商家信息',
icon: 'fas fa-user-tie',
fields: [
{ key: 'shop_author_show', label: '显示商家信息', type: 'switch', default: true },
],
},
],
}
-755
View File
@@ -1,755 +0,0 @@
/**
* AKSRT Product Archive — 商品归档页样式
*
* 商品分类页、标签页共用样式。
* 卡片网格、价格徽章、空状态。
*
* Tokens: 颜色 / 间距 / 圆角 / 阴影 全部来自 core/tokens.css
*/
/* ================================================
* 页面头:标题 + 计数
* ================================================ */
.aksrt-product-archive {
padding-top: var(--space-6);
padding-bottom: var(--space-9);
}
.aksrt-product-archive__header {
margin-bottom: var(--space-6);
padding: var(--space-5);
background: var(--surface-1, #fff);
border-radius: var(--radius-lg, 8px);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.aksrt-product-archive__title {
margin: 0 0 4px;
font-size: 22px;
font-weight: 700;
color: var(--color-ink);
display: flex;
align-items: center;
gap: 8px;
}
.aksrt-product-archive__title i {
color: var(--color-primary, var(--color-primary));
}
.aksrt-product-archive__desc {
margin: 0 0 8px;
font-size: 14px;
color: var(--color-ink-3);
line-height: 1.6;
}
.aksrt-product-archive__count {
margin: 0;
font-size: 13px;
color: var(--color-ink-4, #8A8F98);
}
.aksrt-product-archive__eyebrow {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 12px;
margin-bottom: 10px;
border-radius: 999px;
background: var(--surface-2, #f4f5f7);
color: var(--color-ink-3);
font-size: 12px;
font-weight: 600;
}
.aksrt-product-archive__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: var(--space-5);
margin-bottom: var(--space-8);
}
/* ================================================
* 商品卡片
* ================================================ */
.aksrt-product-card {
display: flex;
flex-direction: column;
background: var(--surface-1, #fff);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg, 8px);
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.aksrt-product-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
}
.aksrt-product-card__link {
display: flex;
flex-direction: column;
flex: 1;
text-decoration: none;
color: inherit;
}
/* 商品图片 */
.aksrt-product-card__image {
position: relative;
aspect-ratio: 4 / 3;
background-color: var(--surface-2, #F4F5F7);
overflow: hidden;
}
.aksrt-product-card__image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.aksrt-product-card:hover .aksrt-product-card__image img {
transform: scale(1.02);
}
.aksrt-product-card__image--placeholder {
display: flex;
align-items: center;
justify-content: center;
color: var(--color-ink-4, #8A8F98);
font-size: 32px;
}
/* 卡片正文 */
.aksrt-product-card__body {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-4);
flex: 1;
}
.aksrt-product-card__title {
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.4;
color: var(--color-ink);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.aksrt-product-card__excerpt {
margin: 0;
font-size: 13px;
color: var(--color-ink-3);
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 底部:价格 + 销量 */
.aksrt-product-card__footer {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-top: auto;
padding-top: var(--space-3);
border-top: 1px solid var(--color-line-soft, #F0F1F3);
}
.aksrt-product-card__price {
font-size: 18px;
font-weight: 700;
color: var(--color-error, #EF4444);
}
.aksrt-product-card__sales {
font-size: 12px;
color: var(--color-ink-4, #8A8F98);
}
/* ================================================
* 分页
* ================================================ */
.aksrt-product-archive__pagination {
display: flex;
justify-content: center;
padding: var(--space-5) 0;
}
.aksrt-product-archive__pagination .page-numbers {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
padding: 0 8px;
margin: 0 4px;
border: 1px solid var(--color-line);
border-radius: var(--radius, 10px);
background: var(--surface-1, #fff);
color: var(--color-ink-2);
font-size: 14px;
text-decoration: none;
transition: all 0.15s ease;
}
.aksrt-product-archive__pagination .page-numbers:hover {
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary, var(--color-primary));
}
.aksrt-product-archive__pagination .page-numbers.current {
background: var(--color-primary, var(--color-primary));
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: #fff;
}
/* ================================================
* 空状态
* ================================================ */
.aksrt-product-archive__empty {
text-align: center;
padding: var(--space-9) var(--space-5);
color: var(--color-ink-3);
}
.aksrt-product-archive__empty i {
font-size: 48px;
color: var(--color-ink-4, #8A8F98);
margin-bottom: var(--space-4);
display: block;
}
.aksrt-product-archive__empty p {
margin: 0;
font-size: 15px;
}
/* ================================================
* 响应式
* ================================================ */
@media (max-width: 640px) {
.aksrt-product-archive__grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
.aksrt-product-card__body {
padding: var(--space-3);
}
.aksrt-product-card__title {
font-size: 14px;
}
.aksrt-product-card__price {
font-size: 16px;
}
}
@media (max-width: 380px) {
.aksrt-product-archive__grid {
grid-template-columns: 1fr;
}
}
/* ================================================
* 产品详情页样式补充
* ================================================ */
.aksrt-product {
padding-top: var(--space-6);
padding-bottom: var(--space-9);
}
.aksrt-product__layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-7);
margin-bottom: var(--space-7);
}
.aksrt-product__image {
border-radius: var(--radius-lg, 8px);
overflow: hidden;
background: var(--surface-2, #F4F5F7);
}
.aksrt-product__img {
width: 100%;
display: block;
}
.aksrt-product__img-placeholder {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-ink-4, #8A8F98);
font-size: 64px;
}
.aksrt-product__info {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.aksrt-product__title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: var(--color-ink);
line-height: 1.3;
}
.aksrt-product__price-row {
display: flex;
align-items: baseline;
gap: var(--space-3);
}
.aksrt-product__price {
font-size: 28px;
font-weight: 700;
color: var(--color-error, #EF4444);
}
.aksrt-product__sales {
font-size: 13px;
color: var(--color-ink-4, #8A8F98);
}
.aksrt-product__excerpt {
margin: 0;
font-size: 14px;
color: var(--color-ink-3);
line-height: 1.6;
}
.aksrt-product__meta {
font-size: 14px;
color: var(--color-ink-2);
}
.aksrt-product__meta i {
margin-right: 4px;
}
.aksrt-product__actions {
margin-top: var(--space-4);
}
.aksrt-product__buy-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 32px;
border: none;
border-radius: var(--radius, 10px);
background: var(--color-primary, var(--color-primary));
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
}
.aksrt-product__buy-btn:hover {
opacity: 0.85;
}
.aksrt-product__detail {
padding: var(--space-6);
background: var(--surface-1, #fff);
border-radius: var(--radius-lg, 8px);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.aksrt-product__detail-title {
margin: 0 0 var(--space-4);
font-size: 18px;
font-weight: 600;
color: var(--color-ink);
}
.aksrt-product__detail-content {
font-size: 15px;
line-height: 1.8;
color: var(--color-ink-2);
}
.aksrt-product__detail-content img {
max-width: 100%;
border-radius: var(--radius, 10px);
}
.aksrt-product__empty {
text-align: center;
padding: var(--space-10) var(--space-5);
color: var(--color-ink-3);
}
.aksrt-product__empty i {
display: block;
margin-bottom: var(--space-4);
}
.aksrt-product__empty p {
margin: 0;
font-size: 16px;
}
/* 产品详情页响应式 */
@media (max-width: 767px) {
.aksrt-product__layout {
grid-template-columns: 1fr;
gap: var(--space-5);
}
.aksrt-product__title {
font-size: 20px;
}
.aksrt-product__price {
font-size: 24px;
}
}
/* ================================================
* 购物车页样式
* ================================================ */
.aksrt-cart {
padding-top: var(--space-6);
padding-bottom: var(--space-9);
}
.aksrt-cart__title {
margin: 0 0 var(--space-6);
font-size: 24px;
font-weight: 700;
color: var(--color-ink);
}
.aksrt-cart__layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: var(--space-6);
}
.aksrt-cart__items {
min-height: 200px;
}
.aksrt-cart__item {
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-4);
background: var(--surface-1, #fff);
border: 1px solid var(--color-line);
border-radius: var(--radius, 10px);
margin-bottom: var(--space-3);
}
.aksrt-cart__item-img {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: var(--radius-sm, 6px);
}
.aksrt-cart__item-info {
flex: 1;
min-width: 0;
}
.aksrt-cart__item-info h4 {
margin: 0 0 4px;
font-size: 15px;
font-weight: 500;
color: var(--color-ink);
}
.aksrt-cart__item-price {
font-size: 14px;
color: var(--color-ink-3);
}
.aksrt-cart__item-qty {
display: flex;
align-items: center;
gap: var(--space-2);
}
.aksrt-cart__qty-btn {
width: 28px;
height: 28px;
border: 1px solid var(--color-line);
border-radius: var(--radius-sm, 6px);
background: var(--surface-1, #fff);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.aksrt-cart__qty-btn:hover {
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
}
.aksrt-cart__item-subtotal {
font-weight: 600;
color: var(--color-error, #EF4444);
min-width: 80px;
text-align: right;
}
.aksrt-cart__remove-btn {
width: 32px;
height: 32px;
border: none;
background: none;
cursor: pointer;
color: var(--color-ink-4, #8A8F98);
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm, 6px);
}
.aksrt-cart__remove-btn:hover {
background: var(--surface-2, #F4F5F7);
color: var(--color-error, #EF4444);
}
.aksrt-cart__sidebar {
position: sticky;
top: 24px;
}
.aksrt-cart__summary {
padding: var(--space-5);
background: var(--surface-1, #fff);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg, 8px);
}
.aksrt-cart__summary-title {
margin: 0 0 var(--space-4);
font-size: 16px;
font-weight: 600;
color: var(--color-ink);
}
.aksrt-cart__summary-row {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-3);
font-size: 14px;
color: var(--color-ink-2);
}
.aksrt-cart__summary-total {
display: flex;
justify-content: space-between;
padding-top: var(--space-3);
border-top: 1px solid var(--color-line);
margin-bottom: var(--space-5);
font-weight: 600;
}
.aksrt-cart__total-price {
font-size: 20px;
color: var(--color-error, #EF4444);
}
.aksrt-cart__checkout-btn {
width: 100%;
padding: 12px;
border: none;
border-radius: var(--radius, 10px);
background: var(--color-primary, var(--color-primary));
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
}
.aksrt-cart__checkout-btn:hover {
opacity: 0.85;
}
.aksrt-cart__checkout-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.aksrt-cart__empty {
text-align: center;
padding: var(--space-9) var(--space-5);
color: var(--color-ink-3);
}
.aksrt-cart__empty i {
display: block;
margin-bottom: var(--space-4);
}
.aksrt-cart__empty p {
margin: 0 0 var(--space-4);
font-size: 15px;
}
/* 购物车响应式 */
@media (max-width: 767px) {
.aksrt-cart__layout {
grid-template-columns: 1fr;
}
.aksrt-cart__sidebar {
position: static;
}
}
/* ================================================
* 商品编辑页样式
* ================================================ */
.aksrt-product-edit {
padding-top: var(--space-6);
padding-bottom: var(--space-9);
max-width: 720px;
margin: 0 auto;
}
.aksrt-product-edit__title {
margin: 0 0 var(--space-6);
font-size: 24px;
font-weight: 700;
color: var(--color-ink);
}
.aksrt-product-edit__form {
padding: var(--space-6);
background: var(--surface-1, #fff);
border-radius: var(--radius-lg, 8px);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.aksrt-product-edit__field {
margin-bottom: var(--space-5);
}
.aksrt-product-edit__field label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--color-ink);
margin-bottom: var(--space-2);
}
.aksrt-product-edit__field input,
.aksrt-product-edit__field textarea {
width: 100%;
padding: 10px 14px;
font-size: 14px;
color: var(--color-ink);
background: var(--surface-1, #fff);
border: 1px solid var(--color-line);
border-radius: var(--radius, 10px);
transition: border-color 0.15s;
}
.aksrt-product-edit__field input:focus,
.aksrt-product-edit__field textarea:focus {
outline: none;
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
.aksrt-product-edit__field textarea {
resize: vertical;
min-height: 100px;
}
.aksrt-product-edit__actions {
display: flex;
gap: var(--space-3);
margin-top: var(--space-6);
}
.aksrt-product-edit__actions .btn-primary {
padding: 10px 24px;
border: none;
border-radius: var(--radius, 10px);
background: var(--color-primary, var(--color-primary));
color: #fff;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.15s;
}
.aksrt-product-edit__actions .btn-primary:hover {
opacity: 0.85;
}
.aksrt-product-edit__actions .btn-ghost {
padding: 10px 24px;
border: 1px solid var(--color-line);
border-radius: var(--radius, 10px);
background: var(--surface-1, #fff);
color: var(--color-ink-2);
font-size: 15px;
text-decoration: none;
transition: border-color 0.15s;
}
.aksrt-product-edit__actions .btn-ghost:hover {
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
}
.aksrt-product-archive__toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px}
.aksrt-product-archive__sort{display:inline-flex;padding:8px 14px;border-radius:999px;background:var(--surface-1,#fff);color:var(--color-ink-3,#5c6370);text-decoration:none;font-weight:600;font-size:14px;box-shadow:var(--shadow-sm)}
.aksrt-product-archive__sort.is-active,.aksrt-product-archive__sort:hover{background:var(--color-primary,#f04494);color:#fff}
.aksrt-product-archive__load-more{display:flex;justify-content:center;margin:20px 0}
.aksrt-revenue-page__layout{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin:20px 0}
.aksrt-revenue-page__panel{padding:18px;border-radius:16px;background:var(--surface-1,#fff);box-shadow:var(--shadow-sm)}
.aksrt-revenue-page__withdraw-form label{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}
.aksrt-revenue-page__withdraw-form input,.aksrt-revenue-page__withdraw-form select,.aksrt-revenue-page__withdraw-form textarea{width:100%;padding:10px 12px;border:1px solid var(--color-line-soft,rgba(0,0,0,.08));border-radius:10px}
.aksrt-revenue-page__item{display:flex;justify-content:space-between;gap:12px;padding:12px 0;border-bottom:1px solid var(--color-line-soft,rgba(0,0,0,.06))}
.aksrt-revenue-page__item .is-in{color:#27ae60;font-weight:700}
.aksrt-revenue-page__item .is-out{color:#e74c3c;font-weight:700}
.aksrt-product-editor__hint{font-size:13px;color:var(--color-ink-4,#8a909c);margin:0 0 10px}
/* ===== zibll-parity cat filter / skeleton ===== */
.aksrt-shop-cat-filter.shop-cat-filter{margin-top:14px}
.shop-cat-filter-tab{display:flex;gap:10px;overflow-x:auto;list-style:none;margin:0 0 10px;padding:0 0 4px;-webkit-overflow-scrolling:touch}
.shop-cat-filter-tab li{flex:0 0 auto}
.shop-cat-filter-tab a,
.shop-cat-filter-tab span,
.shop-cat-filter-child a,
.shop-cat-filter-child span{display:inline-flex;align-items:center;padding:6px 12px;border-radius:999px;font-size:13px;font-weight:600;text-decoration:none;color:var(--color-ink-2,#5c6370);background:var(--surface-4,#f3f4f6)}
.shop-cat-filter-tab .active span,
.shop-cat-filter-child .focus-color,
.shop-cat-filter-orderby .focus-color{color:var(--color-primary,#f04494);background:color-mix(in srgb,var(--color-primary,#f04494) 12%, #fff)}
.shop-cat-filter-child{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px}
.shop-cat-filter-orderby{gap:8px}
.shop-cat-filter-orderby .opacity5{opacity:.55;margin-right:4px;font-size:13px}
.aksrt-product-archive__skeleton{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;padding:8px 0 16px}
.aksrt-product-card--placeholder{min-height:220px;border-radius:12px;background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb 37%,#f3f4f6 63%);background-size:400% 100%;animation:aksrtShopSk 1.2s ease infinite}
@keyframes aksrtShopSk{0%{background-position:100% 0}100%{background-position:0 0}}
@media (max-width:992px){.aksrt-product-archive__skeleton{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:560px){.aksrt-product-archive__skeleton{grid-template-columns:1fr}}
-400
View File
@@ -1,400 +0,0 @@
.aksrt-cart-page {
padding: 28px 0 56px;
}
.aksrt-cart-page__header,
.aksrt-cart-page__panel-head,
.aksrt-cart-page__summary-total,
.aksrt-cart-page__coupon-row,
.aksrt-cart-page__item,
.aksrt-cart-page__item-qty,
.aksrt-cart-page__option-columns {
display: flex;
}
.aksrt-cart-page__header,
.aksrt-cart-page__panel-head,
.aksrt-cart-page__summary-total,
.aksrt-cart-page__coupon-row,
.aksrt-cart-page__option-columns {
justify-content: space-between;
gap: 16px;
}
.aksrt-cart-page__layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 24px;
align-items: start;
}
.aksrt-cart-page__title {
margin: 0 0 8px;
font-size: 30px;
line-height: 1.1;
}
.aksrt-cart-page__subtitle,
.aksrt-cart-page__hint,
.aksrt-cart-page__option-body span {
color: var(--color-ink-3, #5d6676);
}
.aksrt-cart-page__pending {
min-width: 280px;
}
.aksrt-cart-page__pending-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.aksrt-cart-page__pending-chip {
border: 1px solid var(--color-line, #dde3eb);
border-radius: 999px;
background: var(--surface-1, #fff);
padding: 8px 12px;
font-size: 12px;
}
.aksrt-cart-page__panel {
background: var(--surface-1, #fff);
border: none;
box-shadow: var(--shadow-sm);
border-radius: var(--radius-2xl, 16px);
padding: 20px;
}
.aksrt-cart-page__main,
.aksrt-cart-page__grid {
display: grid;
gap: 20px;
}
.aksrt-cart-page__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.aksrt-cart-page__items {
display: grid;
gap: 12px;
}
.aksrt-cart-page__item {
align-items: center;
gap: 14px;
padding: 16px;
border: none;
border-radius: var(--main-radius, var(--radius, 8px));
background: var(--surface-4, #f8fafc);
box-shadow: var(--shadow-sm);
}
.aksrt-cart-page__item-image {
width: 84px;
height: 84px;
object-fit: cover;
border-radius: 12px;
}
.aksrt-cart-page__item-image--placeholder {
display: flex;
align-items: center;
justify-content: center;
background: var(--surface-3, #eef2f7);
color: var(--color-ink-4, #8690a0);
}
.aksrt-cart-page__item-main {
flex: 1;
}
.aksrt-cart-page__item-main h3,
.aksrt-cart-page__mini-title,
.aksrt-cart-page__panel h2 {
margin: 0 0 8px;
}
.aksrt-cart-page__item-qty {
align-items: center;
gap: 10px;
}
.aksrt-cart-page__item-qty button,
.aksrt-cart-page__item-remove {
width: 34px;
height: 34px;
border: 1px solid var(--color-line, #dce3ec);
border-radius: 10px;
background: #fff;
}
.aksrt-cart-page__item-total {
min-width: 92px;
text-align: right;
}
.aksrt-cart-page__option-list {
display: grid;
gap: 12px;
}
.aksrt-cart-page__option-card {
display: flex;
gap: 10px;
padding: 14px;
border: 1px solid var(--color-line, #dde3eb);
border-radius: var(--radius-lg);
background: var(--surface-4, #fff);
}
.aksrt-cart-page__option-body {
display: grid;
gap: 4px;
}
.aksrt-cart-page__field {
display: grid;
gap: 8px;
}
.aksrt-cart-page__input,
.aksrt-cart-page__textarea {
width: 100%;
border: 1px solid var(--color-line, #dce3ec);
border-radius: 12px;
padding: 11px 14px;
background: #fff;
}
.aksrt-cart-page__summary-list {
display: grid;
gap: 12px;
margin: 0 0 20px;
}
.aksrt-cart-page__summary-list div,
.aksrt-cart-page__summary-total {
align-items: center;
}
.aksrt-cart-page__summary-list div {
display: flex;
justify-content: space-between;
}
.aksrt-cart-page__summary-total {
padding-top: 16px;
margin-bottom: 18px;
border-top: 1px solid var(--color-line, #e7ebf1);
}
.aksrt-cart-page__summary-total strong {
font-size: 28px;
}
.aksrt-cart-page__submit {
width: 100%;
}
.aksrt-cart-page__empty {
text-align: center;
padding: 44px 20px;
}
.aksrt-cart-page__empty i {
display: block;
margin-bottom: 12px;
font-size: 42px;
color: var(--color-ink-4, #8690a0);
}
@media (max-width: 980px) {
.aksrt-cart-page__layout,
.aksrt-cart-page__grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 767px) {
.aksrt-cart-page__title {
font-size: 22px;
}
.aksrt-cart-page__option-columns {
flex-direction: column;
}
.aksrt-cart-page__field-row {
grid-template-columns: 1fr;
}
.aksrt-cart-page__pending {
min-width: 0;
}
.aksrt-modal__dialog {
width: 95%;
max-height: 90vh;
}
.aksrt-modal__body {
padding: 16px;
}
}
@media (max-width: 480px) {
.aksrt-cart-page__header {
flex-direction: column;
align-items: flex-start;
}
.aksrt-cart-page__summary {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.1);
}
.aksrt-cart-page__submit {
width: 100%;
}
}
/* ── Address Card Actions ── */
.aksrt-cart-page__address-card {
flex-direction: column;
gap: 8px;
}
.aksrt-cart-page__address-label {
display: flex;
gap: 10px;
flex: 1;
cursor: pointer;
}
.aksrt-cart-page__address-actions {
display: flex;
gap: 8px;
padding-left: 28px;
}
.aksrt-cart-page__default-tag {
display: inline-block;
font-size: 11px;
padding: 1px 6px;
border-radius: 4px;
background: var(--color-primary-1, #e6f7ff);
color: var(--color-primary, #1890ff);
font-weight: 500;
vertical-align: middle;
}
.aksrt-cart-page__address-empty {
text-align: center;
padding: 32px 16px;
color: var(--color-ink-4, #8690a0);
}
.aksrt-cart-page__address-empty i {
font-size: 28px;
margin-bottom: 8px;
display: block;
}
.aksrt-cart-page__field-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.aksrt-cart-page__checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
}
/* ── Shared Modal ── */
.aksrt-modal {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.aksrt-modal__overlay {
position: absolute;
inset: 0;
background: rgba(18, 18, 18, 0.48);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.aksrt-modal__dialog {
position: relative;
background: var(--surface-1, #fff);
border: none;
border-radius: var(--radius-2xl, 16px);
width: 90%;
max-width: 520px;
max-height: 85vh;
overflow-y: auto;
box-shadow: var(--shadow-drawer);
}
.aksrt-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 24px;
border-bottom: 1px solid var(--color-line, #e3e8ef);
}
.aksrt-modal__header h3 {
margin: 0;
font-size: 18px;
}
.aksrt-modal__close {
border: none;
background: none;
font-size: 24px;
cursor: pointer;
color: var(--color-ink-3, #5d6676);
padding: 0;
line-height: 1;
}
.aksrt-modal__body {
padding: 20px 24px;
display: grid;
gap: 16px;
}
.aksrt-modal__footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 8px;
}
/* Soft focus ring (#77 recipe) — avoid hard primary stroke */
.aksrt-cart-page__input:focus,
.aksrt-cart-page__textarea:focus,
.aksrt-cart-page__qty-btn:focus-visible {
outline: none;
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
-491
View File
@@ -1,491 +0,0 @@
/**
* AKSRT Product Page — zibll-aligned
*
* 包含:商品详情页 + 前台商品编辑器
*/
.aksrt-product-page,
.aksrt-product-editor {
padding: 28px 0 56px;
}
.aksrt-product-page__layout,
.aksrt-product-page__detail-grid,
.aksrt-product-editor__layout,
.aksrt-product-editor__columns,
.aksrt-product-page__details,
.aksrt-product-page__actions {
display: grid;
gap: 20px;
}
.aksrt-product-page__layout,
.aksrt-product-editor__layout {
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
align-items: start;
}
.aksrt-product-page__detail-grid {
grid-template-columns: minmax(0, 1fr) 320px;
margin-top: 24px;
}
/* ===== Panel surfaces ===== */
.aksrt-product-page__panel,
.aksrt-product-editor__panel {
background: var(--surface-1);
border: none;
box-shadow: var(--shadow-sm);
border-radius: var(--radius-2xl, 16px);
padding: 20px;
}
.aksrt-product-page__title,
.aksrt-product-editor__title {
margin: 0 0 10px;
font-size: 30px;
line-height: 1.1;
font-weight: 700;
color: var(--color-ink);
}
.aksrt-product-page__gallery,
.aksrt-product-editor__main,
.aksrt-product-editor__sidebar,
.aksrt-product-page__summary,
.aksrt-product-page__side-panels {
display: grid;
gap: 18px;
}
/* ===== Gallery ===== */
.aksrt-product-page__primary-image,
.aksrt-product-page__placeholder {
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--surface-2);
}
.aksrt-product-page__primary-image img {
display: block;
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
}
.aksrt-product-page__placeholder {
display: flex;
align-items: center;
justify-content: center;
min-height: 360px;
font-size: 56px;
color: var(--color-ink-4);
}
.aksrt-product-page__thumbs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
gap: 10px;
}
.aksrt-product-page__thumb {
border: 1px solid var(--color-line);
border-radius: var(--radius);
padding: 0;
background: var(--surface-1);
overflow: hidden;
cursor: pointer;
transition: border-color var(--dur-fast) var(--ease);
}
.aksrt-product-page__thumb:hover,
.aksrt-product-page__thumb.is-active {
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
}
.aksrt-product-page__thumb img {
display: block;
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
}
.aksrt-product-page__service-strip {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 6px 0 4px;
}
.aksrt-product-page__service-strip span {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 999px;
background: var(--surface-4);
color: var(--color-ink-3);
font-size: 12px;
font-weight: 500;
}
.aksrt-product-page__price-row,
.aksrt-product-page__quantity,
.aksrt-product-page__term-list,
.aksrt-product-editor__actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.aksrt-product-page__badge,
.aksrt-product-page__term {
display: inline-flex;
align-items: center;
border-radius: var(--radius-pill);
padding: 6px 12px;
background: var(--surface-4);
color: var(--color-ink-2);
font-size: 12px;
font-weight: 500;
}
.aksrt-product-page__badge--promo {
background: rgba(239, 68, 68, 0.12);
color: var(--color-error);
}
.aksrt-product-page__price {
font-size: 30px;
font-weight: 800;
color: var(--color-error);
line-height: 1;
}
.aksrt-product-page__sales {
font-size: 13px;
color: var(--color-ink-4);
}
/* ===== Sticky buybox (zibll-aligned) ===== */
.aksrt-product-page__summary {
position: sticky;
top: calc(var(--nav-height) + 16px);
}
/* ===== Meta info blocks ===== */
.aksrt-product-page__meta,
.aksrt-product-page__info-list {
display: grid;
gap: 12px;
margin: 0;
}
.aksrt-product-page__meta div,
.aksrt-product-page__detail-block {
padding: 14px;
border: 1px solid var(--color-line);
border-radius: var(--radius-lg);
background: var(--surface-4);
}
.aksrt-product-page__meta dt {
font-size: 12px;
color: var(--color-ink-4);
margin-bottom: 4px;
}
.aksrt-product-page__meta dd {
margin: 0;
font-size: 14px;
color: var(--color-ink);
font-weight: 500;
}
/* ===== Form fields ===== */
.aksrt-product-page__field,
.aksrt-product-editor__field {
display: grid;
gap: 8px;
}
.aksrt-product-page__input,
.aksrt-product-editor__input,
.aksrt-product-editor__textarea {
width: 100%;
border: 1px solid var(--color-line);
border-radius: var(--radius);
padding: 11px 14px;
background: var(--surface-1);
font-size: 14px;
color: var(--color-ink);
transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
.aksrt-product-page__input:focus,
.aksrt-product-editor__input:focus,
.aksrt-product-editor__textarea:focus {
outline: none;
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
/* ===== Quantity controls ===== */
.aksrt-product-page__qty-btn {
width: 36px;
height: 36px;
border: 1px solid var(--color-line);
border-radius: var(--radius-sm);
background: var(--surface-1);
color: var(--color-ink-2);
cursor: pointer;
transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.aksrt-product-page__qty-btn:hover {
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
background: var(--color-primary-softer);
}
.aksrt-product-page__qty-input {
width: 90px;
border: 1px solid var(--color-line);
border-radius: var(--radius-sm);
padding: 8px 12px;
text-align: center;
font-size: 14px;
color: var(--color-ink);
}
/* ===== Content ===== */
.aksrt-product-page__content {
line-height: 1.8;
color: var(--color-ink-2);
}
.aksrt-product-page__content img {
max-width: 100%;
border-radius: var(--radius);
}
/* ===== Empty state ===== */
.aksrt-product-page__empty,
.aksrt-product-editor__empty {
text-align: center;
padding: 48px 20px;
}
.aksrt-product-page__empty i,
.aksrt-product-editor__empty i {
display: block;
font-size: 44px;
color: var(--color-ink-4);
margin-bottom: 12px;
}
.aksrt-product-editor__checkbox {
display: flex;
gap: 10px;
align-items: center;
}
.aksrt-product-editor__sidebar {
position: sticky;
top: 24px;
}
/* ===== Responsive ===== */
@media (max-width: 980px) {
.aksrt-product-page__layout,
.aksrt-product-page__detail-grid,
.aksrt-product-editor__layout,
.aksrt-product-editor__columns,
.aksrt-product-page__details {
grid-template-columns: 1fr;
}
.aksrt-product-page__summary {
position: static;
}
.aksrt-product-editor__sidebar {
position: static;
}
}
@media (max-width: 767px) {
.aksrt-product-page__title,
.aksrt-product-editor__title {
font-size: 22px;
}
.aksrt-product-page__thumbs {
gap: 6px;
}
.aksrt-product-page__spec-values {
flex-wrap: wrap;
}
.aksrt-product-page__actions {
grid-template-columns: 1fr;
}
.aksrt-product-page__actions .btn-lg,
.aksrt-product-editor__actions .btn-lg {
width: 100%;
}
}
@media (max-width: 480px) {
.aksrt-product-page,
.aksrt-product-editor {
padding: 16px 0 40px;
}
.aksrt-product-page__price {
font-size: 24px;
}
.aksrt-product-page__meta {
grid-template-columns: 1fr;
}
}
/* ===== Dark mode ===== */
html.dark .aksrt-product-page__badge--promo,
html.dark .aksrt-product-page__badge--promo {
background: rgba(239, 68, 68, 0.18);
}
html.dark .aksrt-product-page__placeholder,
html.dark .aksrt-product-page__placeholder {
background: var(--surface-2);
}
html.dark .aksrt-product-page__thumb,
html.dark .aksrt-product-page__thumb {
border-color: var(--color-line);
}
html.dark .aksrt-product-page__content img,
html.dark .aksrt-product-page__content img {
opacity: 0.92;
}
/* ============================================================
* SKU 规格选择器
* ============================================================ */
.aksrt-product-page__sku-selector {
margin: 16px 0;
padding: 16px;
background: var(--surface-4);
border: 1px solid var(--color-line);
border-radius: var(--radius-lg);
}
.aksrt-product-page__spec-group {
margin-bottom: 12px;
}
.aksrt-product-page__spec-group:last-child {
margin-bottom: 0;
}
.aksrt-product-page__spec-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--color-ink-2);
margin-bottom: 8px;
}
.aksrt-product-page__spec-values {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-product-page__spec-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 48px;
height: 32px;
padding: 0 12px;
font-size: 13px;
font-weight: 500;
color: var(--color-ink-2);
background: var(--surface-1);
border: 1px solid var(--color-line);
border-radius: 6px;
cursor: pointer;
transition: all var(--dur-fast) var(--ease);
}
.aksrt-product-page__spec-btn:hover {
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary);
}
.aksrt-product-page__spec-btn.is-active {
background: var(--color-primary-soft);
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary);
font-weight: 600;
}
.aksrt-product-page__spec-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
text-decoration: line-through;
}
/* SKU 信息区 */
.aksrt-product-page__sku-info {
margin-top: 12px;
padding: 10px 12px;
background: var(--surface-1);
border-radius: 6px;
display: flex;
align-items: center;
gap: 12px;
}
.aksrt-product-page__sku-price {
font-size: 18px;
font-weight: 700;
color: var(--color-error);
}
.aksrt-product-page__sku-stock {
font-size: 13px;
color: var(--color-ink-3);
}
/* Product detail shell — align with single content stream */
.content-shell--product .content-wrap {
padding-top: 8px;
}
.content-shell--product .aksrt-product-page {
width: 100%;
}
.aksrt-product-page__related {
margin-top: 28px;
padding-top: 8px;
}
.aksrt-product-page__related-title {
margin: 0 0 14px;
font-size: 18px;
font-weight: 700;
color: var(--color-ink, #1f2937);
}
.aksrt-product-page__related-grid.aksrt-product-archive__grid {
margin: 0;
}
.aksrt-product-page__merchant-link {
display: inline-block;
margin-top: 4px;
}
-483
View File
@@ -1,483 +0,0 @@
(function () {
"use strict";
var page = document.querySelector("[data-aksrt-cart-page]");
if (!page || !window.aksrtData) {
return;
}
function restBase() {
return ((aksrtData.siteUrl || "").replace(/\/$/, "")) + "/wp-json/aksrt/v1";
}
function rest(path, method, body) {
var opts = {
method: method || "GET",
credentials: "same-origin",
headers: { "X-WP-Nonce": aksrtData.restNonce || "" }
};
if (body !== undefined) {
opts.headers["Content-Type"] = "application/json";
opts.body = JSON.stringify(body);
}
return fetch(restBase() + path, opts).then(function (r) { return r.json(); });
}
function ordersUrl() {
return aksrtData.ordersUrl || (aksrtData.routes && aksrtData.routes.orders) || "/";
}
function shopHomeUrl() {
return (aksrtData.routes && aksrtData.routes.home) || aksrtData.siteUrl || "/";
}
var cartItems = [];
var currencySymbol = page.getAttribute("data-currency-symbol") || "¥";
var itemsEl = page.querySelector("[data-aksrt-cart-items]");
var subtotalEl = page.querySelector("[data-aksrt-summary-subtotal]");
var shippingEl = page.querySelector("[data-aksrt-summary-shipping]");
var discountEl = page.querySelector("[data-aksrt-summary-discount]");
var totalEl = page.querySelector("[data-aksrt-summary-total]");
var previewMessageEl = page.querySelector("[data-aksrt-preview-message]");
var createBtn = page.querySelector("[data-aksrt-create-order]");
var previewBtn = page.querySelector("[data-aksrt-preview-checkout]");
var previewState = null;
var searchParams = new URLSearchParams(window.location.search);
function getCart() { return cartItems; }
function setCart(items) { cartItems = Array.isArray(items) ? items : []; }
function money(value) {
return currencySymbol + Number(value || 0).toFixed(2);
}
function selectedValue(name) {
var checked = page.querySelector('input[name="' + name + '"]:checked');
return checked ? checked.value : "";
}
function getPayload() {
var items = getCart().map(function (item) {
return {
product_id: Number(item.product_id || item.id || 0),
quantity: Number(item.qty || item.quantity || 1),
options_string: item.options_string || ""
};
}).filter(function (item) {
return item.product_id > 0;
});
return {
items: items,
coupon_code: (page.querySelector("[data-aksrt-coupon-code]") || {}).value || "",
shipping_method: selectedValue("aksrt_cart_shipping"),
address_id: Number(selectedValue("aksrt_cart_address") || 0),
payment_method: selectedValue("aksrt_cart_payment"),
customer_note: (page.querySelector("[data-aksrt-customer-note]") || {}).value || ""
};
}
function renderCart() {
var items = getCart();
if (!items.length) {
itemsEl.innerHTML = '' +
'<div class="aksrt-cart-page__empty">' +
'<i class="fas fa-shopping-cart" aria-hidden="true"></i>' +
"<p>购物车为空,去商品页添加想购买的内容。</p>" +
'<a href="' + shopHomeUrl() + '" class="btn-outline btn-sm">继续浏览</a>' +
"</div>";
subtotalEl.textContent = money(0);
shippingEl.textContent = money(0);
discountEl.textContent = "-" + money(0);
totalEl.textContent = money(0);
createBtn.disabled = true;
previewState = null;
return;
}
var total = 0;
var html = "";
items.forEach(function (item, index) {
var qty = Number(item.qty || item.quantity || 1);
var price = Number(item.unit_price != null ? item.unit_price : (item.price != null ? item.price : (item.product && item.product.price) || 0));
var lineTotal = Number(item.line_total != null ? item.line_total : qty * price);
total += lineTotal;
var title = item.title || (item.product && item.product.title) || "商品";
var image = item.image || (item.product && item.product.image) || "";
var opts = item.options_label || item.options_string || "";
html += '' +
'<article class="aksrt-cart-page__item" data-cart-index="' + index + '" data-product-id="' + Number(item.product_id || item.id || 0) + '" data-options-string="' + String(item.options_string || "").replace(/"/g, "&quot;") + '">' +
(image ? '<img class="aksrt-cart-page__item-image" src="' + image + '" alt="">' : '<div class="aksrt-cart-page__item-image aksrt-cart-page__item-image--placeholder"><i class="fas fa-box"></i></div>') +
'<div class="aksrt-cart-page__item-main">' +
'<h3>' + title + "</h3>" +
(opts ? "<small>" + opts + "</small>" : "") +
'<p>' + money(price) + "</p>" +
"</div>" +
'<div class="aksrt-cart-page__item-qty">' +
'<button type="button" data-qty-action="dec">-</button>' +
"<span>" + qty + "</span>" +
'<button type="button" data-qty-action="inc">+</button>' +
"</div>" +
'<strong class="aksrt-cart-page__item-total">' + money(lineTotal) + "</strong>" +
'<button type="button" class="aksrt-cart-page__item-remove" data-qty-action="remove"><i class="fas fa-trash"></i></button>' +
"</article>";
});
itemsEl.innerHTML = html;
subtotalEl.textContent = money(total);
if (!previewState) {
shippingEl.textContent = money(0);
discountEl.textContent = "-" + money(0);
totalEl.textContent = money(total);
}
}
function setSummary(data) {
previewState = data || null;
subtotalEl.textContent = money((data && data.subtotal) || 0);
shippingEl.textContent = money((data && data.shipping_fee) || 0);
discountEl.textContent = "-" + money((data && data.discount) || 0);
totalEl.textContent = money((data && data.total) || 0);
createBtn.disabled = !data;
}
function request(path, body) { return rest(path, "POST", body || {}); }
function previewCheckout() {
var payload = getPayload();
if (!payload.items.length) {
previewMessageEl.textContent = "请先添加商品后再试算。";
return;
}
previewBtn.disabled = true;
previewMessageEl.textContent = "正在试算订单...";
request("/shop/frontend/checkout/preview", payload)
.then(function (result) {
if (!result.success) {
setSummary(null);
previewMessageEl.textContent = result.message || "试算失败。";
return;
}
setSummary(result.data);
previewMessageEl.textContent = "已完成试算,可直接创建订单。";
})
.catch(function () {
setSummary(null);
previewMessageEl.textContent = "网络错误,请稍后重试。";
})
.finally(function () {
previewBtn.disabled = false;
});
}
function createOrder() {
var payload = getPayload();
if (!previewState) {
previewCheckout();
return;
}
createBtn.disabled = true;
request("/shop/frontend/orders", payload)
.then(function (result) {
if (!result.success) {
previewMessageEl.textContent = result.message || "订单创建失败。";
createBtn.disabled = false;
return;
}
var orderNumber = (result.data && result.data.order_number) || "";
var nextOrdersUrl = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending";
if (!orderNumber) {
window.location.href = nextOrdersUrl;
return;
}
// 立即发起支付:跳转型网关直接前往第三方,余额型同步完成。
request("/shop/frontend/orders/" + orderNumber + "/pay", {})
.then(function (pay) {
var data = (pay && pay.data) || {};
if (pay && pay.success && data.redirect && data.payment_url && /^https?:\/\//i.test(data.payment_url)) {
window.location.href = data.payment_url;
return;
}
// 二维码 / 余额 / 失败:统一回订单中心处理(含 ?payment 提示)。
window.location.href = nextOrdersUrl;
})
.catch(function () {
window.location.href = nextOrdersUrl;
});
})
.catch(function () {
previewMessageEl.textContent = "网络错误,请稍后重试。";
createBtn.disabled = false;
});
}
page.addEventListener("click", function (event) {
var actionButton = event.target.closest("[data-qty-action]");
if (actionButton) {
var itemEl = actionButton.closest("[data-cart-index]");
if (!itemEl) return;
var productId = Number(itemEl.getAttribute("data-product-id") || 0);
var optionsString = itemEl.getAttribute("data-options-string") || "";
var action = actionButton.getAttribute("data-qty-action");
var path = "/shop/frontend/cart/add";
var body = { product_id: productId, options_string: optionsString, count: 1 };
if (action === "dec") {
path = "/shop/frontend/cart/reduce";
} else if (action === "remove") {
path = "/shop/frontend/cart/remove";
body = { product_id: productId, options_string: optionsString };
} else if (action !== "inc") {
return;
}
rest(path, "POST", body).then(function (result) {
if (!result || !result.success) {
window.alert((result && result.message) || "操作失败");
return;
}
setSummary(null);
loadServerCart();
});
return;
}
if (event.target.closest("[data-aksrt-clear-cart]")) {
rest("/shop/frontend/cart/empty", "POST", {}).then(function () {
setSummary(null);
loadServerCart();
});
return;
}
if (event.target.closest("[data-aksrt-preview-checkout]")) {
previewCheckout();
return;
}
if (event.target.closest("[data-aksrt-create-order]")) {
createOrder();
return;
}
var resumeButton = event.target.closest("[data-aksrt-resume-order]");
if (resumeButton) {
window.location.href = ordersUrl() + (String(ordersUrl()).indexOf("?") >= 0 ? "&" : "?") + "order_status=pending";
}
});
function loadServerCart() {
return rest("/shop/frontend/cart", "GET").then(function (result) {
var items = (result && result.success && result.data && result.data.items) ? result.data.items : [];
items = items.map(function (it) {
it.qty = it.quantity || it.qty || 1;
it.price = it.unit_price != null ? it.unit_price : (it.product && it.product.price) || 0;
it.title = it.title || (it.product && it.product.title) || "商品";
it.image = it.image || (it.product && it.product.image) || "";
return it;
});
setCart(items);
renderCart();
}).catch(function () {
setCart([]);
renderCart();
});
}
var couponInput = page.querySelector("[data-aksrt-coupon-code]");
if (couponInput && searchParams.has("coupon")) {
couponInput.value = searchParams.get("coupon") || "";
}
/* ── Address Book Management ── */
var addressModal = page.querySelector("[data-aksrt-address-modal]");
var addressForm = page.querySelector("[data-aksrt-address-form]");
var addressList = page.querySelector("[data-aksrt-address-list]");
var addressTitle = page.querySelector("[data-aksrt-address-modal-title]");
function openAddressModal(mode, address) {
if (!addressModal || !addressForm) return;
addressForm.reset();
addressForm.querySelector('[name="address_id"]').value = "";
if (mode === "edit" && address) {
addressTitle.textContent = "编辑地址";
addressForm.querySelector('[name="address_id"]').value = address.id || "";
addressForm.querySelector('[name="name"]').value = address.name || "";
addressForm.querySelector('[name="phone"]').value = address.phone || "";
addressForm.querySelector('[name="province"]').value = address.province || "";
addressForm.querySelector('[name="city"]').value = address.city || "";
addressForm.querySelector('[name="district"]').value = address.district || "";
addressForm.querySelector('[name="address"]').value = address.address || "";
addressForm.querySelector('[name="is_default"]').checked = !!address.is_default;
} else {
addressTitle.textContent = "新增地址";
}
addressModal.hidden = false;
}
function closeAddressModal() {
if (addressModal) addressModal.hidden = true;
}
function refreshAddressList() {
fetch(restBase() + "/shop/frontend/address", {
headers: { "X-WP-Nonce": aksrtData.restNonce }
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (!result.success || !addressList) return;
var addresses = (result.data && result.data.addresses) || [];
renderAddressList(addresses);
})
.catch(function () {});
}
function renderAddressList(addresses) {
if (!addressList) return;
if (!addresses.length) {
addressList.innerHTML =
'<div class="aksrt-cart-page__address-empty" data-aksrt-address-empty>' +
'<i class="fas fa-map-marker-alt" aria-hidden="true"></i>' +
"<p>暂无收货地址,请先新增。</p>" +
"</div>";
return;
}
var html = "";
addresses.forEach(function (addr, index) {
html +=
'<div class="aksrt-cart-page__option-card aksrt-cart-page__address-card" data-aksrt-address-card="' + addr.id + '">' +
'<label class="aksrt-cart-page__address-label">' +
'<input type="radio" name="aksrt_cart_address" value="' + addr.id + '"' + (index === 0 || addr.is_default ? " checked" : "") + ">" +
'<span class="aksrt-cart-page__option-body">' +
"<strong>" + escHtml(addr.name) + (addr.is_default ? ' <span class="aksrt-cart-page__default-tag">默认</span>' : "") + "</strong>" +
"<span>" + escHtml(addr.phone) + "</span>" +
"<span>" + escHtml(addr.full_text) + "</span>" +
"</span></label>" +
'<div class="aksrt-cart-page__address-actions">' +
'<button type="button" class="btn-ghost btn-xs" data-aksrt-address-edit="' + addr.id + '">编辑</button>' +
'<button type="button" class="btn-ghost btn-xs" data-aksrt-address-delete="' + addr.id + '">删除</button>' +
(addr.is_default ? "" : '<button type="button" class="btn-ghost btn-xs" data-aksrt-address-set-default="' + addr.id + '">设为默认</button>') +
"</div></div>";
});
addressList.innerHTML = html;
}
function escHtml(str) {
var div = document.createElement("div");
div.appendChild(document.createTextNode(str || ""));
return div.innerHTML;
}
function getAddressById(id) {
var cards = addressList ? addressList.querySelectorAll("[data-aksrt-address-card]") : [];
var found = null;
cards.forEach(function (card) {
if (String(card.getAttribute("data-aksrt-address-card")) === String(id)) {
var body = card.querySelector(".aksrt-cart-page__option-body");
if (body) {
var spans = body.querySelectorAll("span");
var strong = body.querySelector("strong");
found = {
id: id,
name: strong ? strong.textContent.replace("默认", "").trim() : "",
phone: spans[0] ? spans[0].textContent : "",
full_text: spans[1] ? spans[1].textContent : ""
};
}
}
});
return found;
}
// Address event delegation
page.addEventListener("click", function (event) {
if (event.target.closest("[data-aksrt-address-add]")) {
openAddressModal("add");
return;
}
var editBtn = event.target.closest("[data-aksrt-address-edit]");
if (editBtn) {
var editId = editBtn.getAttribute("data-aksrt-address-edit");
var addr = getAddressById(editId);
openAddressModal("edit", addr);
return;
}
var deleteBtn = event.target.closest("[data-aksrt-address-delete]");
if (deleteBtn) {
var deleteId = deleteBtn.getAttribute("data-aksrt-address-delete");
if (!confirm("确定删除该地址吗?")) return;
rest("/shop/frontend/address/" + deleteId, "DELETE").then(function (result) {
if (!result || !result.success) {
alert((result && result.message) || "删除失败。");
return;
}
refreshAddressList();
}).catch(function () { alert("网络错误。"); });
return;
}
var defaultBtn = event.target.closest("[data-aksrt-address-set-default]");
if (defaultBtn) {
var defaultId = defaultBtn.getAttribute("data-aksrt-address-set-default");
request("/shop/frontend/address/" + defaultId + "/default", {}).then(function (result) {
if (!result.success) {
alert(result.message || "设置失败。");
return;
}
refreshAddressList();
}).catch(function () { alert("网络错误。"); });
return;
}
if (event.target.closest("[data-aksrt-address-modal-close]") || event.target.classList.contains("aksrt-modal__overlay")) {
closeAddressModal();
}
});
if (addressForm) {
addressForm.addEventListener("submit", function (event) {
event.preventDefault();
var formData = new FormData(addressForm);
var addressId = formData.get("address_id");
var payload = {
name: (formData.get("name") || "").trim(),
phone: (formData.get("phone") || "").trim(),
province: (formData.get("province") || "").trim(),
city: (formData.get("city") || "").trim(),
district: (formData.get("district") || "").trim(),
address: (formData.get("address") || "").trim(),
is_default: formData.get("is_default") === "1"
};
if (!payload.name || !payload.phone || !payload.address) {
alert("请填写必填项。");
return;
}
var url = addressId
? "/shop/frontend/address/" + addressId
: "/shop/frontend/address";
request(url, payload).then(function (result) {
if (!result.success) {
alert(result.message || "保存失败。");
return;
}
closeAddressModal();
refreshAddressList();
}).catch(function () { alert("网络错误。"); });
});
}
loadServerCart();
})();
+1 -226
View File
@@ -89,10 +89,6 @@
return suffix ? base + suffix : base;
}
function cartUrl() {
return aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart) || "/";
}
/* ── Order review modal (zibll 1:1) ── */
var STAR_SVG =
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>';
@@ -434,7 +430,7 @@
badge.textContent = statusLabel || newStatus;
}
// Hide action buttons that are no longer relevant
var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel], [data-aksrt-order-after-sale], [data-aksrt-order-receipt], [data-aksrt-order-rebuy]");
var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel]");
for (var i = 0; i < actions.length; i++) {
actions[i].style.display = "none";
}
@@ -640,21 +636,9 @@
// Two-step cancel inside the modal — no window.confirm.
footer += '<button type="button" class="btn-ghost" data-aksrt-order-cancel="' + escapeHtml(order.order_number) + '" data-aksrt-cancel-step="1">关闭订单</button>';
}
if (actions.can_receipt) {
footer += '<button type="button" class="btn-primary" data-aksrt-order-receipt="' + escapeHtml(order.order_number) + '">确认收货</button>';
}
if (actions.can_review) {
footer += '<button type="button" class="btn-primary c-green" data-aksrt-order-review="' + escapeHtml(order.order_number) + '">评价</button>';
}
if (actions.can_pay) {
footer += '<button type="button" class="btn-primary" data-aksrt-order-pay="' + escapeHtml(order.order_number) + '" data-aksrt-order-method="' + escapeHtml((payment && payment.value) || "") + '">立即支付</button>';
}
if (actions.can_after_sale) {
footer += '<button type="button" class="btn-outline" data-aksrt-order-after-sale="' + escapeHtml(order.order_number) + '">申请售后</button>';
}
if (actions.can_rebuy) {
footer += '<button type="button" class="btn-outline" data-aksrt-order-rebuy="' + escapeHtml(order.order_number) + '">再次购买</button>';
}
if (actions.can_view_product && order.permalink) {
footer += '<a class="btn-ghost" href="' + escapeHtml(order.permalink) + '">查看商品</a>';
}
@@ -695,70 +679,6 @@
}
}
/* ── After-Sale Modal ── */
var afterSaleModal = page.querySelector("[data-aksrt-aftersale-modal]");
var afterSaleForm = page.querySelector("[data-aksrt-aftersale-form]");
var afterSaleOrderInput = afterSaleForm ? afterSaleForm.querySelector('[name="order_number"]') : null;
function openAfterSaleModal(orderNumber) {
if (!afterSaleModal || !afterSaleForm) {
showToast("售后表单加载失败,请刷新页面后重试。");
return;
}
afterSaleForm.reset();
if (afterSaleOrderInput) afterSaleOrderInput.value = orderNumber;
afterSaleModal.hidden = false;
}
function closeAfterSaleModal() {
if (afterSaleModal) afterSaleModal.hidden = true;
}
if (afterSaleForm) {
afterSaleForm.addEventListener("submit", function (event) {
event.preventDefault();
var formData = new FormData(afterSaleForm);
var orderNumber = formData.get("order_number");
var reason = (formData.get("reason") || "").trim();
if (!reason) {
showToast("请填写售后原因。");
return;
}
var submitBtn = afterSaleForm.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = "提交中...";
}
request(orderPath(orderNumber, "/after-sale"), {
type: formData.get("type") || "refund",
reason: reason,
note: (formData.get("note") || "").trim()
}).then(function (result) {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = "提交售后";
}
if (!result.success) {
showToast(result.message || "提交售后失败。");
return;
}
closeAfterSaleModal();
showToast("售后申请已提交。");
// Local refresh: update card status
var card = findOrderCard(orderNumber);
updateCardStatus(card, "refunded", "已退款");
}).catch(function () {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = "提交售后";
}
showToast("网络错误,请稍后重试。");
});
});
}
/* ── Countdown timer ── */
function updateCountdowns() {
var nodes = page.querySelectorAll("[data-aksrt-order-countdown]");
@@ -808,56 +728,6 @@
return;
}
// Confirm receipt
var receiptBtn = event.target.closest("[data-aksrt-order-receipt]");
if (receiptBtn) {
var receiptOrder = receiptBtn.getAttribute("data-aksrt-order-receipt");
if (!receiptOrder) return;
if (!window.confirm("确认已收到货物?")) return;
receiptBtn.disabled = true;
request(orderPath(receiptOrder, "/confirm-receipt"), {})
.then(function (result) {
receiptBtn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "确认收货失败");
return;
}
showToast("已确认收货");
updateCardStatus(findOrderCard(receiptOrder), "completed", "已完成");
if (detailOrderNumber === receiptOrder) {
closeOrderDetailModal();
}
})
.catch(function () {
receiptBtn.disabled = false;
showToast("网络错误");
});
return;
}
// Rebuy
var rebuyBtn = event.target.closest("[data-aksrt-order-rebuy]");
if (rebuyBtn) {
var rebuyOrder = rebuyBtn.getAttribute("data-aksrt-order-rebuy");
if (!rebuyOrder) return;
rebuyBtn.disabled = true;
request(orderPath(rebuyOrder, "/rebuy"), {})
.then(function (result) {
rebuyBtn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "再次购买失败");
return;
}
showToast("已加入购物车");
window.location.href = cartUrl();
})
.catch(function () {
rebuyBtn.disabled = false;
showToast("网络错误");
});
return;
}
// Review modal (zibll order_comment_modal 1:1 UX)
var reviewBtn = event.target.closest("[data-aksrt-order-review]");
if (reviewBtn) {
@@ -992,103 +862,8 @@
});
return;
}
// After-Sale
var afterSaleButton = event.target.closest("[data-aksrt-order-after-sale]");
if (afterSaleButton) {
var afterSaleOrderNumber = afterSaleButton.getAttribute("data-aksrt-order-after-sale");
closeOrderDetailModal();
openAfterSaleModal(afterSaleOrderNumber);
return;
}
// Return shipping
var returnShipBtn = event.target.closest("[data-aksrt-return-ship]");
if (returnShipBtn) {
openReturnShipModal(returnShipBtn.getAttribute("data-aksrt-return-ship"));
return;
}
// Cancel after-sale ticket
var asCancelBtn = event.target.closest("[data-aksrt-as-cancel]");
if (asCancelBtn) {
var tid = asCancelBtn.getAttribute("data-aksrt-as-cancel");
if (!tid || !window.confirm("确认取消该售后申请?")) return;
asCancelBtn.disabled = true;
request("/shop/frontend/after-sale/" + tid + "/cancel", { reason: "" })
.then(function (result) {
asCancelBtn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "取消失败");
return;
}
showToast(result.message || "已取消售后");
window.location.reload();
})
.catch(function () {
asCancelBtn.disabled = false;
showToast("网络错误");
});
return;
}
// Close after-sale modal
if (event.target.closest("[data-aksrt-aftersale-modal-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-aftersale-modal]"))) {
closeAfterSaleModal();
return;
}
// Close return-ship modal
if (event.target.closest("[data-aksrt-return-ship-close]") || (event.target.classList && event.target.classList.contains("aksrt-modal__overlay") && event.target.closest("[data-aksrt-return-ship-modal]"))) {
closeReturnShipModal();
}
});
/* ── Return shipping modal ── */
var returnShipModal = page.querySelector("[data-aksrt-return-ship-modal]");
var returnShipForm = page.querySelector("[data-aksrt-return-ship-form]");
function openReturnShipModal(ticketId) {
if (!returnShipModal || !returnShipForm) return;
returnShipForm.reset();
var idInput = returnShipForm.querySelector('[name="ticket_id"]');
if (idInput) idInput.value = ticketId || "";
returnShipModal.hidden = false;
}
function closeReturnShipModal() {
if (returnShipModal) returnShipModal.hidden = true;
}
if (returnShipForm) {
returnShipForm.addEventListener("submit", function (event) {
event.preventDefault();
var fd = new FormData(returnShipForm);
var tid = fd.get("ticket_id");
var carrier = (fd.get("carrier") || "").trim();
var tracking = (fd.get("tracking_no") || "").trim();
if (!carrier || !tracking) {
showToast("请填写快递公司与运单号");
return;
}
var btn = returnShipForm.querySelector('button[type="submit"]');
if (btn) { btn.disabled = true; }
request("/shop/frontend/after-sale/" + tid + "/return-shipping", {
carrier: carrier,
tracking_no: tracking
}).then(function (result) {
if (btn) btn.disabled = false;
if (!result || !result.success) {
showToast((result && result.message) || "提交失败");
return;
}
closeReturnShipModal();
showToast(result.message || "退货物流已提交");
window.location.reload();
}).catch(function () {
if (btn) btn.disabled = false;
showToast("网络错误");
});
});
}
/* ── Coupon wallet ── */
var couponRoot = page.querySelector("[data-aksrt-coupon-wallet-list]");
if (couponRoot) {
-295
View File
@@ -1,295 +0,0 @@
/**
* Product archive — cat filter + sort + AJAX load more (zibll ajax-next UX).
*
* Uses REST /shop/frontend/products/archive and optional AksrtAjaxList events.
*
* @package AKSRT
*/
(function () {
"use strict";
var root = document.querySelector("[data-aksrt-product-archive]");
if (!root) return;
function restBase() {
var site = window.aksrtData && aksrtData.siteUrl ? String(aksrtData.siteUrl).replace(/\/$/, "") : "";
return site + "/wp-json/aksrt/v1";
}
function toast(msg) {
if (typeof window.aksrtToast === "function") {
window.aksrtToast(msg);
return;
}
if (window.console) console.log(msg);
}
var grid = root.querySelector("[data-aksrt-product-grid], #aksrt-product-grid");
var pager = root.querySelector("[data-aksrt-ajaxpager], .ajaxpager");
var countEl = root.querySelector("[data-aksrt-archive-count]");
var headerHost = root.querySelector("[data-aksrt-ajax-header]");
var loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]");
var loader = root.querySelector("[data-aksrt-ajax-loader], .post_ajax_loader");
var state = {
taxonomy: root.getAttribute("data-taxonomy") || "",
termId: Number(root.getAttribute("data-term-id") || 0),
orderby: root.getAttribute("data-orderby") || "date",
order: (root.getAttribute("data-order") || "DESC").toUpperCase(),
page: Number(root.getAttribute("data-page") || 1),
maxPages: Number(root.getAttribute("data-max-pages") || 1),
paging: root.getAttribute("data-paging") || "number",
ias: root.getAttribute("data-ias") === "1",
iasMax: Number(root.getAttribute("data-ias-max") || 5),
loading: false,
autoLoads: 0,
};
function showLoader(on) {
if (loader) {
loader.hidden = !on;
loader.style.display = on ? "" : "none";
loader.classList.toggle("is-loading", !!on);
}
if (pager) pager.classList.toggle("is-ajax-loading", !!on);
}
function updateLoadMore(hasMore) {
var pag = root.querySelector(".ajax-pag");
if (loadMoreBtn) {
loadMoreBtn.style.display = hasMore ? "" : "none";
loadMoreBtn.disabled = false;
loadMoreBtn.textContent = hasMore ? "加载更多" : "没有更多了";
if (!hasMore) loadMoreBtn.classList.add("ajax-nomore");
else loadMoreBtn.classList.remove("ajax-nomore");
}
if (pag) pag.style.display = hasMore ? "" : "none";
}
function syncOrderbyActive() {
root.querySelectorAll("[data-aksrt-shop-orderby]").forEach(function (a) {
var key = a.getAttribute("data-aksrt-shop-orderby") || "";
var active = key === state.orderby;
a.classList.toggle("is-active", active);
a.classList.toggle("focus-color", active && key === "price");
});
}
function fetchArchive(page, orderby, order, replace, termId) {
if (state.loading) return Promise.resolve();
state.loading = true;
showLoader(true);
if (loadMoreBtn) {
loadMoreBtn.disabled = true;
loadMoreBtn.textContent = "加载中…";
}
var qs = new URLSearchParams();
qs.set("taxonomy", state.taxonomy);
qs.set("term_id", String(typeof termId === "number" ? termId : state.termId));
qs.set("page", String(page));
qs.set("orderby", orderby || state.orderby);
if ((orderby || state.orderby) === "price") {
qs.set("order", order || state.order || "ASC");
}
var container = pager || grid || root;
try {
container.dispatchEvent(
new CustomEvent("post_ajax.before", {
bubbles: true,
detail: { page: page, orderby: orderby || state.orderby, replace: !!replace },
})
);
} catch (e0) {}
return fetch(restBase() + "/shop/frontend/products/archive?" + qs.toString(), {
method: "GET",
credentials: "same-origin",
headers: {
Accept: "application/json",
"X-WP-Nonce": (window.aksrtData && aksrtData.restNonce) || "",
"X-AKSRT-Ajax": "list",
},
})
.then(function (r) {
return r.json();
})
.then(function (result) {
state.loading = false;
showLoader(false);
if (!result || !result.success || !result.data) {
toast((result && result.message) || "加载失败");
updateLoadMore(state.page < state.maxPages);
return;
}
var data = result.data;
state.page = Number(data.page || page);
state.maxPages = Number(data.max_pages || 1);
state.orderby = data.orderby || state.orderby;
state.order = String(data.order || state.order || "DESC").toUpperCase();
if (typeof termId === "number" && termId > 0) {
state.termId = termId;
}
if (data.term && data.term.id) {
state.termId = Number(data.term.id);
}
root.setAttribute("data-page", String(state.page));
root.setAttribute("data-max-pages", String(state.maxPages));
root.setAttribute("data-orderby", state.orderby);
root.setAttribute("data-order", state.order);
root.setAttribute("data-term-id", String(state.termId));
if (countEl && typeof data.total !== "undefined") {
countEl.textContent = "共 " + data.total + " 件商品";
}
var titleEl = root.querySelector("[data-aksrt-archive-title]");
if (titleEl && data.term && data.term.name) {
titleEl.textContent = data.term.name;
}
var descEl = root.querySelector("[data-aksrt-archive-desc]");
if (descEl && data.term) {
descEl.textContent = data.term.description || "";
descEl.style.display = data.term.description ? "" : "none";
}
if (headerHost && data.header_html) {
// Keep title/count; replace filter widget if present.
var existingFilter = headerHost.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter");
var tmp = document.createElement("div");
tmp.innerHTML = data.header_html;
var nextFilter = tmp.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter") || tmp.firstElementChild;
if (existingFilter && nextFilter) {
existingFilter.replaceWith(nextFilter);
} else if (!existingFilter && nextFilter) {
headerHost.appendChild(nextFilter);
}
}
var html = data.html || data.empty_html || "";
if (grid) {
if (replace) {
grid.innerHTML = html;
} else if (html) {
grid.insertAdjacentHTML("beforeend", html);
}
}
if (data.paginate_html) {
var oldPag = root.querySelector(".ajax-pag");
var wrap = document.createElement("div");
wrap.innerHTML = data.paginate_html;
var nextPag = wrap.firstElementChild;
if (oldPag && nextPag) oldPag.replaceWith(nextPag);
else if (!oldPag && nextPag && pager) pager.appendChild(nextPag);
loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]");
}
syncOrderbyActive();
updateLoadMore(!!data.has_more || state.page < state.maxPages);
try {
container.dispatchEvent(
new CustomEvent("post_ajax.ed", {
bubbles: true,
detail: { page: state.page, replace: !!replace, data: data },
})
);
document.dispatchEvent(
new CustomEvent("aksrt:posts:rendered", {
bubbles: true,
detail: { container: grid, replace: !!replace },
})
);
} catch (e1) {}
})
.catch(function () {
state.loading = false;
showLoader(false);
updateLoadMore(state.page < state.maxPages);
toast("网络错误");
});
}
function pushUrl(orderby, order, termUrl) {
try {
var u = termUrl ? new URL(termUrl, window.location.origin) : new URL(window.location.href);
if (orderby) u.searchParams.set("orderby", orderby);
else u.searchParams.delete("orderby");
u.searchParams.delete("shop_orderby");
if (orderby === "price" && order) u.searchParams.set("order", order);
else u.searchParams.delete("order");
u.searchParams.delete("paged");
window.history.replaceState({}, "", u.toString());
} catch (err) {}
}
root.addEventListener("click", function (e) {
// Category / sort links with ajax-next — intercept for REST (richer payload).
var termLink = e.target.closest("[data-aksrt-shop-term].ajax-next, a.ajax-next[data-aksrt-shop-term]");
if (termLink && root.contains(termLink)) {
e.preventDefault();
e.stopPropagation();
var tid = Number(termLink.getAttribute("data-aksrt-shop-term") || 0);
var href = termLink.getAttribute("href") || "";
state.autoLoads = 0;
if (tid > 0) state.termId = tid;
pushUrl(state.orderby, state.order, href);
var rt = termLink.getAttribute("route-title");
if (rt) {
try {
document.title = rt;
} catch (e2) {}
}
fetchArchive(1, state.orderby, state.order, true, tid);
return;
}
var sort = e.target.closest("[data-aksrt-shop-orderby]");
if (sort && root.contains(sort) && sort.tagName === "A") {
e.preventDefault();
e.stopPropagation();
var orderby = sort.getAttribute("data-aksrt-shop-orderby") || "date";
var nextOrder = sort.getAttribute("data-aksrt-shop-order") || "";
if (orderby === "price") {
if (state.orderby === "price" && nextOrder) {
state.order = nextOrder.toUpperCase();
} else if (state.orderby !== "price") {
state.order = "ASC";
} else {
state.order = state.order === "ASC" ? "DESC" : "ASC";
}
}
if (orderby === state.orderby && orderby !== "price" && state.paging === "ajax") return;
state.orderby = orderby;
state.autoLoads = 0;
pushUrl(orderby, state.order);
fetchArchive(1, orderby, state.order, true);
return;
}
var more = e.target.closest("[data-aksrt-shop-load-more]");
if (more && root.contains(more)) {
e.preventDefault();
e.stopPropagation();
if (state.page >= state.maxPages) return;
fetchArchive(state.page + 1, state.orderby, state.order, false);
}
});
if (state.paging === "ajax" && state.ias && "IntersectionObserver" in window && loadMoreBtn) {
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
if (state.loading || state.page >= state.maxPages) return;
if (state.iasMax > 0 && state.autoLoads >= state.iasMax) return;
state.autoLoads += 1;
fetchArchive(state.page + 1, state.orderby, state.order, false);
});
},
{ rootMargin: "240px" }
);
io.observe(loadMoreBtn);
}
})();
-407
View File
@@ -1,407 +0,0 @@
/**
* Product purchase page — options selector + server cart.
*
* @package AKSRT
*/
(function () {
"use strict";
function restBase() {
var root = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/$/, "") : "";
return root + "/wp-json/aksrt/v1";
}
function restNonce() {
return (window.aksrtData && aksrtData.restNonce) || "";
}
function toast(msg, type) {
if (typeof window.aksrtToast === "function") {
window.aksrtToast(msg, type || "info");
return;
}
if (window.aksrt && typeof aksrt.notyf === "function") {
aksrt.notyf(msg, { type: type || "info" });
return;
}
window.alert(msg);
}
function rest(path, method, body) {
var opts = {
method: method || "GET",
credentials: "same-origin",
headers: {
"X-WP-Nonce": restNonce(),
},
};
if (body !== undefined) {
opts.headers["Content-Type"] = "application/json";
opts.body = JSON.stringify(body);
}
return fetch(restBase() + path, opts).then(function (r) {
return r.json();
});
}
function money(value, payModo) {
var n = Number(value || 0).toFixed(2);
return payModo === "points" ? n : "¥" + n;
}
/* ── Product page ── */
var productPage = document.querySelector("[data-aksrt-product-page]");
if (productPage) {
var quantityInput = productPage.querySelector("[data-aksrt-quantity]");
var productId = Number(productPage.getAttribute("data-product-id") || 0);
var titleEl = productPage.querySelector(".aksrt-product-page__title");
var imageEl = productPage.querySelector(".aksrt-product-page__primary-image img");
var priceEl = productPage.querySelector("[data-aksrt-dynamic-price], .aksrt-product-page__price");
var stockEl = productPage.querySelector("[data-aksrt-stock-display]");
var couponEl = productPage.querySelector("[data-aksrt-product-coupon]");
var optionsRoot = productPage.querySelector("[data-aksrt-options-selector]");
var optionResult = productPage.querySelector("[data-aksrt-option-result]");
var selectedLabelsEl = productPage.querySelector("[data-aksrt-selected-labels]");
var resolvedPriceEl = productPage.querySelector("[data-aksrt-resolved-price]");
var resolvedStockEl = productPage.querySelector("[data-aksrt-resolved-stock]");
var payModo = productPage.getAttribute("data-pay-modo") || "0";
var basePrice = Number(productPage.getAttribute("data-base-price") || 0);
var baseStock = Number(productPage.getAttribute("data-base-stock") || 0);
var selectedByGroup = {};
var groupCount = optionsRoot
? optionsRoot.querySelectorAll(".aksrt-product-page__option-group").length
: 0;
function currentOptionsKey() {
if (!groupCount) return "";
var labels = [];
for (var i = 0; i < groupCount; i++) {
if (!selectedByGroup[i]) return null;
labels.push(selectedByGroup[i].label);
}
return labels.join("|");
}
function resolveSelection() {
if (!groupCount) {
return {
key: "",
labels: "",
price: basePrice,
stock: baseStock,
image: "",
};
}
var key = currentOptionsKey();
if (key === null) return null;
var price = basePrice;
var image = "";
var labels = [];
for (var i = 0; i < groupCount; i++) {
var sel = selectedByGroup[i];
labels.push(sel.label);
price += Number(sel.priceChange || 0);
if (sel.image) image = sel.image;
}
return {
key: key,
labels: labels.join(" / "),
price: Math.max(0, price),
stock: baseStock,
image: image,
};
}
function paintSelection() {
var resolved = resolveSelection();
if (!resolved) {
if (optionResult) optionResult.hidden = true;
return;
}
if (priceEl) {
if (groupCount && productPage.getAttribute("data-price-range") === "1" && currentOptionsKey() === null) {
/* keep range until complete selection */
} else {
priceEl.textContent = money(resolved.price, payModo) + (payModo === "points" ? "" : "");
if (payModo === "points" && priceEl.querySelector("small") === null) {
/* leave as-is */
}
}
}
if (stockEl && resolved.stock > -1) {
stockEl.textContent = String(resolved.stock);
}
if (quantityInput && resolved.stock > 0) {
quantityInput.setAttribute("max", String(resolved.stock));
if (Number(quantityInput.value) > resolved.stock) {
quantityInput.value = String(resolved.stock);
}
}
if (imageEl && resolved.image) {
imageEl.setAttribute("src", resolved.image);
}
if (optionResult) {
optionResult.hidden = false;
if (selectedLabelsEl) selectedLabelsEl.textContent = resolved.labels;
if (resolvedPriceEl) resolvedPriceEl.textContent = money(resolved.price, payModo);
if (resolvedStockEl) {
resolvedStockEl.textContent =
resolved.stock <= -1 ? "库存充足" : "库存 " + resolved.stock;
}
}
}
if (optionsRoot) {
optionsRoot.addEventListener("click", function (event) {
var btn = event.target.closest("[data-option-group]");
if (!btn || !optionsRoot.contains(btn)) return;
var group = btn.getAttribute("data-option-group");
var label = btn.getAttribute("data-option-label") || "";
var priceChange = Number(btn.getAttribute("data-price-change") || 0);
var image = btn.getAttribute("data-option-image") || "";
optionsRoot
.querySelectorAll('[data-option-group="' + group + '"]')
.forEach(function (el) {
el.classList.remove("is-active");
});
btn.classList.add("is-active");
selectedByGroup[group] = { label: label, priceChange: priceChange, image: image };
paintSelection();
});
}
function ensureOptionsSelected() {
if (!groupCount) return true;
if (currentOptionsKey() === null) {
toast("请先选择商品规格", "warning");
return false;
}
return true;
}
function addToServerCart(replaceQty) {
if (!window.aksrtData || !aksrtData.isLoggedIn) {
window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/";
return;
}
if (!ensureOptionsSelected()) return;
var qty = Number(quantityInput ? quantityInput.value : 1) || 1;
var optionsString = currentOptionsKey() || "";
var body = {
product_id: productId,
options_string: optionsString,
count: qty,
};
rest("/shop/frontend/cart/add", "POST", body)
.then(function (result) {
if (!result || !result.success) {
toast((result && (result.message || result.error)) || "加入购物车失败", "error");
return;
}
toast("已加入购物车", "success");
if (replaceQty) {
var cartUrl = new URL(
(window.aksrtData && (aksrtData.cartUrl || (aksrtData.routes && aksrtData.routes.cart))) || "/",
window.location.origin
);
if (couponEl && couponEl.value) {
cartUrl.searchParams.set("coupon", couponEl.value);
}
window.location.href = cartUrl.toString();
}
})
.catch(function () {
toast("网络错误,请稍后重试", "error");
});
}
productPage.addEventListener("click", function (event) {
var qtyBtn = event.target.closest("[data-aksrt-qty-action]");
if (qtyBtn && quantityInput) {
var action = qtyBtn.getAttribute("data-aksrt-qty-action");
var current = Number(quantityInput.value || 1);
var max = Number(quantityInput.getAttribute("max") || 999);
quantityInput.value = action === "inc" ? Math.min(max, current + 1) : Math.max(1, current - 1);
return;
}
if (event.target.closest("[data-aksrt-add-cart]")) {
addToServerCart(false);
return;
}
if (event.target.closest("[data-aksrt-buy-now]")) {
addToServerCart(true);
return;
}
var thumb = event.target.closest("[data-aksrt-product-thumb]");
if (thumb && imageEl) {
imageEl.setAttribute("src", thumb.getAttribute("data-aksrt-product-thumb"));
return;
}
var videoBtn = event.target.closest("[data-aksrt-product-video]");
if (videoBtn) {
var url = videoBtn.getAttribute("data-aksrt-product-video");
if (url) window.open(url, "_blank", "noopener");
return;
}
var favBtn = event.target.closest("[data-aksrt-toggle-fav]");
if (favBtn) {
if (!window.aksrtData || !aksrtData.isLoggedIn) {
window.location.href = (aksrtData && aksrtData.routes && aksrtData.routes.login) || "/";
return;
}
var pid = Number(favBtn.getAttribute("data-aksrt-toggle-fav") || productId);
rest("/shop/frontend/products/" + pid + "/favorite", "POST", {})
.then(function (result) {
if (!result || !result.success) {
toast((result && result.message) || "操作失败", "error");
return;
}
var active = !!(result.data && result.data.is_favorite);
productPage.querySelectorAll("[data-aksrt-toggle-fav]").forEach(function (btn) {
btn.classList.toggle("is-active", active);
});
toast((result.message) || (active ? "已收藏" : "已取消收藏"), "success");
})
.catch(function () {
toast("网络错误", "error");
});
return;
}
var tabBtn = event.target.closest("[data-aksrt-product-tab]");
if (tabBtn) {
var tab = tabBtn.getAttribute("data-aksrt-product-tab");
productPage.querySelectorAll("[data-aksrt-product-tab]").forEach(function (b) {
b.classList.toggle("is-active", b === tabBtn);
});
productPage.querySelectorAll("[data-aksrt-product-panel]").forEach(function (panel) {
panel.hidden = panel.getAttribute("data-aksrt-product-panel") !== tab;
});
return;
}
if (event.target.closest("[data-aksrt-show-params]")) {
var paramsModal = productPage.querySelector("[data-aksrt-params-modal]");
if (paramsModal) paramsModal.hidden = false;
return;
}
if (event.target.closest("[data-aksrt-show-services]")) {
var servicesModal = productPage.querySelector("[data-aksrt-services-modal]");
if (servicesModal) servicesModal.hidden = false;
return;
}
if (event.target.closest("[data-aksrt-close-modal]")) {
var modal = event.target.closest("[data-aksrt-params-modal], [data-aksrt-services-modal], .aksrt-modal");
if (modal) modal.hidden = true;
}
});
}
/* ── Product editor (seller) ── */
var editor = document.querySelector("[data-aksrt-product-editor]");
if (!editor || !window.aksrtData) {
return;
}
var form = editor.querySelector("[data-aksrt-product-form]");
var messageEl = editor.querySelector("[data-aksrt-product-editor-message]");
if (!form) return;
function selectedMultiValues(select) {
if (!select) return [];
return Array.prototype.slice
.call(select.options)
.filter(function (option) { return option.selected; })
.map(function (option) { return Number(option.value || 0); })
.filter(Boolean);
}
function parseOptionsText(text) {
var groups = [];
String(text || "").split(String.fromCharCode(10)).forEach(function (line) {
line = line.replace(/\r/g, "").trim();
if (!line) return;
var parts = line.split("|");
if (parts.length < 2) return;
var name = parts[0].trim();
var values = [];
parts.slice(1).join("|").split(",").forEach(function (chunk) {
chunk = chunk.trim();
if (!chunk) return;
var segs = chunk.split(":");
var label = (segs[0] || "").trim();
if (!label) return;
values.push({ label: label, price_change: Number(segs[1] || 0), image: "" });
});
if (name && values.length) {
groups.push({ name: name, view_mode: "list", values: values });
}
});
return groups;
}
form.addEventListener("submit", function (event) {
event.preventDefault();
var formData = new FormData(form);
var productId = Number(editor.getAttribute("data-product-id") || 0);
var gallery = String(formData.get("gallery") || "")
.split(String.fromCharCode(10))
.map(function (line) { return line.replace(/\r/g, "").trim(); })
.filter(Boolean);
var payload = {
title: String(formData.get("title") || "").trim(),
price: Number(formData.get("price") || 0),
stock: Number(formData.get("stock") || 0),
excerpt: String(formData.get("excerpt") || ""),
content: String(formData.get("content") || ""),
status: String(formData.get("status") || "draft"),
categories: selectedMultiValues(form.querySelector('select[name="categories[]"]')),
tags: String(formData.get("tags") || "")
.split(",")
.map(function (tag) { return tag.trim(); })
.filter(Boolean),
image: String(formData.get("image") || "").trim(),
gallery: gallery,
shipping_mode: String(formData.get("shipping_mode") || "express"),
shipping_fee: Number(formData.get("shipping_fee") || 0),
after_sale_policy: String(formData.get("after_sale_policy") || ""),
support_refund: formData.get("support_refund") === "1",
options: parseOptionsText(formData.get("options_text") || ""),
start_price: Number(formData.get("start_price") || 0),
pay_modo: String(formData.get("pay_modo") || "0"),
product_params: String(formData.get("product_params") || ""),
product_video: String(formData.get("product_video") || "")
.split(String.fromCharCode(10))
.map(function (line) { return line.replace(/\r/g, "").trim(); })
.filter(Boolean)
};
var path = productId > 0 ? "/shop/frontend/products/" + productId : "/shop/frontend/products";
if (messageEl) messageEl.textContent = "正在保存商品...";
rest(path, "POST", payload)
.then(function (result) {
if (!result.success) {
if (messageEl) messageEl.textContent = result.message || "保存失败。";
return;
}
var centerUrl = aksrtData.userCenterUrl || "/";
if (messageEl) messageEl.textContent = "商品已保存,正在返回用户中心...";
window.location.href = centerUrl;
})
.catch(function () {
if (messageEl) messageEl.textContent = "网络错误,请稍后重试。";
});
});
})();
+1 -35
View File
@@ -111,7 +111,7 @@ add_filter('query_vars', function ($vars) {
// 主题切换离开时:清理本主题注册的 cron 任务
add_action('switch_theme', function () {
$hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup', 'aksrt_reconcile_refunds', 'aksrt_after_sale_auto_cancel'];
$hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup'];
foreach ($hooks as $hook) {
$next = wp_next_scheduled($hook);
while ($next) {
@@ -318,10 +318,6 @@ if (class_exists('\AKSRT\Payment\Order')) {
\AKSRT\Payment\Order::get_instance()->init();
}
if (class_exists('\AKSRT\Payment\AfterSale')) {
\AKSRT\Payment\AfterSale::get_instance()->init();
}
// Register built-in payment gateways
if (class_exists('\AKSRT\Payment\Gateway')) {
$gw = \AKSRT\Payment\Gateway::get_instance();
@@ -358,30 +354,6 @@ if (class_exists('\AKSRT\Core\TermMeta')) {
\AKSRT\Core\TermMeta::get_instance()->init();
}
// 商城 taxonomy term meta + shop_discount 优惠活动
if (class_exists('\AKSRT\Shop\TermMeta')) {
\AKSRT\Shop\TermMeta::get_instance()->init();
}
// 商品后台列表自定义列(销量/综合数据/商家/优惠活动)
if (is_admin() && class_exists('\AKSRT\Admin\ShopController')) {
\AKSRT\Admin\ShopController::get_instance()->init_product_admin_columns();
}
// 商品原生编辑 meta box(价格/库存/发货售后)
if (is_admin() && class_exists('\AKSRT\Shop\ProductMetaBox')) {
\AKSRT\Shop\ProductMetaBox::get_instance()->init();
}
// 商品原生变体 meta box
if (is_admin() && class_exists('\AKSRT\Shop\ProductVariantsMetaBox')) {
\AKSRT\Shop\ProductVariantsMetaBox::get_instance()->init();
}
// 商品评价/评分系统
if (class_exists('\AKSRT\Shop\ProductReview')) {
\AKSRT\Shop\ProductReview::get_instance()->init();
}
// 文章局部配置 meta box(仅后台)
if (is_admin() && class_exists('\AKSRT\Core\PostMetaBox')) {
\AKSRT\Core\PostMetaBox::get_instance()->init();
@@ -434,12 +406,6 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayCoupons')) {
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayChargeCards')) {
\AKSRT\Admin\Native\PayChargeCards::get_instance()->init();
}
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayShipping')) {
\AKSRT\Admin\Native\PayShipping::get_instance()->init();
}
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayAfterSale')) {
\AKSRT\Admin\Native\PayAfterSale::get_instance()->init();
}
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayOrders')) {
\AKSRT\Admin\Native\PayOrders::get_instance()->init();
}
-96
View File
@@ -1,96 +0,0 @@
<?php
/**
* Commerce center (cart / orders / revenue) presentation helpers.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_commerce_nav_items')) {
/**
* Secondary nav for commerce center pages.
*
* @return array<int, array{key:string,label:string,url:string}>
*/
function aksrt_get_commerce_nav_items(): array {
$items = [];
$cart = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : '';
if ($cart !== '') {
$items[] = [
'key' => 'cart',
'label' => __('购物车', 'aksrt'),
'url' => $cart,
];
}
$orders = function_exists('aksrt_get_orders_page_url') ? aksrt_get_orders_page_url() : '';
if ($orders !== '') {
$items[] = [
'key' => 'orders',
'label' => __('我的订单', 'aksrt'),
'url' => $orders,
];
}
$revenue = function_exists('aksrt_get_revenue_page_url') ? aksrt_get_revenue_page_url() : '';
if ($revenue !== '') {
$items[] = [
'key' => 'revenue',
'label' => __('创作收益', 'aksrt'),
'url' => $revenue,
];
}
$center = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : '';
if ($center !== '') {
$items[] = [
'key' => 'center',
'label' => __('个人中心', 'aksrt'),
'url' => $center,
];
}
return $items;
}
}
if (!function_exists('aksrt_render_commerce_page_header')) {
/**
* Shared commerce page header: title / intro / secondary nav.
*
* @param array{title?:string,subtitle?:string,active?:string} $args
*/
function aksrt_render_commerce_page_header(array $args = []): void {
$title = (string) ($args['title'] ?? '');
$subtitle = (string) ($args['subtitle'] ?? '');
$active = sanitize_key((string) ($args['active'] ?? ''));
$nav = aksrt_get_commerce_nav_items();
?>
<header class="aksrt-commerce-header">
<?php if ($title !== '') : ?>
<h1 class="aksrt-commerce-header__title"><?php echo esc_html($title); ?></h1>
<?php endif; ?>
<?php if ($subtitle !== '') : ?>
<p class="aksrt-commerce-header__subtitle"><?php echo esc_html($subtitle); ?></p>
<?php endif; ?>
<?php if (!empty($nav)) : ?>
<nav class="aksrt-commerce-header__nav" aria-label="<?php esc_attr_e('交易中心导航', 'aksrt'); ?>">
<?php foreach ($nav as $item) :
$is_active = $active !== '' && $active === ($item['key'] ?? '');
?>
<a
class="aksrt-commerce-header__nav-item<?php echo $is_active ? ' is-active' : ''; ?>"
href="<?php echo esc_url((string) ($item['url'] ?? '#')); ?>"
<?php echo $is_active ? ' aria-current="page"' : ''; ?>
>
<?php echo esc_html((string) ($item['label'] ?? '')); ?>
</a>
<?php endforeach; ?>
</nav>
<?php endif; ?>
</header>
<?php
}
}
+1 -40
View File
@@ -17,41 +17,6 @@ function aksrt_get_order_status_labels(): array {
return [];
}
function aksrt_get_product_page_view(int $product_id): array {
if ($product_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) {
return [];
}
return \AKSRT\Shop\Shop::get_instance()->get_product_detail_view($product_id);
}
function aksrt_get_product_edit_page_view(int $user_id, int $product_id = 0): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) {
return [
'allowed' => false,
'is_edit' => false,
'product' => [],
];
}
return \AKSRT\Shop\Shop::get_instance()->get_product_edit_view($user_id, $product_id);
}
function aksrt_get_cart_page_view(int $user_id): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Shop\Shop')) {
return [
'payment_methods' => [],
'shipping_methods' => [],
'address_book' => [],
'coupon_enabled' => false,
'currency_symbol' => '¥',
'pending_orders' => [],
];
}
return \AKSRT\Shop\Shop::get_instance()->get_cart_page_view($user_id);
}
function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = '', string $product_type = ''): array {
$empty = [
'items' => [],
@@ -79,10 +44,6 @@ function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page
return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args);
}
if (class_exists('\AKSRT\Shop\Shop')) {
return \AKSRT\Shop\Shop::get_instance()->get_order_center_view($user_id, $args);
}
return $empty;
}
@@ -135,4 +96,4 @@ function aksrt_get_revenue_page_view(int $user_id): array {
'total_withdrawn' => $total_withdrawn,
'records' => $records,
];
}
}
@@ -25,7 +25,7 @@ $section = sanitize_key((string) ($_GET['section'] ?? ''));
if ($tab === '' && isset($_GET['tab']) && sanitize_key((string) $_GET['tab']) !== 'orders') {
// When rendered as standalone legacy include, outer tab may be status.
$maybe = sanitize_key((string) $_GET['tab']);
if (in_array($maybe, ['pending', 'to-ship', 'shipped', 'received', 'review', 'after'], true)) {
if (in_array($maybe, ['pending', 'after'], true)) {
$tab = $maybe;
}
}
@@ -37,9 +37,6 @@ if (isset($tab_aliases[$tab])) {
$tab_status_map = [
'pending' => \AKSRT\Payment\Order::STATUS_PENDING,
'to-ship' => \AKSRT\Payment\Order::STATUS_PAID,
'shipped' => \AKSRT\Payment\Order::STATUS_DELIVERED,
'review' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
'after' => \AKSRT\Payment\Order::STATUS_REFUNDING,
];
if ($tab !== '' && isset($tab_status_map[$tab])) {
@@ -58,15 +55,10 @@ if ($type_options === [] && class_exists('\AKSRT\Core\DomainModel')) {
$type_options = array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels());
}
$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null;
$tabs = [
'' => __('全部', 'aksrt'),
'pending' => __('待支付', 'aksrt'),
'to-ship' => __('待发货', 'aksrt'),
'shipped' => __('待收货', 'aksrt'),
'review' => __('待评价', 'aksrt'),
'after' => __('售后', 'aksrt'),
'after' => __('退款中', 'aksrt'),
];
$base_query = ['tab' => 'orders'];
@@ -115,16 +107,8 @@ $pending_count = (int) ($stats['pending'] ?? 0);
<i class="fas fa-receipt uc-orders-panel__title-icon" aria-hidden="true"></i>
<?php esc_html_e('我的订单', 'aksrt'); ?>
</div>
<div class="muted-2-color em09 mt3"><?php esc_html_e('查看商城、付费内容、会员与充值等全部订单。', 'aksrt'); ?></div>
<div class="muted-2-color em09 mt3"><?php esc_html_e('查看付费内容、会员与充值等全部订单。', 'aksrt'); ?></div>
</div>
<?php if (function_exists('aksrt_get_cart_page_url') && aksrt_get_cart_page_url() !== '') : ?>
<a class="uc-orders-panel__cart-link" href="<?php echo esc_url(aksrt_get_cart_page_url()); ?>">
<i class="fas fa-shopping-cart" aria-hidden="true"></i>
<span><?php esc_html_e('购物车', 'aksrt'); ?></span>
<i class="fa fa-angle-right em12" aria-hidden="true"></i>
</a>
<?php endif; ?>
</div>
<?php if (!empty($stats)) : ?>
<div class="aksrt-orders-page__stats" data-aksrt-order-stats>
@@ -145,9 +129,7 @@ $pending_count = (int) ($stats['pending'] ?? 0);
<div class="aksrt-orders-page__extra-nav" data-aksrt-orders-extra-nav>
<a href="<?php echo esc_url($orders_url(['section' => null])); ?>" class="aksrt-orders-page__extra-link<?php echo $section === '' ? ' is-active' : ''; ?>"><?php esc_html_e('订单列表', 'aksrt'); ?></a>
<a href="<?php echo esc_url($orders_url(['section' => 'after-sale'])); ?>" class="aksrt-orders-page__extra-link<?php echo $section === 'after-sale' ? ' is-active' : ''; ?>"><?php esc_html_e('售后工单', 'aksrt'); ?></a>
<a href="<?php echo esc_url($orders_url(['section' => 'coupons'])); ?>" class="aksrt-orders-page__extra-link<?php echo $section === 'coupons' ? ' is-active' : ''; ?>"><?php esc_html_e('优惠券', 'aksrt'); ?></a>
<a href="<?php echo esc_url($orders_url(['section' => 'favorites'])); ?>" class="aksrt-orders-page__extra-link<?php echo $section === 'favorites' ? ' is-active' : ''; ?>"><?php esc_html_e('收藏', 'aksrt'); ?></a>
</div>
<?php if ($type_options && $section === '') : ?>
@@ -304,23 +286,11 @@ $pending_count = (int) ($stats['pending'] ?? 0);
<?php if (!empty($actions['can_pay'])) : ?>
<button type="button" class="btn-primary btn-sm" data-aksrt-order-pay="<?php echo esc_attr($on); ?>" data-aksrt-order-method="<?php echo esc_attr((string) ($order['payment_method']['value'] ?? '')); ?>"><?php esc_html_e('去支付', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_receipt'])) : ?>
<button type="button" class="btn-primary btn-sm" data-aksrt-order-receipt="<?php echo esc_attr($on); ?>"><?php esc_html_e('确认收货', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_review']) || (class_exists('\AKSRT\Payment\Order') && $os === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW)) : ?>
<button type="button" class="btn-primary btn-sm c-green" data-aksrt-order-review="<?php echo esc_attr($on); ?>"><?php esc_html_e('评价', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_rebuy'])) : ?>
<button type="button" class="btn-outline btn-sm" data-aksrt-order-rebuy="<?php echo esc_attr($on); ?>"><?php esc_html_e('再次购买', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_after_sale'])) : ?>
<button type="button" class="btn-outline btn-sm" data-aksrt-order-after-sale="<?php echo esc_attr($on); ?>"><?php esc_html_e('售后', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions['can_view_product']) && !empty($order['permalink'])) : ?>
<a class="btn-ghost btn-sm" href="<?php echo esc_url((string) $order['permalink']); ?>"><?php
echo (class_exists('\AKSRT\Core\DomainModel') && $ptype === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE)
? esc_html__('去内容', 'aksrt')
: esc_html__('商品页', 'aksrt');
: esc_html__('查看', 'aksrt');
?></a>
<?php endif; ?>
</div>
@@ -348,7 +318,7 @@ $pending_count = (int) ($stats['pending'] ?? 0);
<div class="aksrt-orders-page__empty">
<i class="fas fa-receipt" aria-hidden="true"></i>
<p><?php esc_html_e('暂无订单记录', 'aksrt'); ?></p>
<p class="aksrt-orders-page__empty-hint muted-2-color em09"><?php esc_html_e('去逛逛商城或开通会员,订单会出现在这里。', 'aksrt'); ?></p>
<p class="aksrt-orders-page__empty-hint muted-2-color em09"><?php esc_html_e('开通会员或购买付费内容,订单会出现在这里。', 'aksrt'); ?></p>
</div>
<?php endif; ?>
@@ -367,114 +337,6 @@ $pending_count = (int) ($stats['pending'] ?? 0);
</div>
</div>
<!-- After-sale modal -->
<div class="aksrt-modal" data-aksrt-aftersale-modal hidden>
<div class="aksrt-modal__overlay"></div>
<div class="aksrt-modal__dialog">
<div class="aksrt-modal__header">
<h3><?php esc_html_e('申请售后', 'aksrt'); ?></h3>
<button type="button" class="aksrt-modal__close" data-aksrt-aftersale-modal-close>&times;</button>
</div>
<form class="aksrt-modal__body" data-aksrt-aftersale-form>
<input type="hidden" name="order_number" value="">
<div class="aksrt-orders-page__field">
<label><?php esc_html_e('售后类型', 'aksrt'); ?></label>
<select name="type" class="aksrt-orders-page__select">
<?php if (class_exists('\AKSRT\Payment\AfterSale')) : ?>
<?php foreach (\AKSRT\Payment\AfterSale::get_valid_types() as $type_key => $type_label) : ?>
<option value="<?php echo esc_attr($type_key); ?>"><?php echo esc_html($type_label); ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="aksrt-orders-page__field">
<label><?php esc_html_e('售后原因', 'aksrt'); ?> *</label>
<textarea name="reason" class="aksrt-orders-page__textarea" rows="4" required placeholder="<?php esc_attr_e('请描述问题', 'aksrt'); ?>"></textarea>
</div>
<div class="aksrt-modal__footer">
<button type="button" class="btn-ghost" data-aksrt-aftersale-modal-close><?php esc_html_e('取消', 'aksrt'); ?></button>
<button type="submit" class="btn-primary"><?php esc_html_e('提交', 'aksrt'); ?></button>
</div>
</form>
</div>
</div>
<!-- Return shipping modal -->
<div class="aksrt-modal" data-aksrt-return-ship-modal hidden>
<div class="aksrt-modal__overlay"></div>
<div class="aksrt-modal__dialog">
<div class="aksrt-modal__header">
<h3><?php esc_html_e('填写退货物流', 'aksrt'); ?></h3>
<button type="button" class="aksrt-modal__close" data-aksrt-return-ship-close>&times;</button>
</div>
<form class="aksrt-modal__body" data-aksrt-return-ship-form>
<input type="hidden" name="ticket_id" value="">
<div class="aksrt-orders-page__field">
<label><?php esc_html_e('快递公司', 'aksrt'); ?> *</label>
<input type="text" name="carrier" class="aksrt-orders-page__input" required placeholder="<?php esc_attr_e('如:顺丰速运', 'aksrt'); ?>">
</div>
<div class="aksrt-orders-page__field">
<label><?php esc_html_e('运单号', 'aksrt'); ?> *</label>
<input type="text" name="tracking_no" class="aksrt-orders-page__input" required placeholder="<?php esc_attr_e('快递单号', 'aksrt'); ?>">
</div>
<div class="aksrt-modal__footer">
<button type="button" class="btn-ghost" data-aksrt-return-ship-close><?php esc_html_e('取消', 'aksrt'); ?></button>
<button type="submit" class="btn-primary"><?php esc_html_e('提交物流', 'aksrt'); ?></button>
</div>
</form>
</div>
</div>
<?php if ($section === 'after-sale' && class_exists('\AKSRT\Payment\AfterSale')) :
$tickets = \AKSRT\Payment\AfterSale::get_instance()->get_user_tickets($user_id, 50);
?>
<section class="aksrt-orders-page__aftersale" data-aksrt-aftersale-list>
<h2><?php esc_html_e('我的售后', 'aksrt'); ?></h2>
<?php if (!empty($tickets)) : ?>
<div class="aksrt-as-list">
<?php foreach ($tickets as $t) :
$tid = (int) ($t['id'] ?? 0);
$events = is_array($t['events'] ?? null) ? $t['events'] : [];
$actions_t = is_array($t['actions'] ?? null) ? $t['actions'] : [];
?>
<article class="aksrt-as-card" data-aksrt-as-ticket="<?php echo esc_attr((string) $tid); ?>">
<div class="aksrt-as-card__head">
<strong>#<?php echo esc_html((string) $tid); ?></strong>
<span class="aksrt-as-card__order"><?php echo esc_html((string) ($t['order_number'] ?? '')); ?></span>
<span class="aksrt-as-card__status"><?php echo esc_html((string) ($t['status_label'] ?? $t['status'] ?? '')); ?></span>
</div>
<div class="aksrt-as-card__meta muted-2-color">
<?php echo esc_html((string) ($t['type_label'] ?? $t['type'] ?? '')); ?>
· <?php echo esc_html((string) ($t['created_at'] ?? '')); ?>
</div>
<div class="aksrt-as-card__reason"><?php echo esc_html((string) ($t['reason'] ?? '')); ?></div>
<?php if ($events) : ?>
<ol class="aksrt-as-timeline">
<?php foreach (array_reverse($events) as $ev) : ?>
<li>
<time><?php echo esc_html((string) ($ev['time'] ?? '')); ?></time>
<span><?php echo esc_html((string) ($ev['label'] ?? '')); ?></span>
</li>
<?php endforeach; ?>
</ol>
<?php endif; ?>
<div class="aksrt-as-card__actions">
<?php if (!empty($actions_t['can_return_ship'])) : ?>
<button type="button" class="btn-primary btn-sm" data-aksrt-return-ship="<?php echo esc_attr((string) $tid); ?>"><?php esc_html_e('填写退货物流', 'aksrt'); ?></button>
<?php endif; ?>
<?php if (!empty($actions_t['can_cancel'])) : ?>
<button type="button" class="btn-ghost btn-sm" data-aksrt-as-cancel="<?php echo esc_attr((string) $tid); ?>"><?php esc_html_e('取消售后', 'aksrt'); ?></button>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else : ?>
<p class="muted-2-color"><?php esc_html_e('暂无售后记录', 'aksrt'); ?></p>
<?php endif; ?>
</section>
<?php endif; ?>
<?php if ($section === 'coupons') : ?>
<section class="aksrt-orders-page__coupons" data-aksrt-coupon-wallet>
<h2><?php esc_html_e('优惠券', 'aksrt'); ?></h2>
@@ -483,27 +345,4 @@ $pending_count = (int) ($stats['pending'] ?? 0);
</div>
</section>
<?php endif; ?>
<?php if ($section === 'favorites' && $shop) :
$fav_data = $shop->get_user_favorites($user_id);
?>
<section class="aksrt-orders-page__favorites">
<h2><?php esc_html_e('收藏商品', 'aksrt'); ?></h2>
<?php if (!empty($fav_data['items'])) : ?>
<div class="aksrt-orders-page__fav-grid">
<?php foreach ($fav_data['items'] as $fav) : ?>
<a href="<?php echo esc_url((string) ($fav['permalink'] ?? '#')); ?>" class="aksrt-orders-page__fav-card">
<?php if (!empty($fav['image'])) : ?>
<img src="<?php echo esc_url((string) $fav['image']); ?>" alt="" loading="lazy">
<?php endif; ?>
<h3><?php echo esc_html((string) ($fav['title'] ?? '')); ?></h3>
<strong>¥<?php echo esc_html(number_format((float) ($fav['price'] ?? 0), 2)); ?></strong>
</a>
<?php endforeach; ?>
</div>
<?php else : ?>
<p><?php esc_html_e('暂无收藏商品', 'aksrt'); ?></p>
<?php endif; ?>
</section>
<?php endif; ?>
</div>
@@ -7,19 +7,12 @@
defined('ABSPATH') || exit;
$cart_url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : '';
$level_url = isset($dashboard_tabs['level'])
? aksrt_get_user_center_tab_url('level')
: '';
$auth_url = isset($dashboard_tabs['auth']) ? aksrt_get_user_center_tab_url('auth') : '';
$services = array_values(array_filter([
[
'label' => __('购物车', 'aksrt'),
'icon' => 'shopping-cart',
'tone' => 'cart',
'url' => $cart_url,
],
[
'label' => __('我的等级', 'aksrt'),
'icon' => 'chart-line',
@@ -19,12 +19,6 @@ $order_stats = class_exists('\AKSRT\Payment\Order')
$order_repo = class_exists('\AKSRT\Payment\OrderRepository')
? \AKSRT\Payment\OrderRepository::get_instance()
: null;
$shop_type = class_exists('\AKSRT\Core\DomainModel')
? \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP
: 'shop_product';
$shop_paid = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PAID, 'product_type' => $shop_type]) : 0;
$shop_delivered = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'product_type' => $shop_type]) : 0;
$shop_review = $order_repo ? $order_repo->count_user_orders($order_user_id, ['status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW, 'product_type' => $shop_type]) : 0;
$order_items = [
[
'label' => __('待支付', 'aksrt'),
@@ -32,24 +26,6 @@ $order_items = [
'tab' => 'pending',
'count' => (int) ($order_stats['pending'] ?? 0),
],
[
'label' => __('待发货', 'aksrt'),
'icon' => 'gift',
'tab' => 'to-ship',
'count' => $shop_paid,
],
[
'label' => __('待收货', 'aksrt'),
'icon' => 'truck',
'tab' => 'shipped',
'count' => $shop_delivered,
],
[
'label' => __('待评价', 'aksrt'),
'icon' => 'comment-dots',
'tab' => 'review',
'count' => $shop_review,
],
[
'label' => __('售后', 'aksrt'),
'icon' => 'undo-alt',
@@ -113,12 +113,6 @@ $_aksrt_special_templates = [
'title' => '我的订单',
'fallback' => '/orders/',
],
'cart' => [
'file' => 'templates/pages/page-cart.php',
'slug' => 'cart',
'title' => '购物车',
'fallback' => '/cart/',
],
'revenue' => [
'file' => 'templates/pages/page-revenue.php',
'slug' => 'revenue',
@@ -413,11 +407,6 @@ function aksrt_get_current_special_template_path(): string {
return $orders_template ?: '';
}
if (get_query_var('aksrt_cart')) {
$cart_template = aksrt_locate_template_file('templates/pages/page-cart.php');
return $cart_template ?: '';
}
if (get_query_var('aksrt_revenue')) {
$revenue_template = aksrt_locate_template_file('templates/pages/page-revenue.php');
return $revenue_template ?: '';
+1 -6
View File
@@ -92,12 +92,7 @@ function aksrt_register_widgets() {
register_widget('\AKSRT\Widgets\WidgetIconCoverCards');
register_widget('\AKSRT\Widgets\WidgetBookmarkLinks');
// Shop product widgets (§6.9)
register_widget('\AKSRT\Widgets\WidgetProductList');
register_widget('\AKSRT\Widgets\WidgetProductOneline');
register_widget('\AKSRT\Widgets\WidgetProductTab');
aksrt_maybe_repair_sidebar_blog_widgets();
aksrt_maybe_repair_sidebar_blog_widgets();
}
add_action('widgets_init', 'aksrt_register_widgets');
-357
View File
@@ -1,357 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
class PayAfterSale {
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_after_sale';
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('admin_post_aksrt_pay_update_after_sale', [$this, 'handle_update_after_sale']);
add_action('admin_post_aksrt_pay_create_after_sale', [$this, 'handle_create_after_sale']);
add_action('admin_post_aksrt_pay_after_sale_action', [$this, 'handle_after_sale_action']);
}
public function handle_create_after_sale(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
$redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order';
$nonce_action = $redirect_page === 'after_sale' ? 'aksrt_pay_create_after_sale' : ('aksrt_pay_create_after_sale_' . $order_number);
check_admin_referer($nonce_action);
$ok = false;
if ($order_number !== '' && class_exists('\AKSRT\Payment\AfterSale')) {
$id = \AKSRT\Payment\AfterSale::get_instance()->create([
'order_number' => $order_number,
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'refund')),
'reason' => sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? '')),
'amount' => (float) ($_POST['amount'] ?? 0),
'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')),
]);
$ok = !is_wp_error($id) && $id > 0;
}
$msg = $ok ? 'after_sale_created' : 'after_sale_create_failed';
if ($redirect_page === 'after_sale') {
wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $msg, 's' => $order_number]));
exit;
}
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $order_number,
'aksrt_pay_msg' => $msg,
]));
exit;
}
public function render_after_sale_page(): void {
$this->ensure_cap();
$this->render_header(__('售后管理', 'aksrt'));
if (!class_exists('\AKSRT\Payment\AfterSale')) {
echo '<div class="notice notice-error"><p>' . esc_html__('售后模块未初始化。', 'aksrt') . '</p></div>';
$this->render_footer();
return;
}
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
if ($search === '' && isset($_GET['order_number'])) {
$search = sanitize_text_field((string) wp_unslash($_GET['order_number']));
}
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$svc = \AKSRT\Payment\AfterSale::get_instance();
$stats = $svc->get_stats();
$status_options = $this->after_sale_status_options(true);
$type_options = $this->after_sale_type_options(true);
echo '<p>' . esc_html(sprintf(
/* translators: 1: total 2: pending */
__('售后工单共 %1$d · 待处理 %2$d', 'aksrt'),
(int) ($stats['total'] ?? 0),
(int) ($stats['pending'] ?? 0)
)) . '</p>';
echo '<h2>' . esc_html__('新建售后', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 20px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_after_sale" />';
echo '<input type="hidden" name="redirect_page" value="after_sale" />';
wp_nonce_field('aksrt_pay_create_after_sale');
echo '<p><label>' . esc_html__('订单号', 'aksrt') . '<br><input type="text" name="order_number" value="' . esc_attr($search) . '" required class="regular-text" /></label></p>';
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
foreach ($this->after_sale_type_options(false) as $k => $label) {
printf('<option value="%s">%s</option>', esc_attr($k), esc_html($label));
}
echo '</select></label></p>';
echo '<p><label>' . esc_html__('金额', 'aksrt') . '<br><input type="number" step="0.01" min="0" name="amount" value="0" /></label></p>';
echo '<p><label>' . esc_html__('原因', 'aksrt') . '<br><input type="text" name="reason" required class="regular-text" /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" /></label></p>';
submit_button(__('创建售后', 'aksrt'), 'primary', '', false);
echo '</form>';
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_after_sale" />';
echo '<select name="status">';
foreach ($status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
echo '<select name="type">';
foreach ($type_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($type, $key, false), esc_html($label));
}
echo '</select> ';
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('订单号', 'aksrt') . '" /> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$list_args = [
'status' => $status,
'type' => $type,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
];
if ($search !== '') {
$list_args['order_number'] = $search;
}
$result = $svc->get_list($list_args);
$items = $result['items'] ?? [];
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
$row_status_options = $this->after_sale_status_options(false);
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无售后工单', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0);
$user_id = (int) ($row['user_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
$st = (string) ($row['status'] ?? '');
$order_number = (string) ($row['order_number'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '"><code>' . esc_html($order_number) . '</code></a></td>';
echo '<td>' . esc_html((string) ($row['type_label'] ?? $type_options[$row['type'] ?? ''] ?? $row['type'] ?? '')) . '</td>';
echo '<td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
echo '<td>' . esc_html((string) ($row['status_label'] ?? $row_status_options[$st] ?? $st)) . '</td>';
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
if ($st === 'pending') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_after_sale_action" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="as_action" value="approve" />';
wp_nonce_field('aksrt_pay_after_sale_action_' . $id);
submit_button(__('同意', 'aksrt'), 'small', '', false);
echo '</form>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_after_sale_action" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="as_action" value="reject" />';
wp_nonce_field('aksrt_pay_after_sale_action_' . $id);
echo '<button type="submit" class="button button-small" onclick="return confirm(\'' . esc_js(__('确认拒绝该售后?', 'aksrt')) . '\');">' . esc_html__('拒绝', 'aksrt') . '</button>';
echo '</form>';
}
if ($st === 'processing') {
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_after_sale_action" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="as_action" value="confirm_return" />';
wp_nonce_field('aksrt_pay_after_sale_action_' . $id);
submit_button(__('确认收货', 'aksrt'), 'small', '', false);
echo '</form>';
}
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;flex-wrap:wrap;">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_after_sale" />';
echo '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_pay_update_after_sale_' . $id);
echo '<select name="new_status">';
foreach ($row_status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($label));
}
echo '</select>';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('更新', 'aksrt'), 'small', '', false);
echo '</form>';
echo '</td></tr>';
}
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
$this->render_footer();
}
public function handle_update_after_sale(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0;
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
check_admin_referer('aksrt_pay_update_after_sale_' . $id);
$ok = false;
if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\AfterSale')) {
$ok = \AKSRT\Payment\AfterSale::get_instance()->update_status($id, $new_status, get_current_user_id(), $note);
}
wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $ok ? 'after_sale_updated' : 'after_sale_failed']));
exit;
}
public function handle_after_sale_action(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0;
$as_action = isset($_POST['as_action']) ? sanitize_key((string) wp_unslash($_POST['as_action'])) : '';
check_admin_referer('aksrt_pay_after_sale_action_' . $id);
$ok = false;
if ($id > 0 && class_exists('\AKSRT\Payment\AfterSale')) {
$svc = \AKSRT\Payment\AfterSale::get_instance();
$operator_id = get_current_user_id();
if ($as_action === 'approve') {
$ok = $svc->merchant_approve($id, $operator_id);
} elseif ($as_action === 'reject') {
$ok = $svc->merchant_reject($id, $operator_id, __('商家拒绝', 'aksrt'));
} elseif ($as_action === 'confirm_return') {
$ok = $svc->merchant_confirm_return($id, $operator_id);
}
}
wp_safe_redirect($this->page_url('aksrt_after_sale', [
'aksrt_pay_msg' => $ok ? 'after_sale_action_ok' : 'after_sale_action_failed',
]));
exit;
}
private function after_sale_status_options(bool $include_all = false): array {
$options = [];
if ($include_all) {
$options[''] = __('全部状态', 'aksrt');
}
if (class_exists('\AKSRT\Payment\AfterSale')) {
foreach (\AKSRT\Payment\AfterSale::get_status_options() as $item) {
$value = (string) ($item['value'] ?? '');
if ($value === '') {
continue;
}
$options[$value] = (string) ($item['label'] ?? $value);
}
}
return $options;
}
/**
* @return array<string, string>
*/
public function after_sale_type_options_public(bool $include_all = false): array {
return $this->after_sale_type_options($include_all);
}
private function after_sale_type_options(bool $include_all = false): array {
$options = [];
if ($include_all) {
$options[''] = __('全部类型', 'aksrt');
}
// Keep operational create options broader than public get_type_options().
$defaults = [
'refund' => __('仅退款', 'aksrt'),
'refund_return' => __('退货退款', 'aksrt'),
'exchange' => __('换货', 'aksrt'),
'repair' => __('维修', 'aksrt'),
];
if (class_exists('\AKSRT\Payment\AfterSale')) {
foreach (\AKSRT\Payment\AfterSale::get_type_options() as $item) {
$value = (string) ($item['value'] ?? '');
if ($value === '') {
continue;
}
$defaults[$value] = (string) ($item['label'] ?? $value);
}
}
return array_merge($options, $defaults);
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
}
private function render_header(string $title): void {
echo '<div class="wrap">';
echo '<h1>' . esc_html($title) . '</h1>';
$this->render_flash();
}
private function render_footer(): void {
echo '</div>';
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$map = [
'after_sale_updated' => __('售后状态已更新。', 'aksrt'),
'after_sale_failed' => __('售后状态更新失败。', 'aksrt'),
'after_sale_created' => __('售后工单已创建。', 'aksrt'),
'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'),
'after_sale_action_ok' => __('售后操作已完成。', 'aksrt'),
'after_sale_action_failed' => __('售后操作失败。', 'aksrt'),
];
if (!isset($map[$code])) {
return;
}
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
}
private function page_url(string $slug, array $args = []): string {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
+1 -1
View File
@@ -372,7 +372,7 @@ class PayCoupons {
'ids' => $ids,
];
if ($type === 'category') {
$scope['taxonomies'] = ['aksrt_product_cat', 'category'];
$scope['taxonomies'] = ['category'];
}
return $scope;
}
+1 -45
View File
@@ -74,28 +74,6 @@ class PayMenus {
wp_die(esc_html__('下载记录模块未初始化。', 'aksrt'));
},
],
'shipping' => [
'slug' => 'aksrt_shipping',
'title' => __('发货及物流', 'aksrt'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) {
PayShipping::get_instance()->render_shipping_page();
return;
}
wp_die(esc_html__('物流模块未初始化。', 'aksrt'));
},
],
'after_sale' => [
'slug' => 'aksrt_after_sale',
'title' => __('售后管理', 'aksrt'),
'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) {
PayAfterSale::get_instance()->render_after_sale_page();
return;
}
wp_die(esc_html__('售后模块未初始化。', 'aksrt'));
},
],
'coupons' => [
'slug' => 'aksrt_coupons',
'title' => __('优惠码', 'aksrt'),
@@ -203,23 +181,6 @@ class PayMenus {
echo '</div>';
}
}
if ($page !== 'aksrt_after_sale' && class_exists('\AKSRT\Payment\AfterSale')) {
$as_stats = \AKSRT\Payment\AfterSale::get_instance()->get_stats();
$pending_as = (int) ($as_stats['pending'] ?? 0);
if ($pending_as > 0) {
$url = admin_url('admin.php?page=aksrt_after_sale&status=pending');
echo '<div class="notice notice-info is-dismissible">';
echo '<h3>' . esc_html__('售后工单待处理', 'aksrt') . '</h3>';
echo '<p>' . esc_html(sprintf(
/* translators: %d: pending after-sale count */
__('您有 %d 个售后工单待处理', 'aksrt'),
$pending_as
)) . '</p>';
echo '<p><a class="button button-primary" href="' . esc_url($url) . '">' . esc_html__('去处理', 'aksrt') . '</a></p>';
echo '</div>';
}
}
}
private function ensure_cap(): void {
@@ -255,17 +216,12 @@ class PayMenus {
? \AKSRT\Payment\ChargeCard::get_instance()->get_stats()
: [];
echo '<p class="description">' . esc_html__('运营数据入口(对齐 zibll 商城中心)。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '</p>';
echo '<p class="description">' . esc_html__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '</p>';
echo '<div class="aksrt-pay-stats" style="display:flex;flex-wrap:wrap;gap:16px;margin:20px 0;">';
$pending_ship = 0;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$pending_ship = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1])['total'] ?? 0);
}
$this->stat_card(__('订单总数', 'aksrt'), (string) (int) ($order_stats['total'] ?? 0), $this->page_url('aksrt_orders'));
$this->stat_card(__('待支付', 'aksrt'), (string) (int) ($order_stats['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending']));
$this->stat_card(__('已支付', 'aksrt'), (string) (int) ($order_stats['paid'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'paid']));
$this->stat_card(__('待发货', 'aksrt'), (string) $pending_ship, $this->page_url('aksrt_shipping', ['view' => 'pending']));
$this->stat_card(__('营收', 'aksrt'), number_format((float) ($order_stats['revenue'] ?? 0), 2), $this->page_url('aksrt_orders'));
$this->stat_card(__('待审提现', 'aksrt'), (string) (int) ($withdraw_stats['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending']));
$this->stat_card(__('可用卡密', 'aksrt'), (string) (int) ($card_stats['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active']));
-112
View File
@@ -288,13 +288,6 @@ class PayOrders {
$admin_note = (string) $order['note'];
}
$shipping_items = class_exists('\AKSRT\Payment\Shipping')
? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number)
: [];
$after_sale = class_exists('\AKSRT\Payment\AfterSale')
? (\AKSRT\Payment\AfterSale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20])['items'] ?? [])
: [];
echo '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">&larr; ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
@@ -344,87 +337,6 @@ class PayOrders {
echo '</select> ';
submit_button(__('保存状态', 'aksrt'), 'primary', '', false);
echo '</form>';
echo '<h2 style="margin-top:24px;">' . esc_html__('物流记录', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 16px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_shipping" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
echo '<input type="hidden" name="redirect_page" value="order" />';
wp_nonce_field('aksrt_pay_create_shipping_' . $order_number);
echo '<p><label>' . esc_html__('承运商', 'aksrt') . '<br>';
$this->render_carrier_field('');
echo '</label></p>';
echo '<p><label>' . esc_html__('运单号', 'aksrt') . '<br><input type="text" name="tracking_no" required /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" /></label></p>';
submit_button(__('创建发货', 'aksrt'), 'secondary', '', false);
echo ' <a class="button" href="' . esc_url($this->page_url('aksrt_shipping', ['s' => $order_number])) . '">' . esc_html__('物流管理', 'aksrt') . '</a>';
echo '</form>';
if (!$shipping_items) {
echo '<p>' . esc_html__('暂无物流记录。', 'aksrt') . '</p>';
} else {
$status_options = $this->shipping_status_options(false);
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>' . esc_html__('承运商', 'aksrt') . '</th><th>' . esc_html__('运单号', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
foreach ($shipping_items as $item) {
$sid = (int) ($item['id'] ?? 0);
$st = (string) ($item['status'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $sid) . '</td>';
echo '<td>' . esc_html((string) ($item['carrier'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($item['tracking_no'] ?? '')) . '</td>';
echo '<td>' . esc_html($status_options[$st] ?? $st) . '</td>';
echo '<td>' . esc_html((string) ($item['created_at'] ?? '')) . '</td>';
echo '<td><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_shipping" />';
echo '<input type="hidden" name="shipping_id" value="' . esc_attr((string) $sid) . '" />';
echo '<input type="hidden" name="redirect_order" value="' . esc_attr($order_number) . '" />';
wp_nonce_field('aksrt_pay_update_shipping_' . $sid);
echo '<select name="new_status">';
foreach ($status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($label));
}
echo '</select>';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('更新', 'aksrt'), 'small', '', false);
echo '</form></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
echo '<h2 style="margin-top:24px;">' . esc_html__('售后工单', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 16px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_after_sale" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $user_id) . '" />';
echo '<input type="hidden" name="redirect_page" value="order" />';
wp_nonce_field('aksrt_pay_create_after_sale_' . $order_number);
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
foreach ($this->after_sale_type_options(false) as $k => $label) {
printf('<option value="%s">%s</option>', esc_attr($k), esc_html($label));
}
echo '</select></label></p>';
echo '<p><label>' . esc_html__('金额', 'aksrt') . '<br><input type="number" step="0.01" min="0" name="amount" value="' . esc_attr((string) ($order['amount'] ?? 0)) . '" /></label></p>';
echo '<p><label>' . esc_html__('原因', 'aksrt') . '<br><input type="text" name="reason" required class="regular-text" /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" /></label></p>';
submit_button(__('创建售后', 'aksrt'), 'secondary', '', false);
echo ' <a class="button" href="' . esc_url($this->page_url('aksrt_after_sale', ['s' => $order_number])) . '">' . esc_html__('售后管理', 'aksrt') . '</a>';
echo '</form>';
if (!$after_sale) {
echo '<p>' . esc_html__('暂无售后工单。', 'aksrt') . '</p>';
} else {
echo '<table class="widefat striped"><thead><tr><th>ID</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
foreach ($after_sale as $item) {
$tid = (int) ($item['id'] ?? 0);
echo '<tr>';
echo '<td>' . esc_html((string) $tid) . '</td>';
echo '<td>' . esc_html((string) ($item['type_label'] ?? $item['type'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($item['status_label'] ?? $item['status'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($item['created_at'] ?? '')) . '</td>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_after_sale', ['s' => $order_number])) . '">' . esc_html__('在售后页处理', 'aksrt') . '</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}
}
public function handle_update_order_status(): void {
@@ -510,30 +422,6 @@ class PayOrders {
exit;
}
private function shipping_status_options(bool $include_all = false): array {
if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) {
return PayShipping::get_instance()->shipping_status_options_public($include_all);
}
return $include_all ? ['' => __('全部状态', 'aksrt')] : [];
}
private function after_sale_type_options(bool $include_all = false): array {
if (class_exists('AKSRT\\Admin\\Native\\PayAfterSale')) {
return PayAfterSale::get_instance()->after_sale_type_options_public($include_all);
}
return $include_all ? ['' => __('全部类型', 'aksrt')] : [
'refund' => __('仅退款', 'aksrt'),
];
}
private function render_carrier_field(string $selected = ''): void {
if (class_exists('AKSRT\\Admin\\Native\\PayShipping')) {
PayShipping::get_instance()->render_carrier_field_public($selected);
return;
}
echo '<input type="text" name="carrier" value="' . esc_attr($selected) . '" required />';
}
private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void {
$actions = [
'completed' => __('完成', 'aksrt'),
-507
View File
@@ -1,507 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
class PayShipping {
public const CAPABILITY = 'manage_options';
public const PAGE_SLUG = 'aksrt_shipping';
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('admin_post_aksrt_pay_update_shipping', [$this, 'handle_update_shipping']);
add_action('admin_post_aksrt_pay_query_express', [$this, 'handle_query_express']);
add_action('admin_post_aksrt_pay_create_shipping', [$this, 'handle_create_shipping']);
}
public function render_shipping_page(): void {
$this->ensure_cap();
$this->render_header(__('发货及物流', 'aksrt'));
if (!class_exists('\AKSRT\Payment\Shipping')) {
echo '<div class="notice notice-error"><p>' . esc_html__('物流模块未初始化。', 'aksrt') . '</p></div>';
$this->render_footer();
return;
}
$view = isset($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : 'records';
if (!in_array($view, ['records', 'pending'], true)) {
$view = 'records';
}
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$svc = \AKSRT\Payment\Shipping::get_instance();
$stats = $svc->get_stats();
$status_options = $this->shipping_status_options(true);
$pending_total = 0;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$pending_total = (int) (\AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders(['limit' => 1, 'search' => ''])['total'] ?? 0);
}
echo '<p>' . esc_html(sprintf(
/* translators: 1: shipping records 2: pending shipment orders */
__('物流记录 %1$d 条 · 待发货订单 %2$d', 'aksrt'),
(int) ($stats['total'] ?? 0),
$pending_total
)) . '</p>';
echo '<h2 class="nav-tab-wrapper" style="margin:12px 0 16px;">';
printf(
'<a href="%s" class="nav-tab%s">%s</a>',
esc_url($this->page_url('aksrt_shipping', ['view' => 'pending'])),
$view === 'pending' ? ' nav-tab-active' : '',
esc_html(sprintf(__('待发货 (%d)', 'aksrt'), $pending_total))
);
printf(
'<a href="%s" class="nav-tab%s">%s</a>',
esc_url($this->page_url('aksrt_shipping', ['view' => 'records'])),
$view === 'records' ? ' nav-tab-active' : '',
esc_html__('物流记录', 'aksrt')
);
echo '</h2>';
if ($view === 'pending') {
$this->render_pending_shipment_board($search, $page_num, $per_page);
$this->render_footer();
return;
}
echo '<h2>' . esc_html__('新建发货', 'aksrt') . '</h2>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:8px 0 20px;display:flex;flex-wrap:wrap;gap:8px;align-items:end;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_shipping" />';
echo '<input type="hidden" name="redirect_page" value="shipping" />';
wp_nonce_field('aksrt_pay_create_shipping');
echo '<p><label>' . esc_html__('订单号', 'aksrt') . '<br><input type="text" name="order_number" value="' . esc_attr($search) . '" required class="regular-text" /></label></p>';
echo '<p><label>' . esc_html__('承运商', 'aksrt') . '<br>';
$this->render_carrier_field('');
echo '</label></p>';
echo '<p><label>' . esc_html__('运单号', 'aksrt') . '<br><input type="text" name="tracking_no" required /></label></p>';
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" /></label></p>';
submit_button(__('创建发货', 'aksrt'), 'primary', '', false);
echo '</form>';
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_shipping" />';
echo '<input type="hidden" name="view" value="records" />';
echo '<select name="status">';
foreach ($status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('订单号 / 运单号', 'aksrt') . '" /> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$args = [
'status' => $status,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
];
if ($search !== '') {
if (preg_match('/^[A-Za-z0-9_-]+$/', $search)) {
$args['order_number'] = $search;
} else {
$args['tracking_no'] = $search;
}
}
$result = $svc->get_list($args);
$items = $result['items'] ?? [];
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
$row_status_options = $this->shipping_status_options(false);
$show_express_id = isset($_GET['express_id']) ? (int) $_GET['express_id'] : 0;
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('承运商', 'aksrt') . '</th><th>' . esc_html__('运单号', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无物流记录', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0);
$st = (string) ($row['status'] ?? '');
$order_number = (string) ($row['order_number'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '"><code>' . esc_html($order_number) . '</code></a></td>';
echo '<td>' . esc_html((string) ($row['carrier'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($row['tracking_no'] ?? '')) . '</td>';
echo '<td>' . esc_html($row_status_options[$st] ?? $st) . '</td>';
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_pay_update_shipping" />';
echo '<input type="hidden" name="shipping_id" value="' . esc_attr((string) $id) . '" />';
wp_nonce_field('aksrt_pay_update_shipping_' . $id);
echo '<select name="new_status">';
foreach ($row_status_options as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($st, $key, false), esc_html($label));
}
echo '</select>';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('更新', 'aksrt'), 'small', '', false);
echo '</form>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_pay_query_express" />';
echo '<input type="hidden" name="shipping_id" value="' . esc_attr((string) $id) . '" />';
echo '<input type="hidden" name="force" value="1" />';
wp_nonce_field('aksrt_pay_query_express_' . $id);
submit_button(__('查物流', 'aksrt'), 'small', '', false);
echo '</form>';
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_shipping', ['view' => 'records', 'express_id' => $id, 's' => $order_number])) . '">' . esc_html__('轨迹', 'aksrt') . '</a>';
echo '</td></tr>';
if ($show_express_id === $id) {
echo '<tr class="aksrt-express-row"><td colspan="7">';
$this->render_express_timeline_panel($id);
echo '</td></tr>';
}
}
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
$this->render_footer();
}
private function render_pending_shipment_board(string $search, int $page_num, int $per_page): void {
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
echo '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
return;
}
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_shipping" />';
echo '<input type="hidden" name="view" value="pending" />';
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('订单号 / 类型', 'aksrt') . '" /> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
echo '</form>';
$result = \AKSRT\Payment\OrderRepository::get_instance()->get_pending_shipment_orders([
'search' => $search,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
]);
$items = $result['items'] ?? [];
$total = (int) ($result['total'] ?? 0);
$pages = max(1, (int) ($result['max_pages'] ?? 1));
$labels = class_exists('\AKSRT\Core\DomainModel')
? \AKSRT\Core\DomainModel::order_status_labels()
: [];
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
echo '<th>' . esc_html__('用户', 'aksrt') . '</th>';
echo '<th>' . esc_html__('类型', 'aksrt') . '</th>';
echo '<th>' . esc_html__('金额', 'aksrt') . '</th>';
echo '<th>' . esc_html__('状态', 'aksrt') . '</th>';
echo '<th>' . esc_html__('支付时间', 'aksrt') . '</th>';
echo '<th>' . esc_html__('立即发货', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无待发货订单', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$order_number = (string) ($row['order_number'] ?? '');
$user_id = (int) ($row['user_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
$status_key = (string) ($row['status'] ?? '');
echo '<tr>';
echo '<td><a href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '"><code>' . esc_html($order_number) . '</code></a></td>';
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
echo '<td>' . esc_html((string) ($row['product_type'] ?? '')) . '</td>';
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '</td>';
echo '<td>' . esc_html($labels[$status_key] ?? $status_key) . '</td>';
echo '<td>' . esc_html((string) ($row['paid_at'] ?? $row['created_at'] ?? '')) . '</td>';
echo '<td>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;flex-wrap:wrap;gap:4px;align-items:center;">';
echo '<input type="hidden" name="action" value="aksrt_pay_create_shipping" />';
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
echo '<input type="hidden" name="redirect_page" value="pending" />';
wp_nonce_field('aksrt_pay_create_shipping_' . $order_number);
$this->render_carrier_field('');
echo '<input type="text" name="tracking_no" required placeholder="' . esc_attr__('运单号', 'aksrt') . '" />';
echo '<input type="text" name="note" placeholder="' . esc_attr__('备注', 'aksrt') . '" />';
submit_button(__('发货', 'aksrt'), 'primary small', '', false);
echo '</form>';
echo '</td>';
echo '</tr>';
}
echo '</tbody></table>';
$this->render_pagination($pages, $page_num);
}
public function handle_update_shipping(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0;
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
check_admin_referer('aksrt_pay_update_shipping_' . $id);
$ok = false;
if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\Shipping')) {
$ok = \AKSRT\Payment\Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id());
}
$redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : '';
if ($redirect_order !== '') {
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $redirect_order,
'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed',
]));
exit;
}
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'records',
'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed',
]));
exit;
}
public function handle_query_express(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0;
$force = !empty($_POST['force']);
check_admin_referer('aksrt_pay_query_express_' . $id);
$ok = false;
$order_number = '';
if ($id > 0 && class_exists('\AKSRT\Payment\Shipping')) {
$svc = \AKSRT\Payment\Shipping::get_instance();
$record = $svc->get($id);
$order_number = is_array($record) ? (string) ($record['order_number'] ?? '') : '';
$result = $svc->query_express($id, $force);
$ok = !empty($result['success']);
}
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'records',
'express_id' => $id,
's' => $order_number,
'aksrt_pay_msg' => $ok ? 'express_ok' : 'express_failed',
]));
exit;
}
public function handle_create_shipping(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : '';
$redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order';
$nonce_action = $redirect_page === 'shipping' ? 'aksrt_pay_create_shipping' : ('aksrt_pay_create_shipping_' . $order_number);
check_admin_referer($nonce_action);
$ok = false;
if ($order_number !== '' && class_exists('\AKSRT\Payment\Shipping')) {
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order) {
$this->redirect_shipping_create_result(false, $order_number, $redirect_page);
}
}
$id = \AKSRT\Payment\Shipping::get_instance()->create([
'order_number' => $order_number,
'carrier' => sanitize_text_field((string) wp_unslash($_POST['carrier'] ?? '')),
'tracking_no' => sanitize_text_field((string) wp_unslash($_POST['tracking_no'] ?? '')),
'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')),
'operator_id' => get_current_user_id(),
]);
$ok = $id > 0;
}
$this->redirect_shipping_create_result($ok, $order_number, $redirect_page);
}
private function redirect_shipping_create_result(bool $ok, string $order_number, string $redirect_page): void {
$msg = $ok ? 'shipping_created' : 'shipping_create_failed';
if ($redirect_page === 'shipping') {
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'records',
'aksrt_pay_msg' => $msg,
's' => $order_number,
]));
exit;
}
if ($redirect_page === 'pending') {
wp_safe_redirect($this->page_url('aksrt_shipping', [
'view' => 'pending',
'aksrt_pay_msg' => $msg,
]));
exit;
}
wp_safe_redirect($this->page_url('aksrt_orders', [
'order' => $order_number,
'aksrt_pay_msg' => $msg,
]));
exit;
}
public function shipping_status_options_public(bool $include_all = false): array {
return $this->shipping_status_options($include_all);
}
private function shipping_status_options(bool $include_all = false): array {
$options = [];
if ($include_all) {
$options[''] = __('全部状态', 'aksrt');
}
if (class_exists('\AKSRT\Payment\Shipping')) {
foreach (\AKSRT\Payment\Shipping::get_status_options() as $item) {
$value = (string) ($item['value'] ?? '');
if ($value === '') {
continue;
}
$options[$value] = (string) ($item['label'] ?? $value);
}
}
if (count($options) === (int) $include_all) {
$options = array_merge($options, [
'shipped' => __('已发货', 'aksrt'),
'in_transit' => __('运输中', 'aksrt'),
'delivered' => __('已签收', 'aksrt'),
'returned' => __('已退回', 'aksrt'),
]);
}
return $options;
}
public function render_carrier_field_public(string $selected = ''): void {
$this->render_carrier_field($selected);
}
private function render_carrier_field(string $selected = ''): void {
$options = [];
if (class_exists('\AKSRT\Payment\Shipping')) {
foreach (\AKSRT\Payment\Shipping::get_carrier_options() as $item) {
$value = (string) ($item['value'] ?? '');
$label = (string) ($item['label'] ?? $value);
if ($value !== '') {
$options[$value] = $label;
}
}
}
if ($options) {
echo '<select name="carrier" required>';
foreach ($options as $value => $label) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr($value),
selected($selected, $value, false),
esc_html($label)
);
}
echo '</select>';
return;
}
echo '<input type="text" name="carrier" value="' . esc_attr($selected) . '" required placeholder="SF/YTO/..." />';
}
private function render_express_timeline_panel(int $shipping_id): void {
if ($shipping_id <= 0 || !class_exists('\AKSRT\Payment\Shipping')) {
echo '<p>' . esc_html__('无法加载物流轨迹。', 'aksrt') . '</p>';
return;
}
$svc = \AKSRT\Payment\Shipping::get_instance();
// Prefer cached data; do not force remote query on pure view.
$timeline = $svc->build_express_timeline($shipping_id);
$record = $svc->get($shipping_id);
echo '<div style="padding:10px 12px;background:#f6f7f7;border:1px solid #dcdcde;">';
echo '<p style="margin:0 0 8px;"><strong>' . esc_html__('物流轨迹', 'aksrt') . '</strong> · ';
echo esc_html((string) ($record['carrier'] ?? '')) . ' / ' . esc_html((string) ($record['tracking_no'] ?? ''));
echo '</p>';
if (!$timeline) {
echo '<p style="margin:0;">' . esc_html__('暂无轨迹,可点击「查物流」拉取快递 API。', 'aksrt') . '</p>';
echo '</div>';
return;
}
echo '<ol style="margin:0;padding-left:18px;">';
// newest first for operators
$timeline = array_reverse($timeline);
foreach ($timeline as $item) {
$time = (string) ($item['time'] ?? '');
$label = (string) ($item['label'] ?? '');
echo '<li style="margin:0 0 6px;"><code>' . esc_html($time) . '</code> ' . esc_html($label) . '</li>';
}
echo '</ol></div>';
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
}
private function render_header(string $title): void {
echo '<div class="wrap">';
echo '<h1>' . esc_html($title) . '</h1>';
$this->render_flash();
}
private function render_footer(): void {
echo '</div>';
}
private function render_flash(): void {
if (empty($_GET['aksrt_pay_msg'])) {
return;
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$map = [
'shipping_updated' => __('物流状态已更新。', 'aksrt'),
'shipping_failed' => __('物流状态更新失败。', 'aksrt'),
'shipping_created' => __('发货记录已创建。', 'aksrt'),
'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'),
'express_ok' => __('物流轨迹已更新。', 'aksrt'),
'express_failed' => __('物流轨迹查询失败。', 'aksrt'),
];
if (!isset($map[$code])) {
return;
}
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
}
private function page_url(string $slug, array $args = []): string {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
private function render_pagination(int $pages, int $page_num): void {
if ($pages <= 1) {
return;
}
echo '<div class="tablenav"><div class="tablenav-pages">';
echo paginate_links([
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
'total' => $pages,
'current' => $page_num,
]);
echo '</div></div>';
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShop {
public static function build(): array {
return [
'key' => 'shop',
'title' => '商城&商品',
'version' => '1.0.0',
'sections' => [
'basic' => SchemaShopBasic::build(),
'list' => SchemaShopList::build(),
'contact' => SchemaShopContact::build(),
'single' => SchemaShopSingle::build(),
'product-defaults' => SchemaShopProductDefaults::build(),
'shipping-config' => SchemaShopShippingConfig::build(),
'timing' => SchemaShopTiming::build(),
],
];
}
}
@@ -1,37 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopBasic {
public static function build(): array {
return [
'key' => 'basic',
'title' => '商城基础',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'general', 'title' => '商城开关', 'fields' => [
['key' => 'shop_s', 'label' => '启用商城', 'type' => 'switch', 'default' => true],
['key' => 'shop_home_url', 'label' => '商城首页链接', 'type' => 'input', 'default' => ''],
]],
['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [
['key' => 'shop_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true],
['key' => 'shop_breadcrumbs_home', 'label' => '显示首页链接', 'type' => 'switch', 'default' => true],
['key' => 'shop_breadcrumbs_shop_home', 'label' => '显示商城首页', 'type' => 'switch', 'default' => false],
['key' => 'shop_breadcrumbs_shop_home_name', 'label' => '商城首页名称', 'type' => 'input', 'default' => '商城首页'],
]],
['key' => 'rewrite', 'title' => 'URL 别名', 'fields' => [
['key' => 'shop_product_rewrite_slug', 'label' => '商品 URL 别名', 'type' => 'input', 'default' => 'product'],
['key' => 'shop_cart_rewrite_slug', 'label' => '购物车 URL 别名', 'type' => 'input', 'default' => 'cart'],
['key' => 'shop_rewrite_suffix_html_s', 'label' => '.html 伪静态后缀', 'type' => 'switch', 'default' => true],
]],
],
];
}
}
@@ -1,30 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopContact {
public static function build(): array {
return [
'key' => 'contact',
'title' => '客服配置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'button', 'title' => '客服按钮', 'fields' => [
['key' => 'shop_contact_msg_s', 'label' => '显示客服按钮', 'type' => 'switch', 'default' => true],
['key' => 'shop_contact_name', 'label' => '按钮文案', 'type' => 'input', 'default' => '联系客服'],
['key' => 'shop_contact_note', 'label' => '客服备注', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'channels', 'title' => '联系方式', 'fields' => [
['key' => 'shop_contact_more', 'label' => '多客服联系方式', 'type' => 'textarea', 'default' => '', 'help' => '每行一个:名称|图标|链接|二维码图片 URL'],
]],
],
];
}
}
-51
View File
@@ -1,51 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopList {
public static function build(): array {
return [
'key' => 'list',
'title' => '商品列表',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'pagination', 'title' => '列表分页', 'fields' => [
['key' => 'shop_list_orderby', 'label' => '默认排序', 'type' => 'select', 'default' => 'date', 'options' => [
['label' => '最新发布', 'value' => 'date'],
['label' => '销量最高', 'value' => 'sales'],
['label' => '价格最低', 'value' => 'price_asc'],
['label' => '价格最高', 'value' => 'price_desc'],
['label' => '评价最多', 'value' => 'comments'],
]],
['key' => 'shop_list_count', 'label' => '每页数量', 'type' => 'number', 'default' => 12, 'min' => 4, 'max' => 100],
['key' => 'shop_list_paging', 'label' => '翻页模式', 'type' => 'radio', 'default' => 'number', 'options' => [
['label' => '数字翻页', 'value' => 'number'],
['label' => 'AJAX 加载', 'value' => 'ajax'],
]],
['key' => 'shop_list_ias_s', 'label' => '滚动自动加载', 'type' => 'switch', 'default' => false],
['key' => 'shop_list_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50],
]],
['key' => 'style', 'title' => '列表样式', 'fields' => [
['key' => 'shop_list_style', 'label' => '卡片样式', 'type' => 'select', 'default' => 'default', 'options' => [
['label' => '默认', 'value' => 'default'],
['label' => '竖向大卡片', 'value' => 'card-lg'],
['label' => '横向小卡片', 'value' => 'card-sm'],
]],
['key' => 'shop_sales_show', 'label' => '销量显示', 'type' => 'select', 'default' => 'show', 'options' => [
['label' => '显示', 'value' => 'show'],
['label' => '不显示', 'value' => 'hide'],
['label' => '超量显示', 'value' => 'threshold'],
]],
['key' => 'shop_sales_show_min', 'label' => '起显阈值', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 99999],
]],
],
];
}
}
@@ -1,46 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopProductDefaults {
public static function build(): array {
return [
'key' => 'product-defaults',
'title' => '商品默认参数',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'policy', 'title' => '购买策略', 'fields' => [
['key' => 'shop_limit_buy', 'label' => '默认限购', 'type' => 'number', 'default' => -1, 'min' => -1, 'max' => 9999, 'help' => '-1 不限,0 禁购,正数为限购数量'],
['key' => 'shop_guest_buy', 'label' => '免登录购买', 'type' => 'switch', 'default' => false],
['key' => 'shop_email_fill', 'label' => '邮箱必留', 'type' => 'switch', 'default' => false],
['key' => 'shop_rebate', 'label' => '允许返佣', 'type' => 'switch', 'default' => true],
]],
['key' => 'shipping', 'title' => '运费默认', 'fields' => [
['key' => 'shop_shipping_fee', 'label' => '默认运费类型', 'type' => 'select', 'default' => 'free', 'options' => [
['label' => '包邮', 'value' => 'free'],
['label' => '固定运费', 'value' => 'fixed'],
['label' => '按金额计算', 'value' => 'amount'],
]],
]],
['key' => 'afterSale', 'title' => '售后与服务', 'fields' => [
['key' => 'shop_after_sale', 'label' => '默认售后政策', 'type' => 'select', 'default' => 'refund_return', 'options' => [
['label' => '仅退款', 'value' => 'refund'],
['label' => '退货退款', 'value' => 'refund_return'],
['label' => '不支持售后', 'value' => 'none'],
]],
['key' => 'shop_service', 'label' => '服务保障', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'params', 'title' => '默认参数模板', 'fields' => [
['key' => 'shop_product_params_default', 'label' => '商品参数模板', 'type' => 'textarea', 'default' => ''],
]],
],
];
}
}
@@ -1,41 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopShippingConfig {
public static function build(): array {
return [
'key' => 'shipping-config',
'title' => '物流配置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'address', 'title' => '收货地址', 'fields' => [
['key' => 'shop_manual_address', 'label' => '手动填写收货地址', 'type' => 'switch', 'default' => false],
]],
['key' => 'express', 'title' => '快递查询', 'fields' => [
['key' => 'shop_express_api_sdk', 'label' => '快递查询接口', 'type' => 'select', 'default' => 'kuaidi100', 'options' => [
['label' => '快递100', 'value' => 'kuaidi100'],
['label' => '阿里云', 'value' => 'aliyun'],
['label' => '快递鸟', 'value' => 'kdniao'],
]],
['key' => 'shop_express_query_interval', 'label' => '查询刷新间隔(分钟)', 'type' => 'number', 'default' => 60, 'min' => 30, 'max' => 240],
]],
['key' => 'credentials', 'title' => '接口凭据', 'fields' => [
['key' => 'shop_express_kdniao_opt', 'label' => '快递鸟凭据', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_express_kuaidi100_opt', 'label' => '快递100凭据', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_express_aliyun_opt', 'label' => '阿里云凭据', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'companies', 'title' => '物流公司', 'fields' => [
['key' => 'shop_shipping_company', 'label' => '物流公司列表', 'type' => 'textarea', 'default' => ''],
]],
],
];
}
}
@@ -1,39 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopSingle {
public static function build(): array {
return [
'key' => 'single',
'title' => '商品详情',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'layout', 'title' => '详情页布局', 'fields' => [
['key' => 'shop_content_layout', 'label' => '内容布局', 'type' => 'select', 'default' => 'adapt', 'options' => [
['label' => '适应宽度', 'value' => 'adapt'],
['label' => '全屏', 'value' => 'full'],
['label' => '适应宽度+侧栏', 'value' => 'sidebar'],
]],
['key' => 'shop_content_show_bg', 'label' => '详情背景盒子', 'type' => 'switch', 'default' => false],
['key' => 'shop_single_tab', 'label' => '自定义 TAB 栏目', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_content_after', 'label' => '详情页底部内容', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'related', 'title' => '相关推荐', 'fields' => [
['key' => 'shop_single_related_s', 'label' => '显示相关推荐', 'type' => 'switch', 'default' => true],
['key' => 'shop_single_related_opt', 'label' => '推荐配置', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'image', 'title' => '商品图片', 'fields' => [
['key' => 'shop_main_image_default', 'label' => '默认商品主图', 'type' => 'media', 'default' => ''],
]],
],
];
}
}
@@ -1,38 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaShopTiming {
public static function build(): array {
return [
'key' => 'timing',
'title' => '时效与售后原因',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'receipt', 'title' => '确认收货', 'fields' => [
['key' => 'shop_order_receipt_max_day', 'label' => '自动确认收货(天)', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 60],
]],
['key' => 'comment', 'title' => '评价时效', 'fields' => [
['key' => 'shop_comment_max_day', 'label' => '评价时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 60],
['key' => 'shop_comment_placeholder', 'label' => '评价占位文案', 'type' => 'input', 'default' => ''],
['key' => 'shop_comment_img_num', 'label' => '评价图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 9],
]],
['key' => 'afterSale', 'title' => '售后退货', 'fields' => [
['key' => 'shop_after_sale_return_express_max_day', 'label' => '退货快递时效(天)', 'type' => 'number', 'default' => 7, 'min' => 1, 'max' => 30],
['key' => 'shop_after_sale_reason_refund', 'label' => '仅退款原因', 'type' => 'textarea', 'default' => ''],
['key' => 'shop_after_sale_reason_return', 'label' => '退货退款原因', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'author', 'title' => '商家信息', 'fields' => [
['key' => 'shop_author_show', 'label' => '显示商家信息', 'type' => 'switch', 'default' => true],
]],
],
];
}
}
-453
View File
@@ -1,453 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
class ShopController extends ControllerBase {
use ShopControllerProductAdminColumns;
use ShopControllerFrontendFlowOperations;
use ShopControllerProductOperations;
use ShopControllerOrderOperations;
use ShopControllerFulfillmentOperations;
use ShopControllerCouponGatewayOperations;
use ShopControllerVariantExportOperations;
private static $instance = null;
private $admin;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function set_admin(Admin $admin): void {
$this->admin = $admin;
}
public function register_routes(): void {
register_rest_route(self::API_NAMESPACE, '/shop/frontend/checkout/preview', [
'methods' => 'POST',
'callback' => [$this, 'preview_checkout'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_orders'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/pay', [
'methods' => 'POST',
'callback' => [$this, 'pay_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/cancel', [
'methods' => 'POST',
'callback' => [$this, 'cancel_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/after-sale', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_after_sale'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_after_sales'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P<id>\d+)/return-shipping', [
'methods' => 'POST',
'callback' => [$this, 'submit_frontend_return_shipping'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/(?P<id>\d+)/cancel', [
'methods' => 'POST',
'callback' => [$this, 'cancel_frontend_after_sale'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/coupons', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_coupons'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// ── Address book CRUD (frontend) ──
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_addresses'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/address/(?P<id>\d+)/default', [
'methods' => 'POST',
'callback' => [$this, 'set_default_frontend_address'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_product'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_frontend_product'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// Cart endpoints (§6.4)
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart', [
'methods' => 'GET',
'callback' => [$this, 'get_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/add', [
'methods' => 'POST',
'callback' => [$this, 'handle_add_to_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/reduce', [
'methods' => 'POST',
'callback' => [$this, 'handle_reduce_from_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove', [
'methods' => 'POST',
'callback' => [$this, 'handle_remove_from_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/remove-multi', [
'methods' => 'POST',
'callback' => [$this, 'handle_remove_multi_from_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/empty', [
'methods' => 'POST',
'callback' => [$this, 'handle_empty_cart'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/cart/count', [
'methods' => 'GET',
'callback' => [$this, 'get_cart_count'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// Shop overview / dashboard
register_rest_route(self::API_NAMESPACE, '/shop/overview', [
'methods' => 'GET',
'callback' => [$this, 'get_overview'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product taxonomy options used by the product editor.
register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [
'methods' => 'GET',
'callback' => [$this, 'get_product_taxonomy'],
'permission_callback' => [$this, 'check_permission'],
]);
// Create product category or tag term for the product editor.
register_rest_route(self::API_NAMESPACE, '/shop/products/taxonomy', [
'methods' => 'POST',
'callback' => [$this, 'create_product_taxonomy_term'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product list
register_rest_route(self::API_NAMESPACE, '/shop/products', [
'methods' => 'GET',
'callback' => [$this, 'get_products'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product create
register_rest_route(self::API_NAMESPACE, '/shop/products', [
'methods' => 'POST',
'callback' => [$this, 'create_product'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product update
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_product'],
'permission_callback' => [$this, 'check_permission'],
]);
// Product single read
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_product'],
'permission_callback' => [$this, 'check_permission'],
]);
// Order list
register_rest_route(self::API_NAMESPACE, '/shop/orders', [
'methods' => 'GET',
'callback' => [$this, 'get_orders'],
'permission_callback' => [$this, 'check_permission'],
]);
// Order single read
register_rest_route(self::API_NAMESPACE, '/shop/orders/(?P<order_number>[A-Za-z0-9_-]+)', [
'methods' => 'GET',
'callback' => [$this, 'get_order'],
'permission_callback' => [$this, 'check_permission'],
]);
// Order status update — canonical field is order_number
register_rest_route(self::API_NAMESPACE, '/shop/orders/update-status', [
'methods' => 'POST',
'callback' => [$this, 'update_order_status'],
'permission_callback' => [$this, 'check_permission'],
]);
// Gateway status
register_rest_route(self::API_NAMESPACE, '/shop/gateways', [
'methods' => 'GET',
'callback' => [$this, 'get_gateways'],
'permission_callback' => [$this, 'check_permission'],
]);
// Coupon list (admin)
register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
'methods' => 'GET',
'callback' => [$this, 'get_coupons'],
'permission_callback' => [$this, 'check_permission'],
]);
// Coupon create (admin)
register_rest_route(self::API_NAMESPACE, '/shop/coupons', [
'methods' => 'POST',
'callback' => [$this, 'create_coupon'],
'permission_callback' => [$this, 'check_permission'],
]);
// Coupon disable (admin)
register_rest_route(self::API_NAMESPACE, '/shop/coupons/(?P<id>\d+)/disable', [
'methods' => 'POST',
'callback' => [$this, 'disable_coupon'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── After Sale ──
// After-sale list (admin)
register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
'methods' => 'GET',
'callback' => [$this, 'get_after_sale_list'],
'permission_callback' => [$this, 'check_permission'],
]);
// After-sale create (admin)
register_rest_route(self::API_NAMESPACE, '/shop/after-sale', [
'methods' => 'POST',
'callback' => [$this, 'create_after_sale'],
'permission_callback' => [$this, 'check_permission'],
]);
// After-sale status update (admin)
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P<id>\d+)/update-status', [
'methods' => 'POST',
'callback' => [$this, 'update_after_sale_status'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_after_sale_detail'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── Shipping ──
// Shipping list (admin)
register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
'methods' => 'GET',
'callback' => [$this, 'get_shipping_list'],
'permission_callback' => [$this, 'check_permission'],
]);
// Shipping create (admin)
register_rest_route(self::API_NAMESPACE, '/shop/shipping', [
'methods' => 'POST',
'callback' => [$this, 'create_shipping'],
'permission_callback' => [$this, 'check_permission'],
]);
// Shipping status update (admin)
register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P<id>\d+)/update-status', [
'methods' => 'POST',
'callback' => [$this, 'update_shipping_status'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/shipping/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_shipping_detail'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/shipping/batch-update-status', [
'methods' => 'POST',
'callback' => [$this, 'batch_update_shipping_status'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── R68: Order export + confirm receipt + after-sale reasons ──
register_rest_route(self::API_NAMESPACE, '/shop/orders/export', [
'methods' => 'GET',
'callback' => [$this, 'export_orders'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── R69: Frontend after-sale reasons (§6.8) ──
register_rest_route(self::API_NAMESPACE, '/shop/frontend/after-sale/reasons', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_after_sale_reasons'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/confirm-receipt', [
'methods' => 'POST',
'callback' => [$this, 'confirm_receipt'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/rebuy', [
'methods' => 'POST',
'callback' => [$this, 'rebuy_frontend_order'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/archive', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_product_archive'],
'permission_callback' => '__return_true',
]);
// Product list widget AJAX (zibll ajax_widget_ui product lists).
register_rest_route(self::API_NAMESPACE, '/shop/frontend/widgets/products', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_widget_products'],
'permission_callback' => '__return_true',
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_revenue'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/revenue/withdraw', [
'methods' => 'POST',
'callback' => [$this, 'create_frontend_withdraw'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/review', [
'methods' => 'POST',
'callback' => [$this, 'submit_frontend_product_review'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/orders/(?P<order_number>[A-Za-z0-9_-]+)/review-modal', [
'methods' => 'GET',
'callback' => [$this, 'get_frontend_product_review_modal'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/frontend/products/(?P<id>\d+)/favorite', [
'methods' => 'POST',
'callback' => [$this, 'toggle_frontend_product_favorite'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/shop/after-sale/reasons', [
'methods' => 'GET',
'callback' => [$this, 'get_after_sale_reasons'],
'permission_callback' => [$this, 'check_permission'],
]);
// ── Product Variants / SKU ──
// List variants for a product
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)/variants', [
'methods' => 'GET',
'callback' => [$this, 'get_product_variants'],
'permission_callback' => [$this, 'check_permission'],
]);
// Save variants for a product (replace all)
register_rest_route(self::API_NAMESPACE, '/shop/products/(?P<id>\d+)/variants', [
'methods' => 'POST',
'callback' => [$this, 'save_product_variants'],
'permission_callback' => [$this, 'check_permission'],
]);
// Delete a variant
register_rest_route(self::API_NAMESPACE, '/shop/variants/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_variant'],
'permission_callback' => [$this, 'check_permission'],
]);
}
public function check_logged_in_permission(): bool {
return is_user_logged_in();
}
}
@@ -1,97 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerCouponGatewayOperations {
/**
* GET /shop/coupons — 优惠券列表
*/
public function get_coupons(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$search = sanitize_text_field((string) $request->get_param('search'));
$result = \AKSRT\Payment\Coupon::get_instance()->get_list([
'status' => $status,
'search' => $search,
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
]);
return $this->success(['data' => $result]);
}
/**
* POST /shop/coupons — 创建优惠券
*/
public function create_coupon(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->error('优惠券系统未启用', 500);
}
$params = $this->get_json_params($request);
$code = sanitize_text_field((string) ($params['code'] ?? ''));
if ($code === '') {
return $this->error('优惠码不能为空');
}
$id = \AKSRT\Payment\Coupon::get_instance()->create($params);
if ($id > 0) {
return $this->success([
'message' => __('优惠券已创建', 'aksrt'),
'data' => ['id' => $id],
], 201);
}
return $this->error(__('创建失败(可能优惠码已存在)', 'aksrt'));
}
/**
* POST /shop/coupons/{id}/disable — 禁用优惠券
*/
public function disable_coupon(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->error('优惠券系统未启用', 500);
}
$id = (int) $request->get_param('id');
if ($id <= 0) {
return $this->error('参数无效');
}
$success = \AKSRT\Payment\Coupon::get_instance()->disable($id);
if ($success) {
return $this->success(['message' => __('优惠券已禁用', 'aksrt')]);
}
return $this->error(__('操作失败', 'aksrt'));
}
/**
* GET /shop/gateways — 支付网关状态列表
*/
public function get_gateways(): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Gateway')) {
return $this->success(['data' => ['gateways' => []]]);
}
return $this->success([
'data' => [
'gateways' => \AKSRT\Payment\Gateway::get_instance()->get_status_summary(),
],
]);
}
}
@@ -1,717 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerFrontendFlowOperations {
public function check_logged_in_permission(): bool {
return is_user_logged_in();
}
public function preview_checkout(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$result = \AKSRT\Shop\Shop::get_instance()->preview_checkout(get_current_user_id(), $this->get_json_params($request));
if (empty($result['success'])) {
return $this->error($result['message'] ?? '结算预览失败');
}
return $this->success(['data' => $result['data']]);
}
public function create_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$result = \AKSRT\Shop\Shop::get_instance()->create_checkout_order(get_current_user_id(), $this->get_json_params($request));
if (empty($result['success'])) {
return $this->error($result['message'] ?? '订单创建失败');
}
return $this->success([
'message' => __('订单已创建', 'aksrt'),
'data' => $result['data'],
], 201);
}
public function get_frontend_orders(\WP_REST_Request $request): \WP_REST_Response {
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$product_type = sanitize_key((string) $request->get_param('product_type'));
$args = [
'page' => $page,
'per_page' => $per_page,
'status' => $status,
'product_type' => $product_type,
];
if (class_exists('\AKSRT\Payment\UserOrders')) {
$view = \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view(get_current_user_id(), $args);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$view = \AKSRT\Shop\Shop::get_instance()->get_order_center_view(get_current_user_id(), $args);
} else {
$view = ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1];
}
return $this->success(['data' => $view]);
}
public function get_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$order = null;
if (class_exists('\AKSRT\Payment\UserOrders')) {
$order = \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
}
if (!$order) {
return $this->error('订单不存在', 404);
}
return $this->success(['data' => $order]);
}
public function pay_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$payment_method = sanitize_key((string) ($params['payment_method'] ?? ''));
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (class_exists('\AKSRT\Payment\UserOrders')) {
$result = \AKSRT\Payment\UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method ?: 'balance');
} else {
return $this->error('订单服务不可用', 500);
}
if (empty($result['success'])) {
return $this->error($result['message'] ?? '支付失败');
}
return $this->success(['data' => $result['data']]);
}
public function cancel_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (class_exists('\AKSRT\Payment\UserOrders')) {
$result = \AKSRT\Payment\UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number);
} else {
return $this->error('订单服务不可用', 500);
}
if (empty($result['success'])) {
return $this->error($result['message'] ?? '取消订单失败');
}
return $this->success(['data' => $result['data']]);
}
public function create_frontend_after_sale(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$params = $this->get_json_params($request);
$params['order_number'] = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\Shop::get_instance()->create_after_sale_request(get_current_user_id(), $params);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '提交售后失败');
}
return $this->success([
'message' => __('售后申请已提交', 'aksrt'),
'data' => $result['data'],
], 201);
}
public function get_frontend_after_sales(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$data = \AKSRT\Shop\Shop::get_instance()->get_user_after_sales(get_current_user_id());
return $this->success(['data' => $data]);
}
public function submit_frontend_return_shipping(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$result = \AKSRT\Shop\Shop::get_instance()->submit_after_sale_return(get_current_user_id(), $id, $params);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '提交退货物流失败');
}
return $this->success([
'message' => $result['message'] ?? __('退货物流已提交', 'aksrt'),
'data' => $result['data'] ?? [],
]);
}
public function cancel_frontend_after_sale(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$reason = sanitize_text_field((string) ($params['reason'] ?? ''));
$result = \AKSRT\Shop\Shop::get_instance()->cancel_after_sale(get_current_user_id(), $id, $reason);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '取消售后失败');
}
return $this->success(['message' => $result['message'] ?? __('已取消', 'aksrt')]);
}
/**
* Active public coupons for wallet UI (not personal inventory — site-wide active codes).
*/
public function get_frontend_coupons(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Coupon')) {
return $this->error('优惠券不可用', 500);
}
$list = \AKSRT\Payment\Coupon::get_instance()->get_list([
'status' => 'active',
'limit' => 50,
'offset' => 0,
]);
$user_id = get_current_user_id();
$items = [];
foreach ((array) ($list['items'] ?? []) as $c) {
$items[] = [
'id' => (int) ($c['id'] ?? 0),
'code' => (string) ($c['code'] ?? ''),
'type' => (string) ($c['type'] ?? ''),
'value' => (float) ($c['value'] ?? 0),
'min_amount' => (float) ($c['min_amount'] ?? 0),
'max_discount' => (float) ($c['max_discount'] ?? 0),
'expires_at' => (string) ($c['expires_at'] ?? ''),
'user_used' => $user_id > 0
? (int) \AKSRT\Payment\Coupon::get_instance()->get_user_usage_count((int) ($c['id'] ?? 0), $user_id)
: 0,
'per_user_limit' => (int) ($c['per_user_limit'] ?? 0),
];
}
return $this->success(['data' => ['items' => $items]]);
}
public function get_cart(\WP_REST_Request $request): \WP_REST_Response {
$user_id = get_current_user_id();
$shop = \AKSRT\Shop\Shop::get_instance();
$cart = $shop->get_cart($user_id);
$items = [];
foreach ($cart as $pid => $opts_map) {
$product_id = (int) $pid;
$product = $shop->build_product_card_view($product_id);
if (empty($product) || !is_array($opts_map)) {
continue;
}
foreach ($opts_map as $opt_str => $qty) {
$options_string = $opt_str === '_default' ? '' : (string) $opt_str;
$unit_price = method_exists($shop, 'resolve_option_price')
? (float) $shop->resolve_option_price($product_id, $options_string !== '' ? $options_string : '_default')
: (float) ($product['price'] ?? 0);
$options_label = method_exists($shop, 'resolve_option_labels')
? (string) $shop->resolve_option_labels($product_id, $options_string !== '' ? $options_string : '_default')
: '';
$option_image = method_exists($shop, 'get_option_image')
? (string) $shop->get_option_image($product_id, $options_string !== '' ? $options_string : '_default')
: '';
$stock = method_exists($shop, 'resolve_option_stock')
? (int) $shop->resolve_option_stock($product_id, $options_string !== '' ? $options_string : '_default')
: (int) ($product['stock'] ?? 0);
$items[] = [
'product_id' => $product_id,
'options_string' => $options_string,
'options_label' => $options_label,
'quantity' => (int) $qty,
'unit_price' => $unit_price,
'line_total' => round($unit_price * (int) $qty, 2),
'stock' => $stock,
'image' => $option_image !== '' ? $option_image : (string) ($product['image'] ?? ''),
'title' => (string) ($product['title'] ?? ''),
'product' => $product,
];
}
}
return $this->success([
'data' => [
'items' => $items,
'count' => $shop->get_cart_count($user_id),
],
]);
}
public function get_cart_count(\WP_REST_Request $request): \WP_REST_Response {
$user_id = get_current_user_id();
return $this->success(['data' => ['count' => \AKSRT\Shop\Shop::get_instance()->get_cart_count($user_id)]]);
}
public function handle_add_to_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$product_id = (int) ($params['product_id'] ?? 0);
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
$count = max(1, (int) ($params['count'] ?? 1));
$result = \AKSRT\Shop\Shop::get_instance()->add_to_cart(get_current_user_id(), $product_id, $options_string, $count);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_reduce_from_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$product_id = (int) ($params['product_id'] ?? 0);
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
$count = max(1, (int) ($params['count'] ?? 1));
$result = \AKSRT\Shop\Shop::get_instance()->reduce_from_cart(get_current_user_id(), $product_id, $options_string, $count);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_remove_from_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$product_id = (int) ($params['product_id'] ?? 0);
$options_string = sanitize_text_field((string) ($params['options_string'] ?? ''));
$result = \AKSRT\Shop\Shop::get_instance()->remove_from_cart(get_current_user_id(), $product_id, $options_string);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_remove_multi_from_cart(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$items = is_array($params['items'] ?? null) ? $params['items'] : [];
$result = \AKSRT\Shop\Shop::get_instance()->remove_multi_from_cart(get_current_user_id(), $items);
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function handle_empty_cart(\WP_REST_Request $request): \WP_REST_Response {
$result = \AKSRT\Shop\Shop::get_instance()->empty_cart(get_current_user_id());
if (isset($result['error'])) {
return $this->error($result['error']);
}
return $this->success(['data' => $result]);
}
public function toggle_frontend_product_favorite(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$product_id = (int) $request->get_param('id');
$result = \AKSRT\Shop\Shop::get_instance()->toggle_favorite(get_current_user_id(), $product_id);
if (empty($result['success'])) {
return $this->error(__('收藏操作失败', 'aksrt'));
}
return $this->success([
'message' => !empty($result['is_favorite']) ? __('已收藏', 'aksrt') : __('已取消收藏', 'aksrt'),
'data' => $result,
]);
}
public function rebuy_frontend_order(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\Shop::get_instance()->re_purchase(get_current_user_id(), $order_number);
if (empty($result['success'])) {
return $this->error($result['message'] ?? __('再次购买失败', 'aksrt'));
}
return $this->success([
'message' => __('商品已加入购物车', 'aksrt'),
'data' => $result,
]);
}
public function get_frontend_addresses(): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->success(['data' => ['addresses' => []]]);
}
$addresses = \AKSRT\Shop\Shop::get_instance()->get_address_book(get_current_user_id());
return $this->success(['data' => ['addresses' => $addresses]]);
}
public function create_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$params = $this->get_json_params($request);
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->add_address($user_id, $params);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '添加地址失败');
}
return $this->success([
'message' => __('地址已添加', 'aksrt'),
'data' => ['address' => $result['address']],
], 201);
}
public function update_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$address_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->update_address($user_id, $address_id, $params);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '更新地址失败');
}
return $this->success([
'message' => __('地址已更新', 'aksrt'),
'data' => ['address' => $result['address']],
]);
}
public function delete_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$address_id = (int) $request->get_param('id');
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->delete_address($user_id, $address_id);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '删除地址失败');
}
return $this->success(['message' => __('地址已删除', 'aksrt')]);
}
public function set_default_frontend_address(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$address_id = (int) $request->get_param('id');
$user_id = get_current_user_id();
$result = \AKSRT\Shop\Shop::get_instance()->set_default_address($user_id, $address_id);
if (empty($result['success'])) {
return $this->error($result['error'] ?? '设置默认地址失败');
}
return $this->success(['message' => __('已设为默认地址', 'aksrt')]);
}
public function get_frontend_product_archive(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$taxonomy = sanitize_key((string) ($request->get_param('taxonomy') ?? ''));
$term_id = (int) ($request->get_param('term_id') ?? 0);
$page = max(1, (int) ($request->get_param('page') ?? 1));
$orderby = sanitize_key((string) ($request->get_param('orderby') ?? ''));
$order = sanitize_key((string) ($request->get_param('order') ?? ''));
$view = \AKSRT\Shop\Shop::get_instance()->get_product_archive_view([
'taxonomy' => $taxonomy,
'term_id' => $term_id,
'paged' => $page,
'orderby' => $orderby,
'order' => $order,
]);
$sales_cfg = is_array($view['sales'] ?? null) ? $view['sales'] : [];
$html = '';
if (!empty($view['products']) && is_array($view['products'])) {
ob_start();
foreach ($view['products'] as $product) {
if (function_exists('get_template_part')) {
get_template_part('templates/parts/shop/product', 'card', [
'product' => $product,
'sales_cfg' => $sales_cfg,
]);
}
}
$html = (string) ob_get_clean();
}
$empty_html = '';
if ($html === '' && function_exists('aksrt_get_ajax_null')) {
$empty_html = aksrt_get_ajax_null(__('内容空空如也', 'aksrt'));
} elseif ($html === '') {
$empty_html = '<div class="aksrt-product-archive__empty ajax-item"><p>' . esc_html__('内容空空如也', 'aksrt') . '</p></div>';
}
// Header HTML for cat filter replace (when filter present).
$header_html = '';
if (!empty($view['filter']) && is_array($view['filter'])) {
ob_start();
$this->render_product_cat_filter_html($view);
$header_html = (string) ob_get_clean();
}
$paginate_html = '';
$page_n = (int) ($view['page'] ?? 1);
$max_pages = (int) ($view['max_pages'] ?? 1);
$has_more = $page_n < $max_pages;
if ($has_more && (string) ($view['paging'] ?? '') === 'ajax') {
$next_url = add_query_arg([
'taxonomy' => $taxonomy,
'term_id' => $term_id,
'page' => $page_n + 1,
'orderby' => (string) ($view['orderby'] ?? 'date'),
'order' => (string) ($view['order'] ?? ''),
], rest_url('aksrt/v1/shop/frontend/products/archive'));
if (function_exists('aksrt_get_ajax_next_paginate')) {
$paginate_html = aksrt_get_ajax_next_paginate(
(int) ($view['total'] ?? 0),
$page_n,
max(1, (int) ($view['per_page'] ?? 12)),
$next_url
);
}
}
return $this->success([
'data' => array_merge($view, [
'html' => $html !== '' ? $html : $empty_html,
'empty_html' => $empty_html,
'header_html' => $header_html,
'paginate_html' => $paginate_html,
'has_more' => $has_more,
'title' => !empty($view['term']['name'])
? (string) $view['term']['name'] . ' - ' . wp_strip_all_tags(get_bloginfo('name'))
: '',
]),
]);
}
/**
* GET /shop/frontend/widgets/products — product list widget AJAX pager.
*/
public function get_frontend_widget_products(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Widgets\WidgetProductList')) {
return $this->error('商品组件不可用', 500);
}
$instance = [
'per_page' => max(1, min(50, (int) ($request->get_param('per_page') ?? 8))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'order' => sanitize_key((string) ($request->get_param('order') ?? 'DESC')),
'cat_ids' => sanitize_text_field((string) ($request->get_param('cat_ids') ?? '')),
'tag_ids' => sanitize_text_field((string) ($request->get_param('tag_ids') ?? '')),
'discount_ids' => sanitize_text_field((string) ($request->get_param('discount_ids') ?? '')),
'exclude_cat' => !empty($request->get_param('exclude_cat')) ? 1 : 0,
'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')),
'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')),
];
$page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1));
$payload = \AKSRT\Widgets\WidgetProductList::query_payload($instance, $page);
return $this->success([
'html' => (string) ($payload['html'] ?? ''),
'paginate_html' => (string) ($payload['paginate_html'] ?? ''),
'has_more' => !empty($payload['has_more']),
'page' => (int) ($payload['page'] ?? $page),
'max_pages' => (int) ($payload['max_pages'] ?? 1),
'total' => (int) ($payload['total'] ?? 0),
]);
}
/**
* Render cat filter bar HTML for AJAX header replace.
*/
private function render_product_cat_filter_html(array $view): void {
$filter = is_array($view['filter'] ?? null) ? $view['filter'] : [];
$orderby = (string) ($view['orderby'] ?? ($filter['orderby'] ?? 'date'));
$order = strtoupper((string) ($view['order'] ?? ($filter['order'] ?? 'DESC')));
$term = is_array($view['term'] ?? null) ? $view['term'] : [];
$current_id = (int) ($filter['current_id'] ?? ($term['id'] ?? 0));
include get_template_directory() . '/templates/parts/shop/product-cat-filter.php';
}
public function get_frontend_revenue(\WP_REST_Request $request): \WP_REST_Response {
if (!function_exists('aksrt_get_revenue_page_view')) {
return $this->error('收益服务不可用', 500);
}
$view = aksrt_get_revenue_page_view(get_current_user_id());
$withdrawals = [];
if (class_exists('\AKSRT\Payment\Withdrawal')) {
$rows = \AKSRT\Payment\Withdrawal::get_instance()->get_user_withdrawals(get_current_user_id(), ['limit' => 20]);
foreach ($rows as $row) {
$withdrawals[] = [
'id' => (int) ($row['id'] ?? 0),
'amount' => (float) ($row['amount'] ?? 0),
'status' => (string) ($row['status'] ?? ''),
'method' => (string) ($row['method'] ?? ''),
'created_at' => (string) ($row['created_at'] ?? ''),
'note' => (string) ($row['note'] ?? ''),
];
}
}
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : [];
return $this->success([
'data' => array_merge($view, [
'withdrawals' => $withdrawals,
'settings' => [
'min_amount' => (float) ($settings['min_amount'] ?? 50),
'service_charge_rate' => (float) ($settings['service_charge_rate'] ?? 0),
'text' => (string) ($settings['text'] ?? ''),
],
]),
]);
}
public function create_frontend_withdraw(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Withdrawal') || !class_exists('\AKSRT\Credits\Credits')) {
return $this->error('提现服务不可用', 500);
}
$params = $this->get_json_params($request);
$amount = (float) ($params['amount'] ?? 0);
$method = sanitize_key((string) ($params['method'] ?? 'alipay'));
$account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? ''));
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : [];
$min = (float) ($settings['min_amount'] ?? 50);
if ($amount < $min) {
return $this->error(sprintf(__('最低提现金额为 %s', 'aksrt'), $min));
}
if ($account === '') {
return $this->error(__('请填写收款账户信息', 'aksrt'));
}
$user_id = get_current_user_id();
$balance = (float) \AKSRT\Credits\Credits::balance($user_id);
if ($amount > $balance) {
return $this->error(__('可提现余额不足', 'aksrt'));
}
$cost = (int) round($amount);
if ($cost <= 0) {
return $this->error(__('提现金额无效', 'aksrt'));
}
$deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'withdraw', ['type' => 'withdraw'], __('用户提现申请', 'aksrt'));
if (is_wp_error($deducted)) {
return $this->error($deducted->get_error_message());
}
$id = \AKSRT\Payment\Withdrawal::get_instance()->create($user_id, $amount, $method, $account);
if ($id <= 0) {
\AKSRT\Credits\Credits::add($user_id, $cost, 'withdraw_refund', ['type' => 'withdraw'], __('提现失败退回', 'aksrt'));
return $this->error(__('提现申请创建失败', 'aksrt'), 500);
}
return $this->success([
'message' => __('提现申请已提交', 'aksrt'),
'data' => [
'id' => $id,
'amount' => $amount,
'balance' => (float) \AKSRT\Credits\Credits::balance($user_id),
],
], 201);
}
public function submit_frontend_product_review(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\ProductReview')) {
return $this->error('评价服务不可用', 500);
}
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$product_id = (int) ($params['product_id'] ?? 0);
$result = \AKSRT\Shop\ProductReview::get_instance()->submit_review(
get_current_user_id(),
$order_number,
$product_id,
$params
);
if (empty($result['success'])) {
return $this->error($result['message'] ?? __('评价失败', 'aksrt'));
}
return $this->success([
'message' => __('评价成功', 'aksrt'),
'data' => $result,
], 201);
}
/**
* GET review modal HTML for an order (zibll order_comment_modal parity).
*/
public function get_frontend_product_review_modal(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\ProductReview')) {
return $this->error(__('评价服务不可用', 'aksrt'), 500);
}
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\ProductReview::get_instance()->get_review_modal_html(
get_current_user_id(),
$order_number
);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('无法打开评价', 'aksrt')));
}
return $this->success([
'html' => (string) ($result['html'] ?? ''),
'product_id' => (int) ($result['product_id'] ?? 0),
'need_shipping' => !empty($result['need_shipping']),
'img_max' => (int) ($result['img_max'] ?? 0),
'remain_seconds' => (int) ($result['remain_seconds'] ?? 0),
]);
}
public function create_frontend_product(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$params['author_id'] = get_current_user_id();
$frontend_request = new \WP_REST_Request('POST', '/');
$frontend_request->set_body(wp_json_encode($params));
return $this->create_product($frontend_request);
}
public function update_frontend_product(\WP_REST_Request $request): \WP_REST_Response {
$id = (int) $request->get_param('id');
$post = get_post($id);
if (!$post || $post->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
if ((int) $post->post_author !== get_current_user_id() && !current_user_can('manage_options')) {
return $this->error('没有权限编辑该商品', 403);
}
return $this->update_product($request);
}
}
@@ -1,353 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerFulfillmentOperations {
/**
* GET /shop/after-sale — 售后工单列表
*/
public function get_after_sale_list(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$stage = sanitize_key((string) $request->get_param('stage'));
$order = sanitize_text_field((string) $request->get_param('order_number'));
$type = sanitize_key((string) $request->get_param('type'));
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$result = \AKSRT\Payment\AfterSale::get_instance()->get_list([
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
'status' => $status,
'stage' => $stage,
'order_number' => $order,
'type' => $type,
'date_from' => $date_from,
'date_to' => $date_to,
'include_order_context' => true,
]);
$stats = \AKSRT\Payment\AfterSale::get_instance()->get_stats();
return $this->success(['data' => array_merge($result, [
'stats' => $stats,
'filters' => [
'status' => $status,
'stage' => $stage,
'order_number' => $order,
'type' => $type,
'date_from' => $date_from,
'date_to' => $date_to,
],
'options' => [
'status' => \AKSRT\Payment\AfterSale::get_status_options(),
'stage' => \AKSRT\Payment\AfterSale::get_stage_options(),
'type' => \AKSRT\Payment\AfterSale::get_type_options(),
],
])]);
}
/**
* POST /shop/after-sale — 创建售后工单
*/
public function create_after_sale(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->error('售后系统未启用', 500);
}
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
if ($order_number === '' || $reason === '') {
return $this->error('订单号和原因不能为空');
}
$order = class_exists('\AKSRT\Payment\OrderRepository')
? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number)
: null;
if (!$order) {
return $this->error(__('订单不存在', 'aksrt'), 404);
}
unset($params['user_id'], $params['applicant_user_id']);
$id = \AKSRT\Payment\AfterSale::get_instance()->create($params);
if (is_wp_error($id)) {
return $this->error($id->get_error_message(), 422);
}
if ($id > 0) {
return $this->success([
'message' => __('售后工单已创建', 'aksrt'),
'data' => ['id' => $id],
], 201);
}
return $this->error(__('创建失败', 'aksrt'));
}
/**
* POST /shop/after-sale/{id}/update-status — 更新售后工单状态
*/
public function update_after_sale_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->error('售后系统未启用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$new_status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['operator_note'] ?? ''));
if ($id <= 0 || $new_status === '') {
return $this->error('参数无效');
}
$success = \AKSRT\Payment\AfterSale::get_instance()->update_status(
$id,
$new_status,
get_current_user_id(),
$note
);
if ($success) {
return $this->success(['message' => __('状态已更新', 'aksrt')]);
}
return $this->error(__('更新失败', 'aksrt'));
}
/**
* GET /shop/shipping — 发货记录列表
*/
public function get_shipping_list(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) $request->get_param('per_page'))) ?: 20;
$status = sanitize_key((string) $request->get_param('status'));
$order = sanitize_text_field((string) $request->get_param('order_number'));
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$tracking_no = sanitize_text_field((string) $request->get_param('tracking_no'));
$result = \AKSRT\Payment\Shipping::get_instance()->get_list([
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
'status' => $status,
'order_number' => $order,
'date_from' => $date_from,
'date_to' => $date_to,
'tracking_no' => $tracking_no,
'include_order_context' => true,
]);
$stats = \AKSRT\Payment\Shipping::get_instance()->get_stats();
return $this->success(['data' => array_merge($result, [
'stats' => $stats,
'filters' => [
'status' => $status,
'order_number' => $order,
'date_from' => $date_from,
'date_to' => $date_to,
'tracking_no' => $tracking_no,
],
'options' => [
'status' => \AKSRT\Payment\Shipping::get_status_options(),
'carrier' => \AKSRT\Payment\Shipping::get_carrier_options(),
],
])]);
}
/**
* POST /shop/shipping — 创建发货记录
*/
public function create_shipping(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
$carrier = sanitize_text_field((string) ($params['carrier'] ?? ''));
$tracking_no = sanitize_text_field((string) ($params['tracking_no'] ?? ''));
if ($order_number === '' || $carrier === '' || $tracking_no === '') {
return $this->error('订单号、快递公司和物流单号不能为空');
}
$order = class_exists('\AKSRT\Payment\OrderRepository')
? \AKSRT\Payment\OrderRepository::get_instance()->get($order_number)
: null;
if (!$order) {
return $this->error(__('订单不存在', 'aksrt'), 404);
}
$params['operator_id'] = get_current_user_id();
$id = \AKSRT\Payment\Shipping::get_instance()->create($params);
if ($id > 0) {
return $this->success([
'message' => __('发货记录已创建', 'aksrt'),
'data' => ['id' => $id],
], 201);
}
return $this->error(__('创建失败', 'aksrt'));
}
/**
* POST /shop/shipping/{id}/update-status — 更新发货状态
*/
public function update_shipping_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$new_status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
if ($id <= 0 || $new_status === '') {
return $this->error('参数无效');
}
$success = \AKSRT\Payment\Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id());
if ($success) {
return $this->success(['message' => __('状态已更新', 'aksrt')]);
}
return $this->error(__('更新失败', 'aksrt'));
}
public function get_after_sale_detail(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\AfterSale')) {
return $this->error('售后系统未启用', 500);
}
$id = (int) $request->get_param('id');
if ($id <= 0) {
return $this->error('参数无效');
}
$record = \AKSRT\Payment\AfterSale::get_instance()->get_with_order_context($id);
if (!$record) {
return $this->error(__('售后工单不存在', 'aksrt'), 404);
}
return $this->success([
'data' => [
'record' => $record,
'timeline' => $this->build_after_sale_timeline($record),
],
]);
}
public function get_shipping_detail(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$id = (int) $request->get_param('id');
if ($id <= 0) {
return $this->error('参数无效');
}
$record = \AKSRT\Payment\Shipping::get_instance()->get_with_order_context($id);
if (!$record) {
return $this->error(__('发货记录不存在', 'aksrt'), 404);
}
return $this->success([
'data' => [
'record' => $record,
'timeline' => $this->build_shipping_timeline($record),
],
]);
}
public function batch_update_shipping_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Shipping')) {
return $this->error('物流系统未启用', 500);
}
$params = $this->get_json_params($request);
$ids = is_array($params['ids'] ?? null) ? $params['ids'] : [];
$status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
if (empty($ids) || $status === '') {
return $this->error('参数无效');
}
$result = \AKSRT\Payment\Shipping::get_instance()->batch_update_status($ids, $status, $note, get_current_user_id());
return $this->success([
'message' => __('批量状态更新已执行', 'aksrt'),
'data' => $result,
]);
}
private function build_shipping_timeline(array $record): array {
$timeline = [];
foreach ([
'created_at' => __('创建发货记录', 'aksrt'),
'updated_at' => __('更新物流状态', 'aksrt'),
'delivered_at' => __('确认签收', 'aksrt'),
] as $field => $label) {
if (!empty($record[$field])) {
$timeline[] = [
'time' => (string) $record[$field],
'label' => $label,
];
}
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
});
return $timeline;
}
private function build_after_sale_timeline(array $record): array {
$timeline = [];
foreach ([
'created_at' => __('提交售后申请', 'aksrt'),
'updated_at' => __('更新售后状态', 'aksrt'),
'resolved_at' => __('售后处理结束', 'aksrt'),
] as $field => $label) {
if (!empty($record[$field])) {
$timeline[] = [
'time' => (string) $record[$field],
'label' => $label,
];
}
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
});
return $timeline;
}
}
@@ -1,136 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerOrderOperations {
/**
* GET /shop/orders — 订单列表,支持 search、status、page、per_page
*/
public function get_orders(\WP_REST_Request $request): \WP_REST_Response {
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
$search = sanitize_text_field((string) $request->get_param('search'));
$status = sanitize_key((string) $request->get_param('status'));
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$args = [
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
];
if ($status !== '' && $status !== 'all') {
$args['status'] = $status;
}
$orders = $repo->get_orders_admin($args, $search);
$total = $repo->get_orders_count($args, $search);
return $this->success([
'data' => [
'items' => $orders,
'total' => $total,
'page' => $page,
'total_pages' => max(1, (int) ceil($total / $per_page)),
],
]);
}
return $this->error('订单仓库不可用', 500);
}
/**
* GET /shop/orders/{order_number} — 单个订单详情(含上下文)
*/
public function get_order(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
return $this->error('订单服务不可用', 500);
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
if (!$order) {
return $this->error('订单不存在', 404);
}
$context = $repo->get_admin_order_context($order_number);
$timeline = [];
if (class_exists('\AKSRT\Shop\Shop')) {
$shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : [];
$after_sale_result = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20]) : ['items' => []];
$after_sale_items = $after_sale_result['items'] ?? [];
$timeline = [];
$date_fields = [
'created_at' => '订单创建',
'paid_at' => '完成支付',
];
foreach ($date_fields as $field => $label) {
if (!empty($order[$field])) {
$timeline[] = ['time' => $order[$field], 'title' => $label, 'type' => 'order'];
}
}
foreach ($shipping_records as $record) {
$timeline[] = [
'time' => $record['created_at'] ?? '',
'title' => '物流更新:' . ($record['status_label'] ?? $record['status'] ?? ''),
'detail' => ($record['carrier_label'] ?? '') . ' / ' . ($record['tracking_no'] ?? ''),
'type' => 'shipping',
];
}
foreach ($after_sale_items as $record) {
$timeline[] = [
'time' => $record['created_at'] ?? '',
'title' => '售后:' . ($record['type_label'] ?? '') . ' — ' . ($record['status_label'] ?? ''),
'detail' => $record['reason'] ?? '',
'type' => 'after_sale',
];
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($b['time'] ?? ''), (string) ($a['time'] ?? ''));
});
}
return $this->success([
'data' => $order,
'context' => $context,
'timeline' => $timeline,
]);
}
/**
* POST /shop/orders/update-status — 更新订单状态
* Canonical field: order_number (前端必须发送 order_number)
*/
public function update_order_status(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$order_number = sanitize_text_field((string) ($params['order_number'] ?? ''));
$new_status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
if ($order_number === '' || $new_status === '') {
return $this->error('参数无效');
}
$success = false;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$success = \AKSRT\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status, [
'note' => $note,
'force' => true,
]);
}
if ($success) {
return $this->success(['message' => __('订单状态已更新', 'aksrt')]);
}
return $this->error(__('更新失败', 'aksrt'));
}
}
@@ -1,187 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerProductAdminColumns {
/**
* Register custom columns for the aksrt_product admin list table.
*/
public function init_product_admin_columns(): void {
add_filter('manage_edit-aksrt_product_columns', [$this, 'product_columns']);
add_action('manage_aksrt_product_posts_custom_column', [$this, 'render_product_column'], 10, 2);
add_filter('manage_edit-aksrt_product_sortable_columns', [$this, 'sortable_product_columns']);
add_action('quick_edit_custom_box', [$this, 'render_product_quick_edit'], 10, 2);
add_action('bulk_edit_custom_box', [$this, 'render_product_quick_edit'], 10, 2);
add_action('save_post_aksrt_product', [$this, 'save_product_quick_edit'], 10, 2);
add_action('admin_footer-edit.php', [$this, 'product_quick_edit_script']);
}
public function product_columns(array $columns): array {
$new = [];
foreach ($columns as $key => $label) {
$new[$key] = $label;
if ($key === 'title') {
$new['product_price'] = __('价格', 'aksrt');
$new['product_stock'] = __('库存', 'aksrt');
$new['product_sales'] = __('销量', 'aksrt');
$new['product_stats'] = __('综合数据', 'aksrt');
$new['product_author_name'] = __('商家', 'aksrt');
$new['product_discount'] = __('优惠活动', 'aksrt');
}
}
return $new;
}
public function render_product_column(string $column, int $post_id): void {
switch ($column) {
case 'product_price':
$price = get_post_meta($post_id, '_aksrt_price', true);
echo esc_html(number_format_i18n((float) $price, 2));
break;
case 'product_stock':
$stock = get_post_meta($post_id, '_aksrt_stock', true);
$stock_type = get_post_meta($post_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts') {
echo esc_html__('按选项', 'aksrt');
} elseif ((int) $stock === -1) {
echo esc_html__('无限', 'aksrt');
} else {
echo esc_html((string) (int) $stock);
}
break;
case 'product_sales':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_sales', true));
break;
case 'product_stats':
$views = (int) get_post_meta($post_id, '_aksrt_views', true);
$fav = (int) get_post_meta($post_id, '_aksrt_favorite_count', true);
printf(
esc_html__('阅读 %1$d / 收藏 %2$d', 'aksrt'),
$views,
$fav
);
break;
case 'product_author_name':
$post = get_post($post_id);
if ($post) {
$author = get_userdata($post->post_author);
echo esc_html($author ? $author->display_name : '—');
}
break;
case 'product_discount':
$terms = wp_get_object_terms($post_id, 'shop_discount', ['fields' => 'all']);
if (!is_wp_error($terms) && !empty($terms)) {
$term_ids = wp_list_pluck($terms, 'term_id');
$names = wp_list_pluck($terms, 'name');
echo '<span class="aksrt-col-data" data-discount-id="' . esc_attr((string) ($term_ids[0] ?? 0)) . '"></span>';
echo esc_html(implode(', ', $names));
} else {
echo '<span class="aksrt-col-data" data-discount-id="0"></span>—';
}
break;
}
}
public function sortable_product_columns(array $columns): array {
$columns['product_sales'] = 'product_sales';
return $columns;
}
public function render_product_quick_edit(string $column_name, string $post_type): void {
if ($column_name !== 'product_discount' || $post_type !== 'aksrt_product') {
return;
}
wp_nonce_field('aksrt_product_quick_edit', 'aksrt_product_qe_nonce');
$discounts = get_terms([
'taxonomy' => 'shop_discount',
'hide_empty' => false,
]);
?>
<fieldset class="inline-edit-col-right" id="aksrt-product-quick-edit">
<div class="inline-edit-col">
<h4><?php esc_html_e('商品优惠活动', 'aksrt'); ?></h4>
<input type="hidden" name="aksrt_product_qe_flag" value="1">
<label class="alignleft">
<span class="title"><?php esc_html_e('优惠活动', 'aksrt'); ?></span>
<select name="aksrt_qe_discount" class="aksrt-qe-discount">
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
<option value="0"><?php esc_html_e('无优惠', 'aksrt'); ?></option>
<?php if (!is_wp_error($discounts)) : ?>
<?php foreach ($discounts as $term) : ?>
<option value="<?php echo esc_attr($term->term_id); ?>"><?php echo esc_html($term->name); ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</label>
</div>
</fieldset>
<?php
}
public function save_product_quick_edit(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_qe_nonce']) || !wp_verify_nonce($_POST['aksrt_product_qe_nonce'], 'aksrt_product_quick_edit')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (!isset($_POST['aksrt_product_qe_flag'])) {
return;
}
$discount_id = isset($_POST['aksrt_qe_discount']) ? sanitize_text_field(wp_unslash($_POST['aksrt_qe_discount'])) : '-1';
if ($discount_id === '-1') {
return;
}
if ($discount_id === '0') {
wp_set_object_terms($post_id, [], 'shop_discount');
} else {
$term_id = (int) $discount_id;
if ($term_id > 0 && term_exists($term_id, 'shop_discount')) {
wp_set_object_terms($post_id, [$term_id], 'shop_discount');
}
}
}
public function product_quick_edit_script(): void {
$screen = get_current_screen();
if (!$screen || $screen->post_type !== 'aksrt_product') {
return;
}
?>
<script>
(function(){
var wpInlineEdit = window.inlineEditPost ? window.inlineEditPost.edit : null;
if (!wpInlineEdit) return;
window.inlineEditPost.edit = function(id) {
wpInlineEdit.apply(this, arguments);
var postId = 0;
if (typeof id === 'object') {
postId = parseInt(this.getId(id));
}
if (!postId) return;
var row = document.getElementById('post-' + postId);
if (!row) return;
var dataEl = row.querySelector('.column-product_discount .aksrt-col-data');
if (!dataEl) return;
var termId = dataEl.getAttribute('data-discount-id') || '0';
var select = document.querySelector('.aksrt-qe-discount');
if (select) {
select.value = termId;
}
};
})();
</script>
<?php
}
}
@@ -1,499 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerProductOperations {
public function get_overview(): \WP_REST_Response {
$product_count = 0;
$post_type_obj = get_post_type_object('aksrt_product');
if ($post_type_obj) {
$counts = wp_count_posts('aksrt_product');
$product_count = (int) ($counts->publish ?? 0);
}
$order_stats = ['total' => 0, 'pending' => 0, 'paid' => 0, 'revenue' => 0];
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$order_stats = \AKSRT\Payment\OrderRepository::get_instance()->get_stats();
}
$revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0];
if (class_exists('\AKSRT\Payment\Revenue')) {
$revenue_stats = \AKSRT\Payment\Revenue::get_instance()->get_stats();
}
$gateway_summary = [];
if (class_exists('\AKSRT\Payment\Gateway')) {
$gateway_summary = \AKSRT\Payment\Gateway::get_instance()->get_status_summary();
}
return $this->success([
'data' => [
'enabled' => true,
'product_count' => $product_count,
'order_count' => $order_stats['total'],
'order_pending' => $order_stats['pending'] ?? 0,
'revenue_total' => $order_stats['revenue'] ?? 0,
'revenue_pending' => $revenue_stats['pending'] ?? 0,
'gateways' => $gateway_summary,
],
]);
}
public function get_product_taxonomy(): \WP_REST_Response {
return $this->success([
'data' => [
'categories' => $this->format_product_terms('aksrt_product_cat'),
'tags' => $this->format_product_terms('aksrt_product_tag'),
],
]);
}
/**
* POST /shop/products/taxonomy — create category or tag term.
*
* Body: { taxonomy: 'category'|'tag'|'aksrt_product_cat'|'aksrt_product_tag', name: string, parent?: int }
*/
public function create_product_taxonomy_term(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$raw_tax = sanitize_key((string) ($params['taxonomy'] ?? 'category'));
$name = sanitize_text_field((string) ($params['name'] ?? ''));
$parent = max(0, (int) ($params['parent'] ?? 0));
$taxonomy_map = [
'category' => 'aksrt_product_cat',
'tag' => 'aksrt_product_tag',
'aksrt_product_cat' => 'aksrt_product_cat',
'aksrt_product_tag' => 'aksrt_product_tag',
];
$taxonomy = $taxonomy_map[$raw_tax] ?? '';
if ($taxonomy === '' || !taxonomy_exists($taxonomy)) {
return $this->error(__('无效的分类法', 'aksrt'));
}
if ($name === '') {
return $this->error(__('名称不能为空', 'aksrt'));
}
$existing = term_exists($name, $taxonomy, $taxonomy === 'aksrt_product_cat' ? $parent : 0);
if (is_array($existing) && !empty($existing['term_id'])) {
$term = get_term((int) $existing['term_id'], $taxonomy);
if ($term && !is_wp_error($term)) {
return $this->success([
'message' => __('术语已存在', 'aksrt'),
'data' => [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'slug' => (string) $term->slug,
'parent' => (int) $term->parent,
'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag',
'existing' => true,
],
]);
}
}
$args = [];
if ($taxonomy === 'aksrt_product_cat' && $parent > 0) {
$args['parent'] = $parent;
}
$result = wp_insert_term($name, $taxonomy, $args);
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
$term_id = (int) ($result['term_id'] ?? 0);
$term = get_term($term_id, $taxonomy);
if (!$term || is_wp_error($term)) {
return $this->error(__('创建失败', 'aksrt'));
}
return $this->success([
'message' => __('术语已创建', 'aksrt'),
'data' => [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'slug' => (string) $term->slug,
'parent' => (int) $term->parent,
'taxonomy' => $taxonomy === 'aksrt_product_cat' ? 'category' : 'tag',
'existing' => false,
],
], 201);
}
/**
* @return array<int, array{id:int,name:string,slug:string,parent:int,count:int}>
*/
private function format_product_terms(string $taxonomy): array {
if (!taxonomy_exists($taxonomy)) {
return [];
}
$terms = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
]);
if (is_wp_error($terms) || !is_array($terms)) {
return [];
}
return array_map(static function (\WP_Term $term): array {
return [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'slug' => (string) $term->slug,
'parent' => (int) $term->parent,
'count' => (int) $term->count,
];
}, $terms);
}
/**
* GET /shop/products — 商品列表,支持 search、status、page、per_page
*/
public function get_products(\WP_REST_Request $request): \WP_REST_Response {
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20;
$search = sanitize_text_field((string) $request->get_param('search'));
$status = sanitize_key((string) $request->get_param('status'));
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
if ($status !== '') {
$query_args['post_status'] = $status;
} else {
$query_args['post_status'] = ['publish', 'draft', 'pending'];
}
if ($search !== '') {
$query_args['s'] = $search;
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$id = get_the_ID();
$products[] = [
'id' => $id,
'title' => get_the_title(),
'price' => (float) get_post_meta($id, '_aksrt_price', true),
'image' => get_the_post_thumbnail_url($id, 'medium') ?: '',
'stock' => (int) get_post_meta($id, '_aksrt_stock', true),
'sales' => (int) get_post_meta($id, '_aksrt_sales', true),
'status' => get_post_status(),
'created' => get_the_date('Y-m-d H:i'),
];
}
wp_reset_postdata();
return $this->success([
'data' => [
'items' => $products,
'total' => (int) $query->found_posts,
'total_pages' => (int) $query->max_num_pages,
'page' => $page,
],
]);
}
/**
* GET /shop/products/{id} — 单个商品详情
*/
public function get_product(\WP_REST_Request $request): \WP_REST_Response {
$id = (int) $request->get_param('id');
$post = get_post($id);
if (!$post || $post->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
return $this->success([
'data' => [
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'price' => (float) get_post_meta($post->ID, '_aksrt_price', true),
'image' => (string) (get_post_meta($post->ID, '_aksrt_product_image', true) ?: get_the_post_thumbnail_url($post->ID, 'medium') ?: ''),
'stock' => (int) get_post_meta($post->ID, '_aksrt_stock', true),
'sales' => (int) get_post_meta($post->ID, '_aksrt_sales', true),
'status' => $post->post_status,
'created' => get_the_date('Y-m-d H:i', $post),
'modified' => get_the_modified_date('Y-m-d H:i', $post),
'categories' => wp_get_post_terms($post->ID, 'aksrt_product_cat', ['fields' => 'names']) ?: [],
'tags' => wp_get_post_terms($post->ID, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
'gallery' => get_post_meta($post->ID, '_aksrt_product_gallery', true) ?: [],
'shipping_mode' => (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true),
'shipping_fee' => (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true),
'after_sale_policy' => (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($post->ID, '_aksrt_support_refund', true) !== '0',
],
]);
}
/**
* POST /shop/products — 创建商品
*/
public function create_product(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$title = sanitize_text_field((string) ($params['title'] ?? ''));
$content = wp_kses_post((string) ($params['content'] ?? ''));
$excerpt = sanitize_textarea_field((string) ($params['excerpt'] ?? ''));
$price = (float) ($params['price'] ?? 0);
$stock = (int) ($params['stock'] ?? 0);
$status = sanitize_key((string) ($params['status'] ?? 'publish'));
$image = esc_url_raw((string) ($params['image'] ?? ''));
$gallery = is_array($params['gallery'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['gallery']))) : [];
$shipping_mode = sanitize_key((string) ($params['shipping_mode'] ?? 'express'));
$shipping_fee = max(0, (float) ($params['shipping_fee'] ?? 0));
$after_sale_policy = sanitize_textarea_field((string) ($params['after_sale_policy'] ?? ''));
$support_refund = !isset($params['support_refund']) || !empty($params['support_refund']);
$author_id = max(0, (int) ($params['author_id'] ?? 0));
if ($title === '') {
return $this->error('商品名称不能为空');
}
$valid_statuses = ['publish', 'draft', 'pending'];
if (!in_array($status, $valid_statuses, true)) {
$status = 'draft';
}
$post_id = wp_insert_post([
'post_type' => 'aksrt_product',
'post_title' => $title,
'post_content' => $content,
'post_excerpt' => $excerpt,
'post_status' => $status,
'post_author' => $author_id > 0 ? $author_id : get_current_user_id(),
'meta_input' => [
'_aksrt_price' => max(0, $price),
'_aksrt_stock' => max(0, $stock),
'_aksrt_sales' => 0,
'_aksrt_score' => 0,
'_aksrt_views' => 0,
'_aksrt_favorite_count' => 0,
'_aksrt_product_gallery' => $gallery,
'_aksrt_shipping_mode' => $shipping_mode,
'_aksrt_shipping_fee' => $shipping_fee,
'_aksrt_after_sale_policy' => $after_sale_policy,
'_aksrt_support_refund' => $support_refund ? '1' : '0',
'_aksrt_product_video' => is_array($params['product_video'] ?? null) ? array_values(array_filter(array_map('esc_url_raw', $params['product_video']))) : [],
'_aksrt_product_params' => sanitize_textarea_field((string) ($params['product_params'] ?? '')),
'_aksrt_pay_modo' => sanitize_key((string) ($params['pay_modo'] ?? '0')),
'_aksrt_start_price' => max(0, (float) ($params['start_price'] ?? 0)),
'_aksrt_product_rebate' => sanitize_text_field((string) ($params['product_rebate'] ?? '')),
'_aksrt_limit_buy' => max(-1, min(9999, (int) ($params['limit_buy'] ?? -1))),
'_aksrt_limit_buy_note' => sanitize_text_field((string) ($params['limit_buy_note'] ?? '')),
'_aksrt_stock_type' => sanitize_key((string) ($params['stock_type'] ?? 'all')),
'_aksrt_user_required' => sanitize_textarea_field((string) ($params['user_required'] ?? '')),
'_aksrt_shipping_type' => sanitize_key((string) ($params['shipping_type'] ?? $shipping_mode)),
'_aksrt_shipping_delivery_desc' => sanitize_text_field((string) ($params['shipping_delivery_desc'] ?? '')),
'_aksrt_shipping_fee_type' => sanitize_key((string) ($params['shipping_fee_type'] ?? 'free')),
'_aksrt_shipping_fee_free_amount'=> max(0, (float) ($params['shipping_fee_free_amount'] ?? 0)),
'_aksrt_after_sale_matrix' => sanitize_textarea_field((string) ($params['after_sale_matrix'] ?? '')),
'_aksrt_service' => sanitize_textarea_field((string) ($params['service'] ?? '')),
'_aksrt_single_tab' => sanitize_textarea_field((string) ($params['single_tab'] ?? '')),
'_aksrt_content_layout' => sanitize_key((string) ($params['content_layout'] ?? '')),
'_aksrt_content_show_bg' => sanitize_text_field((string) ($params['content_show_bg'] ?? '')),
'_aksrt_delivery_note' => sanitize_text_field((string) ($params['delivery_note'] ?? '')),
'_aksrt_marketing_headline' => sanitize_text_field((string) ($params['marketing_headline'] ?? '')),
'_aksrt_shipping_tip' => sanitize_text_field((string) ($params['shipping_tip'] ?? '')),
'_aksrt_service_tip' => sanitize_text_field((string) ($params['service_tip'] ?? '')),
],
], true);
if (is_wp_error($post_id)) {
return $this->error($post_id->get_error_message());
}
if (!empty($params['categories']) && is_array($params['categories'])) {
wp_set_object_terms($post_id, $params['categories'], 'aksrt_product_cat');
}
if (!empty($params['tags']) && is_array($params['tags'])) {
wp_set_object_terms($post_id, $params['tags'], 'aksrt_product_tag');
}
if ($image !== '') {
update_post_meta($post_id, '_aksrt_product_image', $image);
}
if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) {
\AKSRT\Shop\Shop::get_instance()->save_product_options((int) $post_id, $params['options']);
}
return $this->success([
'message' => __('商品已创建', 'aksrt'),
'data' => ['id' => $post_id],
], 201);
}
/**
* POST /shop/products/{id} — 更新商品(含上下架)
*/
public function update_product(\WP_REST_Request $request): \WP_REST_Response {
$id = (int) $request->get_param('id');
$post = get_post($id);
if (!$post || $post->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
$params = $this->get_json_params($request);
$update = ['ID' => $id];
if (isset($params['title'])) {
$update['post_title'] = sanitize_text_field($params['title']);
}
if (isset($params['content'])) {
$update['post_content'] = wp_kses_post($params['content']);
}
if (isset($params['excerpt'])) {
$update['post_excerpt'] = sanitize_textarea_field($params['excerpt']);
}
if (isset($params['status'])) {
$new_status = sanitize_key($params['status']);
$valid_statuses = ['publish', 'draft', 'pending', 'trash'];
if (in_array($new_status, $valid_statuses, true)) {
$update['post_status'] = $new_status;
}
}
$meta = [];
if (isset($params['price'])) {
$meta['_aksrt_price'] = max(0, (float) $params['price']);
}
if (isset($params['stock'])) {
$meta['_aksrt_stock'] = max(0, (int) $params['stock']);
}
if (isset($params['gallery']) && is_array($params['gallery'])) {
$meta['_aksrt_product_gallery'] = array_values(array_filter(array_map('esc_url_raw', $params['gallery'])));
}
if (isset($params['shipping_mode'])) {
$meta['_aksrt_shipping_mode'] = sanitize_key((string) $params['shipping_mode']);
}
if (isset($params['shipping_fee'])) {
$meta['_aksrt_shipping_fee'] = max(0, (float) $params['shipping_fee']);
}
if (isset($params['after_sale_policy'])) {
$meta['_aksrt_after_sale_policy'] = sanitize_textarea_field((string) $params['after_sale_policy']);
}
if (isset($params['support_refund'])) {
$meta['_aksrt_support_refund'] = !empty($params['support_refund']) ? '1' : '0';
}
if (isset($params['product_video']) && is_array($params['product_video'])) {
$meta['_aksrt_product_video'] = array_values(array_filter(array_map('esc_url_raw', $params['product_video'])));
}
if (isset($params['product_params'])) {
$meta['_aksrt_product_params'] = sanitize_textarea_field((string) $params['product_params']);
}
if (isset($params['pay_modo'])) {
$meta['_aksrt_pay_modo'] = sanitize_key((string) $params['pay_modo']);
}
if (isset($params['start_price'])) {
$meta['_aksrt_start_price'] = max(0, (float) $params['start_price']);
}
if (isset($params['product_rebate'])) {
$meta['_aksrt_product_rebate'] = sanitize_text_field((string) $params['product_rebate']);
}
if (isset($params['limit_buy'])) {
$meta['_aksrt_limit_buy'] = max(-1, min(9999, (int) $params['limit_buy']));
}
if (isset($params['limit_buy_note'])) {
$meta['_aksrt_limit_buy_note'] = sanitize_text_field((string) $params['limit_buy_note']);
}
if (isset($params['stock_type'])) {
$meta['_aksrt_stock_type'] = sanitize_key((string) $params['stock_type']);
}
if (isset($params['stock_opts']) && is_array($params['stock_opts'])) {
$meta['_aksrt_stock_opts'] = wp_json_encode($params['stock_opts']);
}
if (isset($params['user_required'])) {
$meta['_aksrt_user_required'] = sanitize_textarea_field((string) $params['user_required']);
}
if (isset($params['shipping_type'])) {
$meta['_aksrt_shipping_type'] = sanitize_key((string) $params['shipping_type']);
}
if (isset($params['shipping_delivery_desc'])) {
$meta['_aksrt_shipping_delivery_desc'] = sanitize_text_field((string) $params['shipping_delivery_desc']);
}
if (isset($params['auto_delivery']) && is_array($params['auto_delivery'])) {
$meta['_aksrt_auto_delivery'] = wp_json_encode($params['auto_delivery']);
}
if (isset($params['shipping_fee_type'])) {
$meta['_aksrt_shipping_fee_type'] = sanitize_key((string) $params['shipping_fee_type']);
}
if (isset($params['shipping_fee_free_amount'])) {
$meta['_aksrt_shipping_fee_free_amount'] = max(0, (float) $params['shipping_fee_free_amount']);
}
if (isset($params['after_sale_matrix'])) {
$meta['_aksrt_after_sale_matrix'] = sanitize_textarea_field((string) $params['after_sale_matrix']);
}
if (isset($params['service'])) {
$meta['_aksrt_service'] = sanitize_textarea_field((string) $params['service']);
}
if (isset($params['single_tab'])) {
$meta['_aksrt_single_tab'] = sanitize_textarea_field((string) $params['single_tab']);
}
if (isset($params['content_layout'])) {
$meta['_aksrt_content_layout'] = sanitize_key((string) $params['content_layout']);
}
if (isset($params['content_show_bg'])) {
$meta['_aksrt_content_show_bg'] = sanitize_text_field((string) $params['content_show_bg']);
}
if (isset($params['delivery_note'])) {
$meta['_aksrt_delivery_note'] = sanitize_text_field((string) $params['delivery_note']);
}
if (isset($params['marketing_headline'])) {
$meta['_aksrt_marketing_headline'] = sanitize_text_field((string) $params['marketing_headline']);
}
if (isset($params['shipping_tip'])) {
$meta['_aksrt_shipping_tip'] = sanitize_text_field((string) $params['shipping_tip']);
}
if (isset($params['service_tip'])) {
$meta['_aksrt_service_tip'] = sanitize_text_field((string) $params['service_tip']);
}
if (!empty($meta)) {
$update['meta_input'] = $meta;
}
$result = wp_update_post($update, true);
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
if (isset($params['categories']) && is_array($params['categories'])) {
wp_set_object_terms($id, $params['categories'], 'aksrt_product_cat');
}
if (isset($params['tags']) && is_array($params['tags'])) {
wp_set_object_terms($id, $params['tags'], 'aksrt_product_tag');
}
if (isset($params['image'])) {
update_post_meta($id, '_aksrt_product_image', esc_url_raw((string) $params['image']));
}
if (isset($params['options']) && is_array($params['options']) && class_exists('\AKSRT\Shop\Shop')) {
\AKSRT\Shop\Shop::get_instance()->save_product_options($id, $params['options']);
}
return $this->success([
'message' => __('商品已更新', 'aksrt'),
]);
}
}
@@ -1,113 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ShopControllerVariantExportOperations {
public function get_product_variants(\WP_REST_Request $request): \WP_REST_Response {
$product_id = (int) $request->get_param('id');
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->success(['data' => ['variants' => []]]);
}
$variants = \AKSRT\Shop\Shop::get_instance()->get_variants($product_id);
return $this->success(['data' => ['variants' => $variants]]);
}
public function save_product_variants(\WP_REST_Request $request): \WP_REST_Response {
$product_id = (int) $request->get_param('id');
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return $this->error('商品不存在', 404);
}
$params = method_exists($this, 'get_json_params')
? $this->get_json_params($request)
: ($request->get_json_params() ?? []);
$variants = $params['variants'] ?? [];
if (!is_array($variants)) {
return $this->error('变体数据格式错误');
}
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城系统未启用', 501);
}
$saved = \AKSRT\Shop\Shop::get_instance()->save_variants($product_id, $variants);
return $this->success(['message' => '变体已保存', 'data' => ['variants' => $saved]]);
}
public function delete_variant(\WP_REST_Request $request): \WP_REST_Response {
$variant_id = (int) $request->get_param('id');
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城系统未启用', 501);
}
$result = \AKSRT\Shop\Shop::get_instance()->delete_variant($variant_id);
if (!$result) {
return $this->error('删除失败', 500);
}
return $this->success(['message' => '变体已删除']);
}
public function export_orders(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$args = [
'status' => sanitize_key((string) $request->get_param('status')),
'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
'date_to' => sanitize_text_field((string) $request->get_param('date_to')),
'limit' => max(1, min(5000, (int) ($request->get_param('limit') ?: 1000))),
];
$result = \AKSRT\Shop\Shop::get_instance()->export_orders_csv($args);
return new \WP_REST_Response($result['csv'], 200, [
'Content-Type' => 'text/csv; charset=utf-8',
'Content-Disposition' => 'attachment; filename="orders-' . date('Ymd-His') . '.csv"',
'X-Order-Count' => $result['count'],
]);
}
public function confirm_receipt(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Shop\Shop')) {
return $this->error('商城服务不可用', 500);
}
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$result = \AKSRT\Shop\Shop::get_instance()->confirm_receipt(get_current_user_id(), $order_number);
if (empty($result['success'])) {
return $this->error($result['message'] ?? '确认收货失败');
}
return $this->success(['data' => $result['data']]);
}
public function get_after_sale_reasons(\WP_REST_Request $request): \WP_REST_Response {
if (class_exists('\AKSRT\Payment\AfterSale')) {
return $this->success(['data' => \AKSRT\Payment\AfterSale::get_reason_options()]);
}
return $this->success(['data' => ['refund' => [], 'return' => []]]);
}
public function get_frontend_after_sale_reasons(\WP_REST_Request $request): \WP_REST_Response {
if (class_exists('\AKSRT\Payment\AfterSale')) {
return $this->success(['data' => \AKSRT\Payment\AfterSale::get_reason_options()]);
}
return $this->success(['data' => ['refund' => [], 'return' => []]]);
}
}
-95
View File
@@ -261,24 +261,6 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_restriction);
// ---------- 13. 商品变体/SKU ----------
$sql_variant = "CREATE TABLE {$prefix}aksrt_product_variant (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
product_id BIGINT UNSIGNED NOT NULL,
sku VARCHAR(64) NOT NULL DEFAULT '',
attributes TEXT DEFAULT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
image VARCHAR(512) NOT NULL DEFAULT '',
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY idx_product (product_id),
KEY idx_sku (sku),
KEY idx_sort (sort_order)
) {$charset_collate};";
dbDelta($sql_variant);
// ── follow ──
$sql_follow = "CREATE TABLE {$prefix}aksrt_follow (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -329,64 +311,6 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_coupon_usage);
// ── after_sale ──
$sql_after_sale = "CREATE TABLE {$prefix}aksrt_after_sale (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(32) NOT NULL DEFAULT '',
user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
type VARCHAR(20) NOT NULL DEFAULT 'refund',
reason TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
note TEXT DEFAULT NULL,
operator_id BIGINT UNSIGNED DEFAULT NULL,
operator_note TEXT DEFAULT NULL,
refund_request_id VARCHAR(64) DEFAULT NULL,
provider VARCHAR(32) NOT NULL DEFAULT '',
provider_refund_id VARCHAR(128) NOT NULL DEFAULT '',
provider_status VARCHAR(24) NOT NULL DEFAULT 'not_started',
refund_effect_status VARCHAR(24) NOT NULL DEFAULT 'not_started',
refund_error TEXT DEFAULT NULL,
refund_attempts INT UNSIGNED NOT NULL DEFAULT 0,
refund_requested_at DATETIME DEFAULT NULL,
provider_updated_at DATETIME DEFAULT NULL,
refund_succeeded_at DATETIME DEFAULT NULL,
refund_effected_at DATETIME DEFAULT NULL,
order_status_before VARCHAR(20) NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
updated_at DATETIME DEFAULT NULL,
resolved_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_refund_request (refund_request_id),
KEY idx_order (order_number),
KEY idx_user (user_id),
KEY idx_status (status),
KEY idx_type (type),
KEY idx_provider_status (provider_status),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_after_sale);
// ── order_shipping ──
$sql_shipping = "CREATE TABLE {$prefix}aksrt_order_shipping (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_number VARCHAR(32) NOT NULL DEFAULT '',
carrier VARCHAR(60) NOT NULL DEFAULT '',
tracking_no VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(20) NOT NULL DEFAULT 'shipped',
note TEXT DEFAULT NULL,
operator_id BIGINT UNSIGNED DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
updated_at DATETIME DEFAULT NULL,
delivered_at DATETIME DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_order (order_number),
KEY idx_status (status),
KEY idx_tracking (tracking_no),
KEY idx_created (created_at)
) {$charset_collate};";
dbDelta($sql_shipping);
// ── charge_card (zibll-style typed card_pass) ──
$sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -461,25 +385,6 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_order_items);
// ── card_key 卡密表 (§6.2 自动发货) ──
$sql_card_key = "CREATE TABLE {$prefix}aksrt_card_key (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
options_key VARCHAR(255) NOT NULL DEFAULT '',
card_code TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'available',
order_number VARCHAR(32) NOT NULL DEFAULT '',
used_by BIGINT UNSIGNED DEFAULT NULL,
used_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (id),
KEY idx_product (product_id),
KEY idx_options (options_key(50)),
KEY idx_status (status),
KEY idx_order (order_number)
) {$charset_collate};";
dbDelta($sql_card_key);
self::migrate_identity_tables($prefix, $charset_collate);
}
}
+4 -8
View File
@@ -31,16 +31,12 @@ trait DBMigratorPart3 {
'report' => 'aksrt_report',
'follow' => 'aksrt_follow',
'coupon' => 'aksrt_coupon',
'after_sale' => 'aksrt_after_sale',
'order_shipping' => 'aksrt_order_shipping',
'charge_card' => 'aksrt_charge_card',
'rebate_log' => 'aksrt_rebate_log',
'coupon_usage' => 'aksrt_coupon_usage',
'card_key' => 'aksrt_card_key',
'user_restriction' => 'aksrt_user_restriction',
'product_variant' => 'aksrt_product_variant',
'identity_application' => 'aksrt_identity_application',
'identity_event' => 'aksrt_identity_event',
'coupon_usage' => 'aksrt_coupon_usage',
'user_restriction' => 'aksrt_user_restriction',
'identity_application' => 'aksrt_identity_application',
'identity_event' => 'aksrt_identity_event',
];
if (!isset($allowed[$name])) {
return '';
-2
View File
@@ -30,7 +30,6 @@ class DomainModel {
public const ORDER_PRODUCT_RECHARGE = 'recharge';
/** Cash purchase of points (zibll order_type=9). */
public const ORDER_PRODUCT_POINTS_BUY = 'points_buy';
public const ORDER_PRODUCT_SHOP = 'shop_product';
/**
* Human labels for aksrt_order.product_type (front-end order center).
@@ -39,7 +38,6 @@ class DomainModel {
*/
public static function order_product_type_labels(): array {
return [
self::ORDER_PRODUCT_SHOP => __('商城商品', 'aksrt'),
self::ORDER_PRODUCT_RESOURCE => __('付费内容', 'aksrt'),
self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'),
self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'),
+2 -91
View File
@@ -635,13 +635,11 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
}
}
// 交易中心共享壳层(购物车 / 订单 / 收益)
// Virtual rewrite routes (aksrt_orders / aksrt_cart / aksrt_revenue) also count.
// 交易中心共享壳层(订单 / 收益)
// Virtual rewrite routes (aksrt_orders / aksrt_revenue) also count.
$is_commerce_center = is_page_template('templates/pages/page-orders.php')
|| is_page_template('templates/pages/page-cart.php')
|| is_page_template('templates/pages/page-revenue.php')
|| (bool) get_query_var('aksrt_orders')
|| (bool) get_query_var('aksrt_cart')
|| (bool) get_query_var('aksrt_revenue');
if ($is_commerce_center) {
$commerce_css = get_template_directory() . '/assets/css/routes/commerce.css';
@@ -735,93 +733,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
}
}
// Product archive pages (category / tag)
if (is_tax('aksrt_product_cat') || is_tax('aksrt_product_tag')) {
wp_enqueue_style(
'aksrt-product-archive',
$base_uri . '/assets/css/frontend/product-archive.css',
['aksrt-tokens'],
$version
);
}
// Product detail page
if (is_page_template('templates/pages/page-product.php')) {
wp_enqueue_style(
'aksrt-route-product',
$base_uri . '/assets/css/routes/product.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/product.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css')
: $version
);
wp_enqueue_script(
'aksrt-product-purchase',
$base_uri . '/assets/js/product-purchase.js',
['aksrt-script'],
file_exists(get_template_directory() . '/assets/js/product-purchase.js')
? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js')
: $version,
true
);
}
// Cart page
if (is_page_template('templates/pages/page-cart.php') || (bool) get_query_var('aksrt_cart')) {
wp_enqueue_style(
'aksrt-route-cart',
$base_uri . '/assets/css/routes/cart.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/cart.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/cart.css')
: $version
);
wp_enqueue_script(
'aksrt-cart-checkout',
$base_uri . '/assets/js/cart-checkout.js',
['aksrt-script'],
file_exists(get_template_directory() . '/assets/js/cart-checkout.js')
? (string) filemtime(get_template_directory() . '/assets/js/cart-checkout.js')
: $version,
true
);
}
// Product edit page
if (is_page_template('templates/pages/page-product-edit.php')) {
wp_enqueue_style(
'aksrt-route-product',
$base_uri . '/assets/css/routes/product.css',
['aksrt-tokens'],
file_exists(get_template_directory() . '/assets/css/routes/product.css')
? (string) filemtime(get_template_directory() . '/assets/css/routes/product.css')
: $version
);
wp_enqueue_script(
'aksrt-product-purchase',
$base_uri . '/assets/js/product-purchase.js',
['aksrt-script'],
file_exists(get_template_directory() . '/assets/js/product-purchase.js')
? (string) filemtime(get_template_directory() . '/assets/js/product-purchase.js')
: $version,
true
);
}
// 商品编辑页共用 page-edit.js
if (is_page_template('templates/pages/page-product-edit.php')) {
$page_edit_js = get_template_directory() . '/assets/js/page-edit.js';
if (file_exists($page_edit_js)) {
wp_enqueue_script(
'aksrt-page-edit',
$base_uri . '/assets/js/page-edit.js',
['aksrt-script'],
(string) filemtime($page_edit_js),
true
);
}
}
// Unified share-data builder — single payload assembler for every share
// surface (dropdown, copy, WeChat, poster). Loaded on any singular view.
if (is_singular()) {
-1
View File
@@ -264,7 +264,6 @@ trait ScriptsPart2 {
*/
private function resolve_frontend_routes(): array {
return [
'cart' => $this->resolve_template_page_url('templates/pages/page-cart.php', '/cart/'),
'orders' => $this->resolve_template_page_url('templates/pages/page-orders.php', '/orders/'),
'resources' => home_url('/resources/'),
];
-105
View File
@@ -31,20 +31,6 @@ class Setup {
add_action('init', [$this, 'register_nav_menus'], 0);
add_action('init', [$this, 'register_post_types'], 1);
add_action('init', [$this, 'register_meta_fields'], 5);
add_action('init', [$this, 'register_shop_rewrite_rules'], 20);
add_filter('query_vars', [$this, 'register_shop_query_vars']);
add_action('template_redirect', [$this, 'shop_template_redirect']);
// Shop fulfillment hook (§6.5 R68)
if (class_exists('\AKSRT\Shop\Shop')) {
add_action('aksrt_order_paid', [\AKSRT\Shop\Shop::get_instance(), 'handle_order_paid'], 10, 2);
}
// Auto-confirm receipt cron (§6.5 R68)
if (!wp_next_scheduled('aksrt_auto_confirm_receipt')) {
wp_schedule_event(time(), 'hourly', 'aksrt_auto_confirm_receipt');
}
if (class_exists('\AKSRT\Shop\Shop')) {
add_action('aksrt_auto_confirm_receipt', [\AKSRT\Shop\Shop::get_instance(), 'run_auto_confirm_cron']);
}
// 文章浏览量统计(单次访问)
add_action('wp', 'aksrt_set_post_views');
// 隐藏默认WordPress管理菜单
@@ -110,97 +96,6 @@ class Setup {
* 通过 _aksrt_is_resource meta 标记是否为资源。
*/
public function register_post_types(): void {
// Shop rewrite settings (safe fallback if settings not yet loaded)
$shop_rewrite = [];
if (class_exists('\AKSRT\Settings\Settings')) {
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$shop_rewrite = $shop['rewrite'] ?? [];
}
$product_slug = sanitize_key((string) ($shop_rewrite['product_slug'] ?? 'product'));
$suffix_html = !empty($shop_rewrite['suffix_html']);
// Shop admin visibility: align with zibll native sidebar when shop is enabled.
$shop_enabled = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$shop_settings = \AKSRT\Settings\Settings::get_shop_settings();
$shop_enabled = !empty($shop_settings['enabled']);
}
// Always register UI in admin so settings/tools remain reachable; hide menu when disabled.
$shop_show_ui = true;
$shop_show_menu = is_admin() ? $shop_enabled : false;
// 商品 CPT — native WP sidebar (zibll: shop_product @ pos 35)
register_post_type('aksrt_product', [
'labels' => [
'name' => __('商城', 'aksrt'),
'singular_name' => __('商品', 'aksrt'),
'add_new' => __('新增商品', 'aksrt'),
'add_new_item' => __('新增商品', 'aksrt'),
'edit_item' => __('编辑商品', 'aksrt'),
'new_item' => __('新商品', 'aksrt'),
'view_item' => __('查看商品', 'aksrt'),
'search_items' => __('搜索商品', 'aksrt'),
'not_found' => __('未找到商品', 'aksrt'),
'not_found_in_trash' => __('回收站中无商品', 'aksrt'),
'all_items' => __('所有商品', 'aksrt'),
'menu_name' => __('商城', 'aksrt'),
],
'public' => true,
'publicly_queryable' => true,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 35,
'menu_icon' => 'dashicons-store',
'supports' => ['title', 'editor', 'excerpt', 'thumbnail', 'author', 'revisions'],
'rewrite' => ['slug' => $product_slug, 'with_front' => false],
'capability_type' => 'post',
'map_meta_cap' => true,
]);
// 商品分类 taxonomy(独立于文章 category)
register_taxonomy('aksrt_product_cat', 'aksrt_product', [
'labels' => [
'name' => __('商品分类', 'aksrt'),
'singular_name' => __('商品分类', 'aksrt'),
'search_items' => __('搜索商品分类', 'aksrt'),
'all_items' => __('所有商品分类', 'aksrt'),
'parent_item' => __('父级分类', 'aksrt'),
'edit_item' => __('编辑分类', 'aksrt'),
'add_new_item' => __('新增分类', 'aksrt'),
'menu_name' => __('商品分类', 'aksrt'),
],
'public' => true,
'hierarchical' => true,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => ['slug' => 'product-cat', 'with_front' => false],
]);
// 商品标签 taxonomy(独立于文章 post_tag)
register_taxonomy('aksrt_product_tag', 'aksrt_product', [
'labels' => [
'name' => __('商品标签', 'aksrt'),
'singular_name' => __('商品标签', 'aksrt'),
'search_items' => __('搜索商品标签', 'aksrt'),
'all_items' => __('所有商品标签', 'aksrt'),
'edit_item' => __('编辑标签', 'aksrt'),
'add_new_item' => __('新增标签', 'aksrt'),
'menu_name' => __('特色标签', 'aksrt'),
],
'public' => true,
'hierarchical' => false,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => ['slug' => 'product-tag', 'with_front' => false],
]);
// 项目展示
register_post_type('project', [
'labels' => [
-24
View File
@@ -382,30 +382,6 @@ trait SetupPart1 {
'after_title' => $title_after,
]);
// ── Shop widget areas (§6.9) ──
$shop_widgets = [
'shop_home_top_fluid' => ['商城首页 · 顶部通栏', '商城首页顶部全宽区域。'],
'shop_home_sidebar' => ['商城首页 · 侧栏', '商城首页侧栏区域。'],
'shop_product_top_fluid' => ['商品详情页 · 顶部通栏', '商品详情页顶部全宽区域。'],
'shop_product_sidebar' => ['商品详情页 · 侧栏', '商品详情页侧栏区域(PC 端)。'],
'shop_product_bottom_fluid' => ['商品详情页 · 底部通栏', '商品详情页底部全宽区域。'],
'shop_cart_top_fluid' => ['购物车页 · 顶部通栏', '购物车页顶部全宽区域。'],
'shop_cart_bottom_fluid' => ['购物车页 · 底部通栏', '购物车页底部全宽区域。'],
'shop_order_top_fluid' => ['订单页 · 顶部通栏', '订单中心顶部全宽。'],
'shop_order_bottom_fluid' => ['订单页 · 底部通栏', '订单中心底部全宽。'],
];
foreach ($shop_widgets as $sid => $meta) {
register_sidebar([
'id' => $sid,
'name' => $meta[0],
'description' => $meta[1],
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
}
// ── Per-page dynamic sidebars (zibll zib_page_register_sidebar parity) ──
// Pages that opt in via meta `widgets_register=1` + `widgets_register_container`
// (a serialized list of area keys) get page_{key}_{ID} sidebars registered, and
-92
View File
@@ -23,97 +23,5 @@ trait SetupPart2 {
// 移除仪表盘中的不必要组件
remove_meta_box('dashboard_primary', 'dashboard', 'normal');
remove_meta_box('dashboard_quick_press', 'dashboard', 'side');
// Separators around shop (35) and forum (40) — match zibll admin_menu_separator.
global $menu;
if (!is_array($menu)) {
return;
}
if (!isset($menu[34])) {
$menu[34] = ['', 'read', 'separator-aksrt-shop', '', 'wp-menu-separator'];
}
}
/**
* Register shop rewrite rules for:
* - /shop/{ID}.html — product detail (ID-based, .html suffix)
* - /shop/{ID}.html/comment-{page} — product comment pagination
* - /{cart_slug}/ — cart page (fixed alias)
*
* Runs at init priority 20 (after CPT registration at priority 1).
*/
public function register_shop_rewrite_rules(): void {
if (!class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
if (empty($shop['enabled'])) {
return;
}
$rw = $shop['rewrite'] ?? [];
$product_slug = sanitize_key((string) ($rw['product_slug'] ?? 'product'));
$cart_slug = sanitize_key((string) ($rw['cart_slug'] ?? 'cart'));
$suffix_html = !empty($rw['suffix_html']);
// Product ID-based .html routes: /shop/{ID}.html
if ($suffix_html) {
add_rewrite_rule(
'^shop/(\d+)\.html/comment-page-([0-9]{1,})/?$',
'index.php?post_type=aksrt_product&p=$matches[1]&cpage=$matches[2]',
'top'
);
add_rewrite_rule(
'^shop/(\d+)\.html/?$',
'index.php?post_type=aksrt_product&p=$matches[1]',
'top'
);
// Slug-based .html: /{product_slug}/{slug}.html
add_rewrite_rule(
'^' . preg_quote($product_slug) . '/([^/]+)\.html/comment-page-([0-9]{1,})/?$',
'index.php?post_type=aksrt_product&aksrt_product=$matches[1]&cpage=$matches[3]',
'top'
);
add_rewrite_rule(
'^' . preg_quote($product_slug) . '/([^/]+)\.html/?$',
'index.php?post_type=aksrt_product&aksrt_product=$matches[1]',
'top'
);
}
// Cart page: /{cart_slug}/
add_rewrite_rule(
'^' . preg_quote($cart_slug) . '/?$',
'index.php?pagename=' . $cart_slug,
'top'
);
}
/**
* Register shop-specific query vars.
*/
public function register_shop_query_vars(array $vars): array {
$vars[] = 'product_id';
$vars[] = 'shop_action';
return $vars;
}
/**
* Template redirect for shop pages.
*
* - Single aksrt_product: serve page-product.php template
* - Cart page: serve page-cart.php template
*/
public function shop_template_redirect(): void {
// Single product: if the main query resolves to aksrt_product, use our template
if (is_singular('aksrt_product')) {
$tpl = AKSRT_DIR . '/templates/pages/page-product.php';
if (file_exists($tpl)) {
// Set product_id query var for template consumption
set_query_var('product_id', get_queried_object_id());
include $tpl;
exit;
}
}
}
}
File diff suppressed because it is too large Load Diff
+2 -6
View File
@@ -159,7 +159,7 @@ class Coupon {
}
// Product / content scope check.
// Supports shop product cats and content post cats.
// Supports product and content post cats.
if (!empty($coupon['product_scope']) && !empty($args['product_ids'])) {
$scope = is_array($coupon['product_scope']) ? $coupon['product_scope'] : json_decode($coupon['product_scope'], true);
if (is_array($scope) && !empty($scope)) {
@@ -168,11 +168,7 @@ class Coupon {
$product_ids = array_map('intval', (array) $args['product_ids']);
$taxonomies = array_values(array_filter(array_map('sanitize_key', (array) ($scope['taxonomies'] ?? []))));
if ($taxonomies === []) {
// content context prefers category; shop admin may pass aksrt_product_cat.
$context = sanitize_key((string) ($args['context'] ?? ''));
$taxonomies = ($context === 'content')
? ['category']
: ['aksrt_product_cat', 'category'];
$taxonomies = ['aksrt_product_cat', 'category'];
}
if ($scope_type === 'include' && empty(array_intersect($product_ids, $scope_ids))) {
-167
View File
@@ -1,167 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
class Discount {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get all active discount campaigns for a product.
*
* @param int $product_id
* @return array<int, array> Sorted by priority desc.
*/
public function get_active_discounts(int $product_id): array {
$terms = wp_get_object_terms($product_id, 'shop_discount', ['fields' => 'all']);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
$now = current_time('timestamp');
$active = [];
foreach ($terms as $term) {
$meta = \AKSRT\Shop\TermMeta::get_discount_meta($term->term_id);
// Time limit check
if (!empty($meta['start']) && strtotime($meta['start']) > $now) {
continue;
}
if (!empty($meta['end']) && strtotime($meta['end']) < $now) {
continue;
}
$meta['term_id'] = $term->term_id;
$meta['name'] = $term->name;
$active[] = $meta;
}
// Sort by priority desc
usort($active, function ($a, $b) {
return ((int) $b['priority']) - ((int) $a['priority']);
});
return $active;
}
/**
* Calculate discount for a single line item.
*
* @param float $unit_price Unit price
* @param int $quantity Quantity
* @param int $product_id Product ID
* @param int $user_id User ID (for user_limit checks)
* @return array{
* original: float,
* discount_amount: float,
* final_price: float,
* gifts: array,
* applied: array<int, array>
* }
*/
public function calculate(float $unit_price, int $quantity, int $product_id, int $user_id = 0): array {
$original = $unit_price * $quantity;
$discount_amount = 0.0;
$gifts = [];
$applied = [];
$discounts = $this->get_active_discounts($product_id);
foreach ($discounts as $d) {
// User limit check
if (!empty($d['user_limit']) && $user_id > 0) {
if (!$this->check_user_limit($d['user_limit'], $user_id)) {
continue;
}
}
// Price limit check
if ((float) $d['price_limit'] > 0 && $original < (float) $d['price_limit']) {
continue;
}
$type = $d['type'] ?? 'reduction';
$value = (float) ($d['value'] ?? 0);
switch ($type) {
case 'reduction':
// 立减
$discount_amount += min($value * $quantity, $original - $discount_amount);
break;
case 'discount':
// 打折 (value = 折扣系数 e.g. 8.5 = 85折)
if ($value > 0 && $value < 10) {
$rate = $value / 10.0;
$save = ($original - $discount_amount) * (1 - $rate);
$discount_amount += $save;
}
break;
case 'gift':
// 买赠 — collect gift config, no price reduction
if (!empty($d['gift_config'])) {
$gift_data = json_decode($d['gift_config'], true);
if (is_array($gift_data)) {
$gifts[] = $gift_data;
}
}
break;
}
$applied[] = [
'term_id' => $d['term_id'],
'name' => $d['name'] ?? '',
'type' => $type,
'value' => $value,
];
}
$discount_amount = min($discount_amount, $original);
return [
'original' => $original,
'discount_amount' => round($discount_amount, 2),
'final_price' => round($original - $discount_amount, 2),
'gifts' => $gifts,
'applied' => $applied,
];
}
/**
* Check if a user meets the user_limit requirement.
*
* @param string $limit 'vip' / 'vip_2' / 'auth'
* @param int $user_id
* @return bool
*/
private function check_user_limit(string $limit, int $user_id): bool {
if (empty($limit)) {
return true;
}
switch ($limit) {
case 'vip':
return function_exists('aksrt_is_vip') ? aksrt_is_vip($user_id) : false;
case 'vip_2':
return function_exists('aksrt_is_vip2') ? aksrt_is_vip2($user_id) : false;
case 'auth':
return function_exists('aksrt_is_verified') ? aksrt_is_verified($user_id) : false;
default:
return true;
}
}
}
+10 -21
View File
@@ -81,17 +81,10 @@ class OrderCenterRest {
}
$payment_method = sanitize_key((string) ($params['payment_method'] ?? ''));
if (class_exists(UserOrders::class)) {
$result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->pay_order(
get_current_user_id(),
$order_number,
$payment_method
);
} else {
if (!class_exists(UserOrders::class)) {
return $this->error(__('订单服务不可用', 'aksrt'), 500);
}
}
$result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('支付失败', 'aksrt')), 400);
@@ -103,13 +96,10 @@ class OrderCenterRest {
public function cancel(\WP_REST_Request $request): \WP_REST_Response {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
if (class_exists(UserOrders::class)) {
$result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$result = \AKSRT\Shop\Shop::get_instance()->cancel_order(get_current_user_id(), $order_number);
} else {
if (!class_exists(UserOrders::class)) {
return $this->error(__('订单服务不可用', 'aksrt'), 500);
}
}
$result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number);
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('取消订单失败', 'aksrt')), 400);
@@ -122,11 +112,10 @@ class OrderCenterRest {
$order_number = sanitize_text_field((string) $request->get_param('order_number'));
$order = null;
if (class_exists(UserOrders::class)) {
$order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
} elseif (class_exists('\AKSRT\Shop\Shop')) {
$order = \AKSRT\Shop\Shop::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
}
if (!class_exists(UserOrders::class)) {
return $this->error(__('订单服务不可用', 'aksrt'), 500);
}
$order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number);
if (!$order) {
return $this->error(__('订单不存在', 'aksrt'), 404);
+5 -74
View File
@@ -63,18 +63,9 @@ trait OrderRepositoryAdmin {
}
$resolved_order_numbers = array_keys($orders);
$shipping_map = class_exists('AKSRT\\Payment\\Shipping')
? Shipping::get_instance()->get_summary_map($resolved_order_numbers)
: [];
$after_sale_map = class_exists('AKSRT\\Payment\\AfterSale')
? AfterSale::get_instance()->get_summary_map($resolved_order_numbers)
: [];
foreach ($orders as $order_number => $order) {
$contexts[$order_number] = $this->build_admin_order_context($order, [
'shipping_summary' => $shipping_map[$order_number] ?? [],
'after_sale_summary' => $after_sale_map[$order_number] ?? [],
]);
$contexts[$order_number] = $this->build_admin_order_context($order, []);
}
return $contexts;
@@ -130,64 +121,6 @@ trait OrderRepositoryAdmin {
/**
* 管理后台订单列表 — 支持 search(order_number / product_type 模糊匹配)
*/
/**
* Paid orders that still need shipping (no shipping row yet).
*
* @return array{items: array<int,array<string,mixed>>, total: int, page: int, max_pages: int}
*/
public function get_pending_shipment_orders(array $args = []): array {
global $wpdb;
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? ''));
$order_table = $this->table();
// Must match Shipping::table() key.
$shipping_table = class_exists('\AKSRT\Core\DBMigrator')
? \AKSRT\Core\DBMigrator::table('order_shipping')
: $wpdb->prefix . 'aksrt_order_shipping';
$where = ["o.status = %s"];
$params = ['paid'];
if ($search !== '') {
$where[] = '(o.order_number LIKE %s OR o.product_type LIKE %s)';
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
}
// NOT EXISTS avoids ONLY_FULL_GROUP_BY issues from LEFT JOIN + GROUP BY.
$where[] = "NOT EXISTS (
SELECT 1 FROM {$shipping_table} s
WHERE s.order_number = o.order_number
)";
$where_sql = 'WHERE ' . implode(' AND ', $where);
$count_sql = "SELECT COUNT(*) FROM {$order_table} o {$where_sql}";
$total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
$sql = "SELECT o.*
FROM {$order_table} o
{$where_sql}
ORDER BY COALESCE(o.paid_at, o.created_at) DESC, o.id DESC
LIMIT %d OFFSET %d";
$query_params = $params;
$query_params[] = $limit;
$query_params[] = $offset;
$rows = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
$items = array_map([$this, 'normalize_row'], (array) $rows);
return [
'items' => $items,
'total' => $total,
'page' => (int) floor($offset / $limit) + 1,
'max_pages' => (int) ceil(max(1, $total) / $limit),
];
}
public function get_orders_admin(array $args = [], string $search = ''): array {
global $wpdb;
@@ -499,8 +432,6 @@ trait OrderRepositoryAdmin {
$discount = (float) ($meta['discount'] ?? 0);
$amount = (float) ($order['amount'] ?? 0);
$subtotal = $this->calculate_order_subtotal($items, max(0, $amount + $discount - $shipping_fee));
$shipping_summary = is_array($related['shipping_summary'] ?? null) ? $related['shipping_summary'] : [];
$after_sale_summary = is_array($related['after_sale_summary'] ?? null) ? $related['after_sale_summary'] : [];
$address['full_text'] = $this->build_address_text($address);
@@ -534,12 +465,12 @@ trait OrderRepositoryAdmin {
'fee' => $shipping_fee,
],
'shipping' => [
'count' => (int) ($shipping_summary['count'] ?? 0),
'latest' => is_array($shipping_summary['latest'] ?? null) ? $shipping_summary['latest'] : [],
'count' => 0,
'latest' => [],
],
'after_sale' => [
'count' => (int) ($after_sale_summary['count'] ?? 0),
'latest' => is_array($after_sale_summary['latest'] ?? null) ? $after_sale_summary['latest'] : [],
'count' => 0,
'latest' => [],
],
'coupon' => $coupon,
'pricing' => [
-371
View File
@@ -1,371 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
class Shipping {
use ShippingPart1;
use ShippingPart2;
private static $instance = null;
const STATUS_SHIPPED = 'shipped';
const STATUS_IN_TRANSIT = 'in_transit';
const STATUS_DELIVERED = 'delivered';
const STATUS_RETURNED = 'returned';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function table(): string {
return \AKSRT\Core\DBMigrator::table('order_shipping');
}
/**
* 创建发货记录
*/
public function create(array $data): int {
global $wpdb;
$order_number = sanitize_text_field((string) ($data['order_number'] ?? ''));
$carrier = sanitize_text_field((string) ($data['carrier'] ?? ''));
$tracking_no = sanitize_text_field((string) ($data['tracking_no'] ?? ''));
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
$operator_id = (int) ($data['operator_id'] ?? 0);
if ($order_number === '' || $carrier === '' || $tracking_no === '') {
return 0;
}
$result = $wpdb->insert($this->table(), [
'order_number' => $order_number,
'carrier' => $carrier,
'tracking_no' => $tracking_no,
'status' => self::STATUS_SHIPPED,
'note' => $note,
'operator_id' => $operator_id ?: null,
'created_at' => current_time('mysql'),
]);
$insert_id = $result ? (int) $wpdb->insert_id : 0;
if ($insert_id > 0) {
$this->sync_order_status($order_number, self::STATUS_SHIPPED, sprintf(
__('发货记录已创建:%1$s %2$s', 'aksrt'),
$this->get_carrier_label($carrier),
$tracking_no
));
// R68: Email notification on shipping creation (§6.6)
$this->send_shipping_notification($order_number, $carrier, $tracking_no, $note);
}
return $insert_id;
}
/**
* 获取发货记录列表
*/
public function get_list(array $args = []): array {
global $wpdb;
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$include_order_context = !empty($args['include_order_context']);
$status = sanitize_key((string) ($args['status'] ?? ''));
$order_number = sanitize_text_field((string) ($args['order_number'] ?? ''));
$date_from = sanitize_text_field((string) ($args['date_from'] ?? ''));
$date_to = sanitize_text_field((string) ($args['date_to'] ?? ''));
$tracking_no = sanitize_text_field((string) ($args['tracking_no'] ?? ''));
$where = [];
$params = [];
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
if ($order_number !== '') {
$where[] = 'order_number = %s';
$params[] = $order_number;
}
if ($tracking_no !== '') {
$where[] = 'tracking_no LIKE %s';
$params[] = '%' . $wpdb->esc_like($tracking_no) . '%';
}
if ($date_from !== '') {
$where[] = 'created_at >= %s';
$params[] = $date_from . ' 00:00:00';
}
if ($date_to !== '') {
$where[] = 'created_at <= %s';
$params[] = $date_to . ' 23:59:59';
}
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
$total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql);
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
$items = array_map([$this, 'decorate_record'], (array) $items);
if ($include_order_context && !empty($items) && class_exists('AKSRT\\Payment\\OrderRepository')) {
$contexts = OrderRepository::get_instance()->get_admin_order_context_map(array_column($items, 'order_number'));
foreach ($items as &$item) {
$item['order_context'] = $contexts[(string) ($item['order_number'] ?? '')] ?? [];
}
unset($item);
}
return [
'items' => $items ?: [],
'total' => $total,
'page' => floor($offset / $limit) + 1,
'max_pages' => ceil($total / $limit),
];
}
/**
* 获取单个发货记录
*/
public function get(int $id): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE id = %d",
$id
), ARRAY_A);
return $row ? $this->decorate_record($row) : null;
}
/**
* 获取订单的发货记录
*/
public function get_by_order(string $order_number): array {
global $wpdb;
$items = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE order_number = %s ORDER BY created_at DESC",
$order_number
), ARRAY_A);
return array_map([$this, 'decorate_record'], (array) $items);
}
/**
* 获取单个发货记录并附带订单上下文。
*/
public function get_with_order_context(int $id): ?array {
$record = $this->get($id);
if (!$record) {
return null;
}
$record['order_context'] = class_exists('AKSRT\\Payment\\OrderRepository')
? OrderRepository::get_instance()->get_admin_order_context((string) ($record['order_number'] ?? ''))
: [];
return $record;
}
/**
* @param array<int, string> $order_numbers
* @return array<string, array<string, mixed>>
*/
public function get_summary_map(array $order_numbers): array {
global $wpdb;
$order_numbers = array_values(array_unique(array_filter(array_map(
static function ($order_number): string {
return sanitize_text_field((string) $order_number);
},
$order_numbers
))));
if (empty($order_numbers)) {
return [];
}
$map = [];
foreach ($order_numbers as $order_number) {
$map[$order_number] = [
'count' => 0,
'latest' => [],
];
}
$placeholders = implode(', ', array_fill(0, count($order_numbers), '%s'));
$sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders}) ORDER BY created_at DESC, id DESC";
$rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A);
foreach ((array) $rows as $row) {
$record = $this->decorate_record($row);
$order_number = (string) ($record['order_number'] ?? '');
if (!isset($map[$order_number])) {
continue;
}
$map[$order_number]['count']++;
if (empty($map[$order_number]['latest'])) {
$map[$order_number]['latest'] = $this->build_summary_item($record);
}
}
return $map;
}
public function batch_update_status(array $ids, string $new_status, string $note = '', int $operator_id = 0): array {
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
$result = [
'updated_ids' => [],
'failed' => [],
];
foreach ($ids as $id) {
$record = $this->get($id);
if (!$record) {
$result['failed'][] = [
'id' => $id,
'reason' => __('发货记录不存在', 'aksrt'),
];
continue;
}
if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) {
$result['failed'][] = [
'id' => $id,
'reason' => __('当前状态不允许批量更新到目标状态', 'aksrt'),
];
continue;
}
if ($this->update_status($id, $new_status, $note, $operator_id)) {
$result['updated_ids'][] = $id;
} else {
$result['failed'][] = [
'id' => $id,
'reason' => __('状态更新失败', 'aksrt'),
];
}
}
return $result;
}
/**
* 更新发货状态
*/
public function update_status(int $id, string $new_status, string $note = '', int $operator_id = 0): bool {
global $wpdb;
$record = $this->get($id);
if (!$record) {
return false;
}
$valid = [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED];
if (!in_array($new_status, $valid, true)) return false;
if (!$this->can_transition_status((string) ($record['status'] ?? ''), $new_status)) {
return false;
}
$update = [
'status' => $new_status,
'updated_at' => current_time('mysql'),
];
if ($note !== '') {
$update['note'] = sanitize_textarea_field($note);
}
if ($operator_id > 0) {
$update['operator_id'] = $operator_id;
}
if ($new_status === self::STATUS_DELIVERED) {
$update['delivered_at'] = current_time('mysql');
}
$updated = (bool) $wpdb->update($this->table(), $update, ['id' => $id]);
if ($updated) {
$this->sync_order_status(
(string) ($record['order_number'] ?? ''),
$new_status,
$note !== '' ? $note : sprintf(__('物流状态更新为:%s', 'aksrt'), $this->get_status_label($new_status))
);
}
return $updated;
}
/**
* 获取统计
*/
public function get_stats(): array {
global $wpdb;
$table = $this->table();
$counts = $wpdb->get_results(
"SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status",
ARRAY_A
);
$counts = array_column($counts, 'cnt', 'status');
$stats = [
'total' => 0,
'shipped' => 0,
'in_transit' => 0,
'delivered' => 0,
'returned' => 0,
];
foreach ($counts as $status => $cnt) {
if (isset($stats[$status])) {
$stats[$status] = (int) $cnt;
}
$stats['total'] += (int) $cnt;
}
return $stats;
}
/**
* 获取常用快递公司列表
*/
public static function get_carrier_options(): array {
return [
['value' => 'sf', 'label' => '顺丰速运'],
['value' => 'yd', 'label' => '韵达快递'],
['value' => 'zt', 'label' => '中通快递'],
['value' => 'yt', 'label' => '圆通速递'],
['value' => 'sto', 'label' => '申通快递'],
['value' => 'jd', 'label' => '京东物流'],
['value' => 'ems', 'label' => 'EMS'],
['value' => 'db', 'label' => '德邦快递'],
['value' => 'yunda', 'label' => '韵达快递'],
['value' => 'other', 'label' => '其他'],
];
}
public static function get_status_options(): array {
return [
['value' => self::STATUS_SHIPPED, 'label' => __('已发货', 'aksrt')],
['value' => self::STATUS_IN_TRANSIT, 'label' => __('运输中', 'aksrt')],
['value' => self::STATUS_DELIVERED, 'label' => __('已签收', 'aksrt')],
['value' => self::STATUS_RETURNED, 'label' => __('已退回', 'aksrt')],
];
}
/**
* Send email notification to order customer when shipping record is created.
*/
}
-371
View File
@@ -1,371 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
trait ShippingPart1 {
private function send_shipping_notification(string $order_number, string $carrier, string $tracking_no, string $note = ''): void {
if (!class_exists('AKSRT\\Payment\\OrderRepository')) {
return;
}
$order = OrderRepository::get_instance()->get($order_number);
if (!$order) {
return;
}
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) {
return;
}
$user = get_userdata($user_id);
if (!$user || empty($user->user_email)) {
return;
}
$carrier_label = $this->get_carrier_label($carrier);
$site_name = get_bloginfo('name');
$subject = sprintf(__('[%s] 您的订单已发货', 'aksrt'), $site_name);
$body = sprintf(
__('您好 %1$s,\n\n您的订单 %2$s 已发货。\n\n物流公司:%3$s\n运单号:%4$s\n%5$s\n\n如有疑问请联系客服。\n\n%s', 'aksrt'),
$user->display_name,
$order_number,
$carrier_label,
$tracking_no,
$note !== '' ? sprintf(__('备注:%s', 'aksrt'), $note) : '',
$site_name
);
wp_mail($user->user_email, $subject, $body);
}
private function decorate_record(array $record): array {
$record['id'] = (int) ($record['id'] ?? 0);
$record['operator_id'] = isset($record['operator_id']) ? (int) $record['operator_id'] : 0;
$record['status_label'] = $this->get_status_label((string) ($record['status'] ?? ''));
$record['carrier_label'] = $this->get_carrier_label((string) ($record['carrier'] ?? ''));
$record['operator'] = $this->get_user_summary((int) ($record['operator_id'] ?? 0));
return $record;
}
private function get_carrier_label(string $carrier): string {
foreach (self::get_carrier_options() as $item) {
if (($item['value'] ?? '') === $carrier) {
return (string) ($item['label'] ?? $carrier);
}
}
return $carrier;
}
private function get_status_label(string $status): string {
foreach (self::get_status_options() as $item) {
if (($item['value'] ?? '') === $status) {
return (string) ($item['label'] ?? $status);
}
}
return $status;
}
private function get_user_summary(int $user_id): array {
if ($user_id <= 0) {
return [];
}
$user = get_userdata($user_id);
if (!$user) {
return [];
}
return [
'id' => $user_id,
'display_name' => (string) $user->display_name,
'user_login' => (string) $user->user_login,
];
}
private function build_summary_item(array $record): array {
return [
'id' => (int) ($record['id'] ?? 0),
'status' => (string) ($record['status'] ?? ''),
'status_label' => (string) ($record['status_label'] ?? ''),
'carrier' => (string) ($record['carrier'] ?? ''),
'carrier_label' => (string) ($record['carrier_label'] ?? ''),
'tracking_no' => (string) ($record['tracking_no'] ?? ''),
'created_at' => (string) ($record['created_at'] ?? ''),
'updated_at' => (string) ($record['updated_at'] ?? ''),
'delivered_at' => (string) ($record['delivered_at'] ?? ''),
];
}
private function can_transition_status(string $current_status, string $new_status): bool {
$map = [
self::STATUS_SHIPPED => [self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED, self::STATUS_RETURNED],
self::STATUS_IN_TRANSIT => [self::STATUS_DELIVERED, self::STATUS_RETURNED],
self::STATUS_DELIVERED => [],
self::STATUS_RETURNED => [],
];
return in_array($new_status, $map[$current_status] ?? [], true);
}
private function sync_order_status(string $order_number, string $shipping_status, string $note = ''): void {
if ($order_number === '' || !class_exists('AKSRT\\Payment\\OrderRepository') || !class_exists('AKSRT\\Payment\\Order')) {
return;
}
if (!in_array($shipping_status, [self::STATUS_SHIPPED, self::STATUS_IN_TRANSIT, self::STATUS_DELIVERED], true)) {
return;
}
OrderRepository::get_instance()->update_status($order_number, Order::STATUS_DELIVERED, [
'note' => $note,
]);
}
/* ============================================================
* Express Query API (§6.6 快递查询)
*
* Supports kuaidi100, aliyun, kdniao.
* Traces cached in order_shipping.express_data JSON.
* Refresh interval from shop settings.
* ============================================================ */
public function query_express(int $shipping_id, bool $force = false): array {
global $wpdb;
$record = $this->get($shipping_id);
if (!$record) {
return ['success' => false, 'message' => __('发货记录不存在', 'aksrt')];
}
$tracking_no = (string) ($record['tracking_no'] ?? '');
$carrier = (string) ($record['carrier'] ?? '');
if ($tracking_no === '' || $carrier === '') {
return ['success' => false, 'message' => __('缺少运单号或物流公司', 'aksrt')];
}
$cached = $this->get_express_cache($shipping_id);
if (!$force && !empty($cached) && !$this->should_refresh_cache($cached)) {
return ['success' => true, 'data' => $cached];
}
if (!empty($cached['signed'])) {
return ['success' => true, 'data' => $cached];
}
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$api_sdk = sanitize_key((string) ($shop_settings['express']['api_sdk'] ?? 'kuaidi100'));
$result = $this->call_express_api($api_sdk, $carrier, $tracking_no, $shop_settings);
if ($result['success']) {
$express_data = [
'query_time' => current_time('mysql'),
'traces' => $result['traces'] ?? [],
'state' => $result['state'] ?? 'unknown',
'signed' => !empty($result['signed']),
'api' => $api_sdk,
];
$wpdb->update(
$this->table(),
['note' => wp_json_encode($express_data, JSON_UNESCAPED_UNICODE)],
['id' => $shipping_id],
['%s'],
['%d']
);
if (!empty($result['signed']) && ($record['status'] ?? '') !== self::STATUS_DELIVERED) {
$this->update_status($shipping_id, self::STATUS_DELIVERED, __('快递已签收(API检测)', 'aksrt'));
}
return ['success' => true, 'data' => $express_data];
}
return $result;
}
private function get_express_cache(int $shipping_id): array {
global $wpdb;
$note = $wpdb->get_var($wpdb->prepare(
"SELECT note FROM {$this->table()} WHERE id = %d",
$shipping_id
));
if (!$note) return [];
$data = json_decode($note, true);
return is_array($data) && isset($data['traces']) ? $data : [];
}
private function should_refresh_cache(array $cached): bool {
if (empty($cached['query_time'])) return true;
$shop = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$interval = max(30, min(240, (int) ($shop['express']['query_interval'] ?? 60)));
$last = strtotime($cached['query_time']);
return (time() - $last) > ($interval * 60);
}
private function call_express_api(string $sdk, string $carrier, string $tracking_no, array $settings): array {
$carrier_code = $this->map_carrier_code($sdk, $carrier);
if ($sdk === 'kuaidi100') {
return $this->query_kuaidi100($carrier_code, $tracking_no, $settings);
}
if ($sdk === 'kdniao') {
return $this->query_kdniao($carrier_code, $tracking_no, $settings);
}
if ($sdk === 'aliyun') {
return $this->query_aliyun($carrier_code, $tracking_no, $settings);
}
return ['success' => false, 'message' => __('未配置快递查询接口', 'aksrt')];
}
private function query_kuaidi100(string $com, string $num, array $settings): array {
$key = sanitize_text_field((string) ($settings['express']['kuaidi100_key'] ?? ''));
$customer = sanitize_text_field((string) ($settings['express']['kuaidi100_customer'] ?? ''));
if ($key === '' || $customer === '') {
return ['success' => false, 'message' => __('快递100 API 未配置', 'aksrt')];
}
$param = wp_json_encode(['com' => $com, 'num' => $num]);
$sign = strtoupper(md5($param . $key . $customer));
$response = wp_remote_post('https://poll.kuaidi100.com/poll/query.do', [
'body' => ['customer' => $customer, 'sign' => $sign, 'param' => $param],
'timeout' => 10,
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($body) || (int) ($body['status'] ?? 0) !== 200) {
return ['success' => false, 'message' => (string) ($body['message'] ?? __('查询失败', 'aksrt'))];
}
$state = (string) ($body['state'] ?? '0');
$signed = $state === '3';
$traces = [];
foreach ((array) ($body['data'] ?? []) as $item) {
$traces[] = [
'time' => (string) ($item['ftime'] ?? $item['time'] ?? ''),
'context' => (string) ($item['context'] ?? ''),
];
}
return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed];
}
private function query_kdniao(string $code, string $num, array $settings): array {
$app_id = sanitize_text_field((string) ($settings['express']['kdniao_app_id'] ?? ''));
$app_key = sanitize_text_field((string) ($settings['express']['kdniao_app_key'] ?? ''));
if ($app_id === '' || $app_key === '') {
return ['success' => false, 'message' => __('快递鸟 API 未配置', 'aksrt')];
}
$request_data = wp_json_encode(['ShipperCode' => $code, 'LogisticCode' => $num]);
$data_sign = urlencode(base64_encode(md5($request_data . $app_key)));
$response = wp_remote_post('https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx', [
'body' => [
'RequestData' => urlencode($request_data),
'EBusinessID' => $app_id,
'RequestType' => '1002',
'DataSign' => $data_sign,
'DataType' => '2',
],
'timeout' => 10,
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($body) || !($body['Success'] ?? false)) {
return ['success' => false, 'message' => (string) ($body['Reason'] ?? __('查询失败', 'aksrt'))];
}
$state = (string) ($body['State'] ?? '0');
$signed = $state === '3';
$traces = [];
foreach ((array) ($body['Traces'] ?? []) as $item) {
$traces[] = [
'time' => (string) ($item['AcceptTime'] ?? ''),
'context' => (string) ($item['AcceptStation'] ?? ''),
];
}
return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed];
}
private function query_aliyun(string $code, string $num, array $settings): array {
$app_code = sanitize_text_field((string) ($settings['express']['aliyun_app_code'] ?? ''));
if ($app_code === '') {
return ['success' => false, 'message' => __('阿里云快递 API 未配置', 'aksrt')];
}
$url = add_query_arg(['no' => $num, 'type' => $code], 'https://wuliu.market.alicloudapi.com/kdi');
$response = wp_remote_get($url, [
'headers' => ['Authorization' => 'APPCODE ' . $app_code],
'timeout' => 10,
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($body) || (int) ($body['status'] ?? -1) !== 0) {
return ['success' => false, 'message' => (string) ($body['msg'] ?? __('查询失败', 'aksrt'))];
}
$result_data = $body['result'] ?? [];
$state = (string) ($result_data['deliverystatus'] ?? '0');
$signed = $state === '3';
$traces = [];
foreach ((array) ($result_data['list'] ?? []) as $item) {
$traces[] = [
'time' => (string) ($item['time'] ?? ''),
'context' => (string) ($item['status'] ?? ''),
];
}
return ['success' => true, 'traces' => $traces, 'state' => $state, 'signed' => $signed];
}
private function map_carrier_code(string $sdk, string $carrier): string {
$kuaidi100_map = [
'sf' => 'shunfeng', 'yd' => 'yunda', 'zt' => 'zhongtong', 'yt' => 'yuantong',
'sto' => 'shentong', 'jd' => 'jd', 'ems' => 'ems', 'db' => 'debangkuaidi',
'yunda' => 'yunda',
];
$kdniao_map = [
'sf' => 'SF', 'yd' => 'YD', 'zt' => 'ZTO', 'yt' => 'YTO',
'sto' => 'STO', 'jd' => 'JD', 'ems' => 'EMS', 'db' => 'DBL',
'yunda' => 'YD',
];
if ($sdk === 'kuaidi100') {
return $kuaidi100_map[$carrier] ?? $carrier;
}
if ($sdk === 'kdniao') {
return $kdniao_map[$carrier] ?? strtoupper($carrier);
}
return $carrier;
}
}
-152
View File
@@ -1,152 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
trait ShippingPart2 {
public function build_express_timeline(int $shipping_id): array {
$record = $this->get($shipping_id);
if (!$record) return [];
$cached = $this->get_express_cache($shipping_id);
$timeline = [];
$timeline[] = [
'time' => (string) ($record['created_at'] ?? ''),
'label' => sprintf(__('商家已发货:%s %s', 'aksrt'), $record['carrier_label'] ?? '', $record['tracking_no'] ?? ''),
'type' => 'shipped',
];
if (!empty($cached['traces'])) {
foreach ($cached['traces'] as $trace) {
$timeline[] = [
'time' => (string) ($trace['time'] ?? ''),
'label' => (string) ($trace['context'] ?? ''),
'type' => 'trace',
];
}
}
if (!empty($cached['signed'])) {
$timeline[] = [
'time' => (string) ($record['delivered_at'] ?? ($cached['query_time'] ?? '')),
'label' => __('快递已签收', 'aksrt'),
'type' => 'signed',
];
}
usort($timeline, static fn($a, $b) => strcmp($a['time'] ?? '', $b['time'] ?? ''));
return $timeline;
}
/* ============================================================
* Shipping Fee Resolution (§6.6 运费模板)
*
* 3 types: free / fixed / amount (满额包邮)
* Inheritance: product → category → theme defaults
* ============================================================ */
public function resolve_shipping_fee(int $product_id, float $order_amount = 0, array $address = []): array {
$config = $this->get_product_shipping_config($product_id);
$fee_type = (string) ($config['fee_type'] ?? 'fixed');
$fee_amount = (float) ($config['fee_amount'] ?? 0);
$free_threshold = (float) ($config['free_threshold'] ?? 0);
if ($fee_type === 'free') {
return ['fee' => 0.0, 'type' => 'free', 'label' => __('包邮', 'aksrt')];
}
if ($fee_type === 'amount' && $free_threshold > 0 && $order_amount >= $free_threshold) {
return ['fee' => 0.0, 'type' => 'free_threshold', 'label' => sprintf(__('满%.0f包邮', 'aksrt'), $free_threshold)];
}
$region_fee = $this->resolve_region_fee($config, $address);
if ($region_fee !== null) {
return ['fee' => $region_fee, 'type' => 'region', 'label' => __('地区运费', 'aksrt')];
}
return ['fee' => $fee_amount, 'type' => 'fixed', 'label' => sprintf(__('运费 ¥%.2f', 'aksrt'), $fee_amount)];
}
private function get_product_shipping_config(int $product_id): array {
$meta = get_post_meta($product_id, '_aksrt_shipping_config', true);
if (is_array($meta) && !empty($meta['fee_type'])) {
return $meta;
}
if (class_exists('\AKSRT\Shop\TermMeta')) {
$cat_config = \AKSRT\Shop\TermMeta::resolve_inheritance($product_id, 'shipping_fee');
if (is_array($cat_config) && !empty($cat_config['fee_type'])) {
return $cat_config;
}
}
if (class_exists('\AKSRT\Settings\Settings')) {
$shop = \AKSRT\Settings\Settings::get_shop_settings();
return [
'fee_type' => sanitize_key((string) ($shop['defaults']['shipping_fee_type'] ?? 'fixed')),
'fee_amount' => (float) ($shop['defaults']['shipping_fee_amount'] ?? 12),
'free_threshold' => (float) ($shop['defaults']['shipping_free_threshold'] ?? 0),
'region_fees' => (array) ($shop['defaults']['region_fees'] ?? []),
];
}
return ['fee_type' => 'fixed', 'fee_amount' => 12, 'free_threshold' => 0, 'region_fees' => []];
}
private function resolve_region_fee(array $config, array $address): ?float {
$region_fees = (array) ($config['region_fees'] ?? []);
if (empty($region_fees) || empty($address['province'])) {
return null;
}
$province = (string) $address['province'];
foreach ($region_fees as $region) {
$provinces = (array) ($region['provinces'] ?? []);
if (in_array($province, $provinces, true)) {
return (float) ($region['fee'] ?? 0);
}
}
return null;
}
public static function get_china_regions(): array {
$file = get_template_directory() . '/assets/data/china-regions.json';
if (!file_exists($file)) {
return self::get_builtin_provinces();
}
$data = json_decode((string) file_get_contents($file), true);
return is_array($data) ? $data : self::get_builtin_provinces();
}
private static function get_builtin_provinces(): array {
return [
['code' => '11', 'name' => '北京市'], ['code' => '12', 'name' => '天津市'],
['code' => '13', 'name' => '河北省'], ['code' => '14', 'name' => '山西省'],
['code' => '15', 'name' => '内蒙古自治区'], ['code' => '21', 'name' => '辽宁省'],
['code' => '22', 'name' => '吉林省'], ['code' => '23', 'name' => '黑龙江省'],
['code' => '31', 'name' => '上海市'], ['code' => '32', 'name' => '江苏省'],
['code' => '33', 'name' => '浙江省'], ['code' => '34', 'name' => '安徽省'],
['code' => '35', 'name' => '福建省'], ['code' => '36', 'name' => '江西省'],
['code' => '37', 'name' => '山东省'], ['code' => '41', 'name' => '河南省'],
['code' => '42', 'name' => '湖北省'], ['code' => '43', 'name' => '湖南省'],
['code' => '44', 'name' => '广东省'], ['code' => '45', 'name' => '广西壮族自治区'],
['code' => '46', 'name' => '海南省'], ['code' => '50', 'name' => '重庆市'],
['code' => '51', 'name' => '四川省'], ['code' => '52', 'name' => '贵州省'],
['code' => '53', 'name' => '云南省'], ['code' => '54', 'name' => '西藏自治区'],
['code' => '61', 'name' => '陕西省'], ['code' => '62', 'name' => '甘肃省'],
['code' => '63', 'name' => '青海省'], ['code' => '64', 'name' => '宁夏回族自治区'],
['code' => '65', 'name' => '新疆维吾尔自治区'],
['code' => '71', 'name' => '台湾省'],
['code' => '81', 'name' => '香港特别行政区'],
['code' => '82', 'name' => '澳门特别行政区'],
];
}
}
+8 -36
View File
@@ -52,9 +52,7 @@ class UserOrders {
if ($status !== '') {
$repo_args['status'] = $status;
}
if (in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_PENDING_REVIEW], true)) {
$repo_args['product_type'] = \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP;
} elseif ($product_type !== '' && $product_type !== 'all') {
if ($product_type !== '' && $product_type !== 'all') {
$repo_args['product_type'] = $product_type;
}
@@ -124,8 +122,6 @@ class UserOrders {
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
$status = (string) ($order['status'] ?? Order::STATUS_PENDING);
$product_id = (int) ($order['product_id'] ?? 0);
$is_shop = $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_SHOP
|| $product_type === 'shop';
$currency = sanitize_key((string) ($order['currency'] ?? 'CNY'));
$is_points = $currency === 'points'
|| sanitize_key((string) ($order['payment_method'] ?? '')) === 'points'
@@ -139,17 +135,11 @@ class UserOrders {
$permalink = $this->resolve_order_permalink($product_type, $product_id, $meta);
$order_items = [];
if ($is_shop && class_exists('\AKSRT\Shop\Shop')) {
$order_items = \AKSRT\Shop\Shop::get_instance()->get_order_items($order_number);
}
$has_structured_items = !empty($order_items);
$product = [];
if ($has_structured_items && class_exists('\AKSRT\Shop\Shop')) {
$product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0));
} elseif ($is_shop && $product_id > 0 && class_exists('\AKSRT\Shop\Shop')) {
$product = \AKSRT\Shop\Shop::get_instance()->build_product_card_view($product_id);
}
// Shop product cards were removed with the shop feature; content/VIP
// orders fall through to the shared meta-based fields below.
if (!empty($product['title'])) {
$title = (string) $product['title'];
@@ -163,20 +153,6 @@ class UserOrders {
$shipping_records = [];
$after_sale_items = [];
if ($is_shop) {
if (class_exists(Shipping::class)) {
$shipping_records = Shipping::get_instance()->get_by_order($order_number);
}
if (class_exists(AfterSale::class)) {
$as = AfterSale::get_instance()->get_list([
'limit' => 20,
'offset' => 0,
'order_number' => $order_number,
]);
$after_sale_items = $as['items'] ?? [];
}
}
$subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount'] ?? 0);
$shipping_fee = (float) ($meta['shipping_fee'] ?? 0);
$discount = (float) ($meta['discount'] ?? 0);
@@ -186,10 +162,10 @@ class UserOrders {
$can_pay = in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true)
&& $payment_value !== 'manual_review';
$can_cancel = in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true);
$can_after_sale = $is_shop && in_array($status, [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED], true);
$can_receipt = $is_shop && $status === Order::STATUS_DELIVERED;
$can_review = $is_shop && $status === Order::STATUS_PENDING_REVIEW;
$can_rebuy = $is_shop && in_array($status, [Order::STATUS_COMPLETED, Order::STATUS_PENDING_REVIEW], true);
$can_after_sale = false;
$can_receipt = false;
$can_review = false;
$can_rebuy = false;
// Order detail modal is always available; product permalink is optional.
$can_view = true;
$can_view_product = $permalink !== '' && in_array($status, [
@@ -306,11 +282,7 @@ class UserOrders {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
// Prefer shop checkout when available (handles points + shop meta).
if (class_exists('\AKSRT\Shop\Shop')) {
return \AKSRT\Shop\Shop::get_instance()->pay_order($user_id, $order_number, $payment_method);
}
// Resume via shared gateway stack (any product_type; shop removed).
$repo = OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
-9
View File
@@ -27,15 +27,6 @@ trait DomainGroups {
Integration::update_wechat_settings($settings);
}
/**
* 获取商城设置
*
* @return array
*/
public static function get_shop_settings(): array {
return ForumShop::get_shop_settings();
}
/**
* 获取文档模式设置
*/
-185
View File
@@ -1,185 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Settings;
defined('ABSPATH') || exit;
class ForumShop {
/**
* 获取商城设置
*
* @return array
*/
public static function get_shop_settings(): array {
$s = Settings::get_all();
// -- basic / general --
$enabled = !empty($s['shop_s']);
$home_url = esc_url_raw((string) ($s['shop_home_url'] ?? ''));
// breadcrumbs
$breadcrumbs_s = !isset($s['shop_breadcrumbs_s']) || !empty($s['shop_breadcrumbs_s']);
$breadcrumbs_home = !isset($s['shop_breadcrumbs_home']) || !empty($s['shop_breadcrumbs_home']);
$breadcrumbs_shop_home = !isset($s['shop_breadcrumbs_shop_home']) || !empty($s['shop_breadcrumbs_shop_home']);
$breadcrumbs_shop_home_name = sanitize_text_field((string) ($s['shop_breadcrumbs_shop_home_name'] ?? '商城首页'));
// rewrite
$product_rewrite_slug = sanitize_key((string) ($s['shop_product_rewrite_slug'] ?? 'product'));
$cart_rewrite_slug = sanitize_key((string) ($s['shop_cart_rewrite_slug'] ?? 'cart'));
$rewrite_suffix_html = !empty($s['shop_rewrite_suffix_html_s']);
// -- list --
$list_orderby = sanitize_key((string) ($s['shop_list_orderby'] ?? 'date'));
if (!in_array($list_orderby, ['date', 'sales', 'price_asc', 'price_desc', 'comments'], true)) {
$list_orderby = 'date';
}
$list_count = max(4, min(100, (int) ($s['shop_list_count'] ?? 12)));
$list_paging = sanitize_key((string) ($s['shop_list_paging'] ?? 'number'));
if (!in_array($list_paging, ['number', 'ajax'], true)) {
$list_paging = 'number';
}
$list_ias_s = !empty($s['shop_list_ias_s']);
$list_ias_max = max(1, min(50, (int) ($s['shop_list_ias_max'] ?? 5)));
$list_style = sanitize_key((string) ($s['shop_list_style'] ?? 'default'));
if (!in_array($list_style, ['default', 'card-lg', 'card-sm'], true)) {
$list_style = 'default';
}
$sales_show = sanitize_key((string) ($s['shop_sales_show'] ?? 'show'));
if (!in_array($sales_show, ['show', 'hide', 'threshold'], true)) {
$sales_show = 'show';
}
$sales_show_min = max(0, min(99999, (int) ($s['shop_sales_show_min'] ?? 10)));
// -- contact --
$contact_msg_s = !isset($s['shop_contact_msg_s']) || !empty($s['shop_contact_msg_s']);
$contact_name = sanitize_text_field((string) ($s['shop_contact_name'] ?? '联系客服'));
$contact_note = sanitize_textarea_field((string) ($s['shop_contact_note'] ?? ''));
$contact_more = sanitize_textarea_field((string) ($s['shop_contact_more'] ?? ''));
// -- single --
$content_layout = sanitize_key((string) ($s['shop_content_layout'] ?? 'adapt'));
if (!in_array($content_layout, ['adapt', 'full', 'sidebar'], true)) {
$content_layout = 'adapt';
}
$content_show_bg = !empty($s['shop_content_show_bg']);
$single_tab = sanitize_textarea_field((string) ($s['shop_single_tab'] ?? ''));
$content_after = wp_kses_post((string) ($s['shop_content_after'] ?? ''));
$related_s = !isset($s['shop_single_related_s']) || !empty($s['shop_single_related_s']);
$related_opt = sanitize_textarea_field((string) ($s['shop_single_related_opt'] ?? ''));
$main_image_default = esc_url_raw((string) ($s['shop_main_image_default'] ?? ''));
// -- product defaults --
$limit_buy = max(-1, min(9999, (int) ($s['shop_limit_buy'] ?? -1)));
$guest_buy = !empty($s['shop_guest_buy']);
$email_fill = !empty($s['shop_email_fill']);
$rebate = !isset($s['shop_rebate']) || !empty($s['shop_rebate']);
$shipping_fee = sanitize_key((string) ($s['shop_shipping_fee'] ?? 'free'));
if (!in_array($shipping_fee, ['free', 'fixed', 'amount'], true)) {
$shipping_fee = 'free';
}
$after_sale = sanitize_key((string) ($s['shop_after_sale'] ?? 'refund_return'));
if (!in_array($after_sale, ['refund', 'refund_return', 'none'], true)) {
$after_sale = 'refund_return';
}
$service = sanitize_textarea_field((string) ($s['shop_service'] ?? ''));
$params_default = sanitize_textarea_field((string) ($s['shop_product_params_default'] ?? ''));
// -- shipping config --
$manual_address = !empty($s['shop_manual_address']);
$express_api_sdk = sanitize_key((string) ($s['shop_express_api_sdk'] ?? 'kuaidi100'));
if (!in_array($express_api_sdk, ['kuaidi100', 'aliyun', 'kdniao'], true)) {
$express_api_sdk = 'kuaidi100';
}
$express_query_interval = max(30, min(240, (int) ($s['shop_express_query_interval'] ?? 60)));
$express_kdniao_opt = sanitize_textarea_field((string) ($s['shop_express_kdniao_opt'] ?? ''));
$express_kuaidi100_opt = sanitize_textarea_field((string) ($s['shop_express_kuaidi100_opt'] ?? ''));
$express_aliyun_opt = sanitize_textarea_field((string) ($s['shop_express_aliyun_opt'] ?? ''));
$shipping_company = sanitize_textarea_field((string) ($s['shop_shipping_company'] ?? ''));
// -- timing --
$receipt_max_day = max(1, min(60, (int) ($s['shop_order_receipt_max_day'] ?? 15)));
$comment_max_day = max(1, min(60, (int) ($s['shop_comment_max_day'] ?? 7)));
$comment_placeholder = sanitize_text_field((string) ($s['shop_comment_placeholder'] ?? ''));
$comment_img_num = max(0, min(9, (int) ($s['shop_comment_img_num'] ?? 3)));
$after_sale_return_max_day = max(1, min(30, (int) ($s['shop_after_sale_return_express_max_day'] ?? 7)));
$after_sale_reason_refund = sanitize_textarea_field((string) ($s['shop_after_sale_reason_refund'] ?? ''));
$after_sale_reason_return = sanitize_textarea_field((string) ($s['shop_after_sale_reason_return'] ?? ''));
$author_show = !isset($s['shop_author_show']) || !empty($s['shop_author_show']);
return [
'enabled' => $enabled,
'home_url' => $home_url,
'breadcrumbs' => [
'enabled' => $breadcrumbs_s,
'home' => $breadcrumbs_home,
'shop_home' => $breadcrumbs_shop_home,
'shop_home_name' => $breadcrumbs_shop_home_name,
],
'rewrite' => [
'product_slug' => $product_rewrite_slug,
'cart_slug' => $cart_rewrite_slug,
'suffix_html' => $rewrite_suffix_html,
],
'list' => [
'orderby' => $list_orderby,
'count' => $list_count,
'paging' => $list_paging,
'ias_s' => $list_ias_s,
'ias_max' => $list_ias_max,
'style' => $list_style,
],
'sales' => [
'show' => $sales_show,
'show_min' => $sales_show_min,
],
'contact' => [
'msg_s' => $contact_msg_s,
'name' => $contact_name,
'note' => $contact_note,
'more' => $contact_more,
],
'single' => [
'content_layout' => $content_layout,
'content_show_bg' => $content_show_bg,
'tab' => $single_tab,
'content_after' => $content_after,
'related_s' => $related_s,
'related_opt' => $related_opt,
'main_image_default' => $main_image_default,
],
'product_defaults' => [
'limit_buy' => $limit_buy,
'guest_buy' => $guest_buy,
'email_fill' => $email_fill,
'rebate' => $rebate,
'shipping_fee' => $shipping_fee,
'after_sale' => $after_sale,
'service' => $service,
'params_default' => $params_default,
],
'shipping' => [
'manual_address' => $manual_address,
'express_api_sdk' => $express_api_sdk,
'express_query_interval' => $express_query_interval,
'express_kdniao_opt' => $express_kdniao_opt,
'express_kuaidi100_opt' => $express_kuaidi100_opt,
'express_aliyun_opt' => $express_aliyun_opt,
'shipping_company' => $shipping_company,
],
'timing' => [
'receipt_max_day' => $receipt_max_day,
'comment_max_day' => $comment_max_day,
'comment_placeholder' => $comment_placeholder,
'comment_img_num' => $comment_img_num,
'after_sale_return_max_day' => $after_sale_return_max_day,
'after_sale_reason_refund' => $after_sale_reason_refund,
'after_sale_reason_return' => $after_sale_reason_return,
],
'author_show' => $author_show,
];
}
}
-204
View File
@@ -1,204 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class AddressService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
/**
* 获取用户地址簿(公开接口)
*/
public function get_address_book(int $user_id): array {
return $this->get_user_address_book($user_id);
}
public function get_user_address_book(int $user_id): array {
$saved = get_user_meta($user_id, 'aksrt_shop_addresses', true);
if (is_array($saved) && !empty($saved)) {
return array_values(array_filter(array_map([$this, 'sanitize_address'], $saved)));
}
return [];
}
private function sanitize_address($address): array {
if (!is_array($address)) {
return [];
}
$name = sanitize_text_field((string) ($address['name'] ?? ''));
$phone = sanitize_text_field((string) ($address['phone'] ?? ''));
$province = sanitize_text_field((string) ($address['province'] ?? ''));
$city = sanitize_text_field((string) ($address['city'] ?? ''));
$district = sanitize_text_field((string) ($address['district'] ?? ''));
$street = sanitize_text_field((string) ($address['address'] ?? ''));
return [
'id' => (int) ($address['id'] ?? 0),
'name' => $name,
'phone' => $phone,
'province' => $province,
'city' => $city,
'district' => $district,
'address' => $street,
'is_default' => !empty($address['is_default']),
'full_text' => trim($name . ' / ' . $province . $city . $district . $street),
];
}
/**
* 新增地址
*
* @param int $user_id
* @param array $data 地址数据(name, phone, province, city, district, address, is_default)
* @return array{success:bool, address:array|null, error:string|null}
*/
public function add_address(int $user_id, array $data): array {
$address = $this->sanitize_address($data);
if ($address['name'] === '' || $address['phone'] === '' || $address['address'] === '') {
return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')];
}
$book = $this->get_user_address_book($user_id);
$address['id'] = $this->next_address_id($book);
if (!empty($data['is_default']) || empty($book)) {
foreach ($book as &$item) {
$item['is_default'] = false;
}
$address['is_default'] = true;
}
$book[] = $address;
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'address' => $address, 'error' => null];
}
/**
* 更新地址
*
* @param int $user_id
* @param int $address_id
* @param array $data
* @return array{success:bool, address:array|null, error:string|null}
*/
public function update_address(int $user_id, int $address_id, array $data): array {
$book = $this->get_user_address_book($user_id);
$found = false;
foreach ($book as &$item) {
if ((int) ($item['id'] ?? 0) !== $address_id) {
continue;
}
$found = true;
$merged = array_merge($item, $this->sanitize_address(array_merge($item, $data)));
$merged['id'] = $address_id;
if ($merged['name'] === '' || $merged['phone'] === '' || $merged['address'] === '') {
return ['success' => false, 'address' => null, 'error' => __('姓名、手机号和详细地址为必填项。', 'aksrt')];
}
if (!empty($data['is_default'])) {
foreach ($book as &$clear) {
$clear['is_default'] = false;
}
$merged['is_default'] = true;
}
$item = $merged;
break;
}
if (!$found) {
return ['success' => false, 'address' => null, 'error' => __('地址不存在。', 'aksrt')];
}
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'address' => $merged, 'error' => null];
}
/**
* 删除地址
*
* @param int $user_id
* @param int $address_id
* @return array{success:bool, error:string|null}
*/
public function delete_address(int $user_id, int $address_id): array {
$book = $this->get_user_address_book($user_id);
$found = false;
$was_default = false;
$filtered = [];
foreach ($book as $item) {
if ((int) ($item['id'] ?? 0) === $address_id) {
$found = true;
$was_default = !empty($item['is_default']);
continue;
}
$filtered[] = $item;
}
if (!$found) {
return ['success' => false, 'error' => __('地址不存在。', 'aksrt')];
}
if ($was_default && !empty($filtered)) {
$filtered[0]['is_default'] = true;
}
update_user_meta($user_id, 'aksrt_shop_addresses', array_values($filtered));
return ['success' => true, 'error' => null];
}
/**
* 设置默认地址
*
* @param int $user_id
* @param int $address_id
* @return array{success:bool, error:string|null}
*/
public function set_default_address(int $user_id, int $address_id): array {
$book = $this->get_user_address_book($user_id);
$found = false;
foreach ($book as &$item) {
$item['is_default'] = ((int) ($item['id'] ?? 0) === $address_id);
if ($item['is_default']) {
$found = true;
}
}
if (!$found) {
return ['success' => false, 'error' => __('地址不存在。', 'aksrt')];
}
update_user_meta($user_id, 'aksrt_shop_addresses', $book);
return ['success' => true, 'error' => null];
}
/**
* 生成下一个地址 ID
*/
private function next_address_id(array $book): int {
$max = 0;
foreach ($book as $item) {
$max = max($max, (int) ($item['id'] ?? 0));
}
return $max + 1;
}
}
-173
View File
@@ -1,173 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class CartService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
public function get_cart(int $user_id): array {
if ($user_id <= 0) {
return [];
}
$cart = get_user_meta($user_id, 'shop_cart', true);
return is_array($cart) ? $cart : [];
}
public function get_cart_count(int $user_id): int {
$cart = $this->get_cart($user_id);
$count = 0;
foreach ($cart as $opts) {
if (is_array($opts)) {
foreach ($opts as $qty) {
$count += (int) $qty;
}
}
}
return $count;
}
public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
if ($user_id <= 0 || $product_id <= 0 || $count <= 0) {
return ['error' => 'invalid_params'];
}
$validation = $this->validate_cart_item($product_id, $options_string);
if ($validation !== true) {
return ['error' => $validation];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if (!isset($cart[$pid_key]) || !is_array($cart[$pid_key])) {
$cart[$pid_key] = [];
}
$cart[$pid_key][$opt_key] = ((int) ($cart[$pid_key][$opt_key] ?? 0)) + $count;
// Enforce limit_buy if set
$limit = (int) get_post_meta($product_id, '_aksrt_limit_buy', true);
if ($limit > 0 && $cart[$pid_key][$opt_key] > $limit) {
$cart[$pid_key][$opt_key] = $limit;
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
if ($user_id <= 0 || $product_id <= 0 || $count <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if (isset($cart[$pid_key][$opt_key])) {
$cart[$pid_key][$opt_key] -= $count;
if ($cart[$pid_key][$opt_key] <= 0) {
unset($cart[$pid_key][$opt_key]);
}
}
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
$pid_key = (string) $product_id;
$opt_key = $options_string !== '' ? $options_string : '_default';
if ($options_string === '' && isset($cart[$pid_key])) {
// Remove all options for this product
unset($cart[$pid_key]);
} elseif (isset($cart[$pid_key][$opt_key])) {
unset($cart[$pid_key][$opt_key]);
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function remove_multi_from_cart(int $user_id, array $items): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
$cart = $this->get_cart($user_id);
foreach ($items as $item) {
$pid_key = (string) ($item['product_id'] ?? 0);
$opt_key = !empty($item['options_string']) ? $item['options_string'] : '_default';
if (isset($cart[$pid_key][$opt_key])) {
unset($cart[$pid_key][$opt_key]);
if (empty($cart[$pid_key])) {
unset($cart[$pid_key]);
}
}
}
update_user_meta($user_id, 'shop_cart', $cart);
return ['success' => true, 'cart_count' => $this->get_cart_count($user_id)];
}
public function empty_cart(int $user_id): array {
if ($user_id <= 0) {
return ['error' => 'invalid_params'];
}
delete_user_meta($user_id, 'shop_cart');
return ['success' => true, 'cart_count' => 0];
}
/**
* Validate a product for cart addition.
* Returns true if valid, or a string error code.
*/
public function validate_cart_item(int $product_id, string $options_string = '') {
$post = get_post($product_id);
if (!$post || $post->post_type !== 'aksrt_product') {
return 'invalid_product';
}
if ($post->post_status !== 'publish') {
return 'product_not_published';
}
// Check stock (unless unlimited)
$stock = (int) get_post_meta($product_id, '_aksrt_stock', true);
if ($stock > -1 && $stock <= 0) {
return 'out_of_stock';
}
// Check guest_buy — if disabled and user not logged in
if (!is_user_logged_in()) {
$guest = get_post_meta($product_id, '_aksrt_guest_buy', true);
if ($guest === '0' || $guest === '') {
// Fall back to inheritance chain via TermMeta
if (class_exists('AKSRT\Shop\TermMeta')) {
$resolved = TermMeta::resolve_inheritance($product_id, 'guest_buy');
if (empty($resolved)) {
return 'login_required';
}
}
}
}
return true;
}
}
-414
View File
@@ -1,414 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class CheckoutService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
public function preview_checkout(int $user_id, array $payload): array {
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录后再结算。', 'aksrt')];
}
$items = $this->normalize_checkout_items($payload['items'] ?? []);
if (empty($items)) {
return ['success' => false, 'message' => __('请选择至少一个商品。', 'aksrt')];
}
$product_views = [];
$subtotal = 0.0;
foreach ($items as $item) {
$product = $this->shop->get_product_detail_view((int) $item['product_id']);
if (empty($product) || empty($product['purchase_enabled'])) {
return ['success' => false, 'message' => __('存在不可购买商品,请刷新后重试。', 'aksrt')];
}
if ((int) $product['stock'] < (int) $item['quantity']) {
return ['success' => false, 'message' => sprintf(__('商品 %s 库存不足。', 'aksrt'), $product['title'])];
}
$line_total = round(((float) $product['price']) * (int) $item['quantity'], 2);
// R69: Apply discount engine (shop_discount taxonomy campaigns)
$discount_result = ['discount_amount' => 0, 'gifts' => [], 'applied' => []];
if (class_exists('\AKSRT\Payment\Discount')) {
$discount_result = \AKSRT\Payment\Discount::get_instance()->calculate(
(float) $product['price'],
(int) $item['quantity'],
(int) $product['id'],
$user_id
);
$line_total = $discount_result['final_price'];
}
$subtotal += $line_total;
$product_views[] = [
'product_id' => (int) $product['id'],
'title' => $product['title'],
'price' => (float) $product['price'],
'quantity' => (int) $item['quantity'],
'line_total' => $line_total,
'image' => $product['image'],
'shipping' => $product['shipping'],
'discount_amount' => (float) ($discount_result['discount_amount'] ?? 0),
'gifts' => (array) ($discount_result['gifts'] ?? []),
'applied_discounts' => (array) ($discount_result['applied'] ?? []),
];
}
$shipping_method = sanitize_key((string) ($payload['shipping_method'] ?? 'express'));
$shipping_profile = $this->resolve_shipping_quote($product_views, $shipping_method);
$coupon_code = sanitize_text_field((string) ($payload['coupon_code'] ?? ''));
$product_ids = array_values(array_unique(array_map(static function ($row) {
return (int) ($row['product_id'] ?? $row['id'] ?? 0);
}, $product_views)));
$coupon_result = $this->resolve_coupon_discount($coupon_code, $subtotal + $shipping_profile['fee'], $product_ids);
if (!$coupon_result['success']) {
return $coupon_result;
}
$total = max(0, round($subtotal + $shipping_profile['fee'] - $coupon_result['discount'], 2));
$address = $this->resolve_checkout_address($user_id, (int) ($payload['address_id'] ?? 0));
$payment_method = $this->resolve_payment_method((string) ($payload['payment_method'] ?? 'balance'));
return [
'success' => true,
'data' => [
'items' => $product_views,
'subtotal' => round($subtotal, 2),
'shipping_fee' => $shipping_profile['fee'],
'shipping_method' => $shipping_profile['method'],
'discount' => $coupon_result['discount'],
'coupon' => $coupon_result['coupon'],
'address' => $address,
'payment_method' => $payment_method,
'total' => $total,
],
];
}
public function create_checkout_order(int $user_id, array $payload): array {
$preview = $this->preview_checkout($user_id, $payload);
if (empty($preview['success'])) {
return $preview;
}
$data = $preview['data'];
$primary_item = $data['items'][0] ?? null;
if (!$primary_item || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')];
}
$payment_method = sanitize_key((string) ($data['payment_method']['value'] ?? 'balance'));
$meta_payload = [
'customer_note' => sanitize_textarea_field((string) ($payload['customer_note'] ?? '')),
'items' => $data['items'],
'coupon' => $data['coupon'],
'shipping_fee' => $data['shipping_fee'],
'shipping_method' => $data['shipping_method'],
'address' => $data['address'],
'payment_method' => $data['payment_method'],
'discount' => $data['discount'],
];
$expiry_minutes = 30;
if (class_exists('\AKSRT\Settings\Settings')) {
$order_settings = \AKSRT\Settings\Settings::get_order_settings();
$expiry_minutes = max(5, min(1440, (int) ($order_settings['expiry'] ?? 30)));
}
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
'user_id' => $user_id,
'product_type' => 'shop_product',
'product_id' => (int) $primary_item['product_id'],
'amount' => (float) $data['total'],
'currency' => 'CNY',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => $payment_method,
'expires_at' => date('Y-m-d H:i:s', strtotime('+' . $expiry_minutes . ' minutes')),
'note' => wp_json_encode($meta_payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
if ($order_number === '') {
return ['success' => false, 'message' => __('订单创建失败。', 'aksrt')];
}
$this->shop->save_order_items($order_number, $data['items']);
if (!empty($data['coupon']['code']) && class_exists('\AKSRT\Payment\Coupon')) {
$coupon_instance = \AKSRT\Payment\Coupon::get_instance();
$coupon_instance->use_coupon($data['coupon']['code']);
// R69: Log coupon usage for per-user tracking
$coupon_row = $coupon_instance->validate($data['coupon']['code']);
$coupon_id = (int) ($coupon_row['coupon']['id'] ?? 0);
if ($coupon_id > 0) {
$coupon_instance->log_usage(
$coupon_id,
$user_id,
$order_number,
(float) ($data['discount'] ?? 0)
);
}
}
$detail = $this->shop->get_order_detail_view($user_id, $order_number);
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'order' => $detail,
],
];
}
public function pay_order(int $user_id, string $order_number, string $payment_method): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [
\AKSRT\Payment\Order::STATUS_CREATED,
\AKSRT\Payment\Order::STATUS_PENDING,
], true)) {
return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')];
}
// Payment attempts are bound to the method selected when the order was created.
$stored_method = sanitize_key((string) ($order['payment_method'] ?? ''));
$payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance'));
if ($stored_method !== '' && $payment_method !== $stored_method) {
return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')];
}
$gateway_manager = class_exists('\AKSRT\Payment\Gateway') ? \AKSRT\Payment\Gateway::get_instance() : null;
$gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null;
if (!$gateway) {
return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')];
}
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')];
}
// Points payment is a purely internal settlement (no provider attempt),
// but still needs the attempt lock against double-click double deduction.
if ($payment_method === 'points') {
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
: false;
if ($lock === false) {
return ['success' => false, 'message' => __('该订单正在发起支付,请稍后重试。', 'aksrt')];
}
try {
return $this->pay_with_points($user_id, $order_number, $order);
} finally {
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
}
}
// 携带订单号与摘要,供网关生成支付单。
// PaymentOrchestrator 内部持有 attempt 锁并分配 provider out_trade_no。
$order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number);
$order['payment_method'] = $payment_method;
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($gateway, $order, [
'paid_note' => __('用户在前台完成支付。', 'aksrt'),
]);
if (empty($payment_result['success'])) {
return ['success' => false, 'message' => $payment_result['message']];
}
// 跳转/扫码型网关(支付宝/微信/PayPal/Stripe):返回支付凭据,订单保持待支付,
// 由 /payment/{gateway}/notify|return 回调确认到账;此处绝不预先标记已支付。
if (!empty($payment_result['redirect'])) {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
'order_number' => $order_number,
'message' => $payment_result['message'],
],
];
}
return [
'success' => true,
'data' => [
'order' => $this->shop->get_order_detail_view($user_id, $order_number),
'message' => $payment_result['message'],
],
];
}
private function pay_with_points(int $user_id, string $order_number, array $order): array {
if (!class_exists('\AKSRT\Credits\Credits')) {
return ['success' => false, 'message' => __('积分系统不可用。', 'aksrt')];
}
$amount = (int) round((float) ($order['amount'] ?? 0));
if ($amount <= 0) {
return ['success' => false, 'message' => __('订单金额无效。', 'aksrt')];
}
$balance = \AKSRT\Credits\Credits::balance($user_id);
if ($balance < $amount) {
return ['success' => false, 'message' => sprintf(__('积分不足,需要 %d 积分,当前余额 %d。', 'aksrt'), $amount, $balance)];
}
$deducted = \AKSRT\Credits\Credits::deduct(
$user_id,
$amount,
'shop_points_pay',
['order' => $order_number],
sprintf(__('积分购买订单 %s', 'aksrt'), $order_number)
);
if (is_wp_error($deducted)) {
return ['success' => false, 'message' => $deducted->get_error_message() ?: __('积分扣除失败。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$paid = $repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount),
'require_transition' => true,
]);
if (!$paid) {
$compensation = \AKSRT\Credits\Credits::add(
$user_id,
$amount,
'shop_points_pay_rollback',
['order' => $order_number],
sprintf(__('订单 %s 支付失败退回积分', 'aksrt'), $order_number)
);
if (is_wp_error($compensation)) {
error_log(sprintf(
'[AKSRT Payment] points rollback failed order=%s: %s',
$order_number,
$compensation->get_error_message()
));
return ['success' => false, 'message' => __('订单状态已变化,积分补偿失败,请联系管理员处理。', 'aksrt')];
}
return ['success' => false, 'message' => __('订单状态已变化,积分已退回。', 'aksrt')];
}
return [
'success' => true,
'data' => [
'order' => $this->shop->get_order_detail_view($user_id, $order_number),
'message' => __('积分支付成功。', 'aksrt'),
],
];
}
private function normalize_checkout_items($items): array {
if (!is_array($items)) {
return [];
}
$normalized = [];
foreach ($items as $item) {
$product_id = (int) ($item['product_id'] ?? 0);
$quantity = max(1, (int) ($item['quantity'] ?? 1));
if ($product_id <= 0) {
continue;
}
$normalized[] = [
'product_id' => $product_id,
'quantity' => $quantity,
];
}
return $normalized;
}
private function resolve_coupon_discount(string $coupon_code, float $order_amount, array $product_ids = []): array {
if ($coupon_code === '') {
return ['success' => true, 'discount' => 0.0, 'coupon' => []];
}
if (!class_exists('\AKSRT\Payment\Coupon')) {
return ['success' => false, 'message' => __('优惠券系统不可用。', 'aksrt')];
}
$result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $order_amount, [
'user_id' => get_current_user_id(),
'product_ids' => array_values(array_filter(array_map('intval', $product_ids))),
'context' => 'shop',
]);
if (empty($result['valid'])) {
return ['success' => false, 'message' => $result['message'] ?? __('优惠券不可用。', 'aksrt')];
}
return [
'success' => true,
'discount' => (float) ($result['discount'] ?? 0),
'coupon' => [
'code' => (string) ($result['coupon']['code'] ?? $coupon_code),
'type' => (string) ($result['coupon']['type'] ?? ''),
'value' => (float) ($result['coupon']['value'] ?? 0),
],
];
}
private function resolve_shipping_quote(array $items, string $shipping_method): array {
$methods = $this->shop->get_shipping_method_options();
$method = $methods[$shipping_method] ?? $methods['express'];
$base_fee = (float) ($method['fee'] ?? 0);
foreach ($items as $item) {
$base_fee += (float) ($item['shipping']['fee'] ?? 0);
}
return [
'method' => $method,
'fee' => round($base_fee, 2),
];
}
private function resolve_checkout_address(int $user_id, int $address_id): array {
$addresses = $this->shop->get_address_book($user_id);
foreach ($addresses as $address) {
if ((int) ($address['id'] ?? 0) === $address_id) {
return $address;
}
}
return $addresses[0] ?? [];
}
private function resolve_payment_method(string $value): array {
$methods = $this->shop->get_payment_method_options();
return $methods[$value] ?? reset($methods);
}
public function get_checkout_hint(array $product): array {
return [
'delivery' => $product['shipping']['delivery_note'] ?: __('付款后自动进入订单中心,可继续查看物流和售后进度。', 'aksrt'),
'service' => $product['after_sale']['policy'] ?: __('支持在订单中心发起售后申请。', 'aksrt'),
];
}
}
-677
View File
@@ -1,677 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part1 {
public function create_after_sale_request(int $user_id, array $payload): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')];
}
$order_number = sanitize_text_field((string) ($payload['order_number'] ?? ''));
$reason = sanitize_textarea_field((string) ($payload['reason'] ?? ''));
$type = sanitize_key((string) ($payload['type'] ?? \AKSRT\Payment\AfterSale::TYPE_REFUND));
if ($order_number === '' || $reason === '') {
return ['success' => false, 'message' => __('请填写售后原因。', 'aksrt')];
}
$order = $this->get_order_detail_view($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$ticket_id = \AKSRT\Payment\AfterSale::get_instance()->create([
'order_number' => $order_number,
'applicant_user_id' => $user_id,
'type' => $type,
'reason' => $reason,
'amount' => (float) ($payload['amount'] ?? $order['amount'] ?? 0),
'note' => sanitize_textarea_field((string) ($payload['note'] ?? '')),
]);
if (is_wp_error($ticket_id)) {
return ['success' => false, 'message' => $ticket_id->get_error_message()];
}
if ($ticket_id <= 0) {
return ['success' => false, 'message' => __('售后申请提交失败。', 'aksrt')];
}
return [
'success' => true,
'data' => [
'ticket_id' => $ticket_id,
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
/**
* List after-sale tickets for current user.
*/
public function get_user_after_sales(int $user_id): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['items' => []];
}
$items = \AKSRT\Payment\AfterSale::get_instance()->get_user_tickets($user_id, 50);
return ['items' => $items];
}
/**
* User submits return shipping info.
*/
public function submit_after_sale_return(int $user_id, int $ticket_id, array $payload): array {
if ($user_id <= 0 || $ticket_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')];
}
$carrier = sanitize_text_field((string) ($payload['carrier'] ?? ''));
$tracking = sanitize_text_field((string) ($payload['tracking_no'] ?? ''));
if ($carrier === '' || $tracking === '') {
return ['success' => false, 'message' => __('请填写快递公司与运单号。', 'aksrt')];
}
$ok = \AKSRT\Payment\AfterSale::get_instance()->user_submit_return_shipping($ticket_id, $user_id, $carrier, $tracking);
if (!$ok) {
return ['success' => false, 'message' => __('提交退货物流失败,请确认工单状态。', 'aksrt')];
}
$ticket = \AKSRT\Payment\AfterSale::get_instance()->get($ticket_id);
return [
'success' => true,
'message' => __('退货物流已提交', 'aksrt'),
'data' => $ticket ? \AKSRT\Payment\AfterSale::get_instance()->decorate_user_ticket($ticket) : ['id' => $ticket_id],
];
}
/**
* User cancels after-sale ticket.
*/
public function cancel_after_sale(int $user_id, int $ticket_id, string $reason = ''): array {
if ($user_id <= 0 || $ticket_id <= 0 || !class_exists('\AKSRT\Payment\AfterSale')) {
return ['success' => false, 'message' => __('售后系统不可用。', 'aksrt')];
}
$ok = \AKSRT\Payment\AfterSale::get_instance()->user_cancel($ticket_id, $user_id, $reason);
if (!$ok) {
return ['success' => false, 'message' => __('无法取消该售后工单。', 'aksrt')];
}
return ['success' => true, 'message' => __('已取消售后申请', 'aksrt')];
}
public function get_product_archive_view(array $args = []): array {
$taxonomy = sanitize_key((string) ($args['taxonomy'] ?? ''));
$term_id = (int) ($args['term_id'] ?? 0);
$paged = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1));
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$list = is_array($shop_settings['list'] ?? null) ? $shop_settings['list'] : [];
$per_page = max(4, min(100, (int) ($args['per_page'] ?? ($list['count'] ?? 12))));
// Normalize orderby aliases (zibll + legacy AKSRT keys).
$orderby_raw = sanitize_key((string) ($args['orderby'] ?? ($list['orderby'] ?? 'date')));
$order = strtoupper(sanitize_key((string) ($args['order'] ?? '')));
if (!in_array($order, ['ASC', 'DESC'], true)) {
$order = '';
}
// Legacy split price keys → price + order.
if ($orderby_raw === 'price_asc') {
$orderby_raw = 'price';
$order = 'ASC';
} elseif ($orderby_raw === 'price_desc') {
$orderby_raw = 'price';
$order = 'DESC';
} elseif ($orderby_raw === 'sales_volume') {
$orderby_raw = 'sales';
} elseif ($orderby_raw === 'zibpay_price') {
$orderby_raw = 'price';
} elseif ($orderby_raw === 'comments') {
$orderby_raw = 'score';
}
$allowed_orderby = ['date', 'views', 'favorite_count', 'score', 'sales', 'price', 'modified'];
if (!in_array($orderby_raw, $allowed_orderby, true)) {
$orderby_raw = 'date';
}
if ($orderby_raw === 'price' && $order === '') {
$order = 'ASC';
}
if ($order === '' && $orderby_raw !== 'price') {
$order = 'DESC';
}
$orderby = $orderby_raw;
$paging = sanitize_key((string) ($args['paging'] ?? ($list['paging'] ?? 'number')));
if (!in_array($paging, ['number', 'ajax'], true)) {
$paging = 'number';
}
$ias_s = array_key_exists('ias_s', $args) ? !empty($args['ias_s']) : !empty($list['ias_s']);
$ias_max = max(1, min(50, (int) ($args['ias_max'] ?? ($list['ias_max'] ?? 5))));
$style = sanitize_key((string) ($list['style'] ?? 'default'));
$empty = [
'products' => [],
'term' => null,
'total' => 0,
'max_pages' => 0,
'page' => $paged,
'orderby' => $orderby,
'order' => $order,
'paging' => $paging,
'ias_s' => $ias_s,
'ias_max' => $ias_max,
'per_page' => $per_page,
'style' => $style,
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
'filter' => null,
];
if (!in_array($taxonomy, ['aksrt_product_cat', 'aksrt_product_tag'], true) && empty($args['allow_all'])) {
return $empty;
}
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $per_page,
'paged' => $paged,
'post_status' => 'publish',
'order' => $order ?: 'DESC',
'orderby' => 'date',
];
$term = null;
if ($taxonomy !== '' && $term_id > 0) {
$term = get_term($term_id, $taxonomy);
if (is_wp_error($term) || !$term) {
return $empty;
}
$query_args['tax_query'] = [[
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $term_id,
]];
}
if ($orderby === 'sales') {
$query_args['meta_key'] = '_aksrt_sales';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'price') {
$query_args['meta_key'] = '_aksrt_price';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = $order === 'ASC' ? 'ASC' : 'DESC';
} elseif ($orderby === 'favorite_count') {
$query_args['meta_key'] = '_aksrt_favorite_count';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'views') {
$query_args['meta_key'] = '_aksrt_views';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'score') {
$query_args['meta_key'] = '_aksrt_score_average';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
} elseif ($orderby === 'modified') {
$query_args['orderby'] = 'modified';
$query_args['order'] = 'DESC';
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
$filter = null;
if ($taxonomy === 'aksrt_product_cat' && $term) {
$filter = $this->build_product_cat_filter((int) $term->term_id, $orderby, $order);
}
return [
'products' => $products,
'term' => $term ? [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'count' => (int) $term->count,
'taxonomy' => $taxonomy,
'parent' => (int) $term->parent,
'url' => get_term_link($term),
] : null,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
'page' => $paged,
'orderby' => $orderby,
'order' => $order,
'paging' => $paging,
'ias_s' => $ias_s,
'ias_max' => $ias_max,
'per_page' => $per_page,
'style' => $style,
'sales' => is_array($shop_settings['sales'] ?? null) ? $shop_settings['sales'] : [],
'filter' => $filter,
];
}
/**
* Hierarchical cat filter data (zibll zib_shop_get_cat_filter).
*
* @return array{siblings:array<int,array>,children:array<int,array>,parent_id:int,mode:string}
*/
public function build_product_cat_filter(int $cat_id, string $orderby = 'date', string $order = 'DESC'): array {
$taxonomy = 'aksrt_product_cat';
$cat = get_term($cat_id, $taxonomy);
if (!$cat || is_wp_error($cat)) {
return ['siblings' => [], 'children' => [], 'parent_id' => 0, 'mode' => 'flat', 'orderby' => $orderby, 'order' => $order];
}
$map_term = static function ($t) use ($orderby, $order): array {
$url = get_term_link($t);
if (!is_wp_error($url) && $orderby) {
$url = add_query_arg(array_filter([
'orderby' => $orderby,
'order' => $orderby === 'price' ? $order : null,
]), $url);
}
return [
'id' => (int) $t->term_id,
'name' => (string) $t->name,
'url' => is_wp_error($url) ? '' : (string) $url,
'count' => (int) $t->count,
];
};
$parent_id = (int) $cat->parent;
$children = get_terms([
'taxonomy' => $taxonomy,
'parent' => $cat_id,
'hide_empty' => false,
]);
if (is_wp_error($children)) {
$children = [];
}
if ($parent_id > 0) {
$siblings = get_terms([
'taxonomy' => $taxonomy,
'parent' => $parent_id,
'hide_empty' => false,
]);
} else {
$siblings = get_terms([
'taxonomy' => $taxonomy,
'parent' => 0,
'hide_empty' => false,
]);
}
if (is_wp_error($siblings)) {
$siblings = [];
}
$mode = 'siblings';
$parent_terms = [];
if (empty($children) && $parent_id > 0) {
$parent_term = get_term($parent_id, $taxonomy);
if ($parent_term && !is_wp_error($parent_term) && (int) $parent_term->parent > 0) {
$parent_terms = get_terms([
'taxonomy' => $taxonomy,
'parent' => (int) $parent_term->parent,
'hide_empty' => false,
]);
if (is_wp_error($parent_terms)) {
$parent_terms = [];
}
$mode = 'parent_siblings';
} elseif ($parent_id > 0) {
$parent_terms = get_terms([
'taxonomy' => $taxonomy,
'parent' => 0,
'hide_empty' => false,
]);
if (is_wp_error($parent_terms)) {
$parent_terms = [];
}
$mode = 'parent_siblings';
}
} elseif (!empty($children)) {
$mode = 'with_children';
}
$parent_url = '';
if ($parent_id > 0) {
$plink = get_term_link($parent_id, $taxonomy);
if (!is_wp_error($plink)) {
$parent_url = add_query_arg(array_filter([
'orderby' => $orderby,
'order' => $orderby === 'price' ? $order : null,
]), $plink);
}
}
return [
'mode' => $mode,
'current_id' => $cat_id,
'parent_id' => $parent_id,
'parent_url' => $parent_url,
'siblings' => array_map($map_term, empty($parent_terms) ? $siblings : $parent_terms),
'children' => array_map($map_term, $mode === 'with_children' ? $children : ($mode === 'parent_siblings' ? $siblings : [])),
'child_tabs' => array_map($map_term, $children),
'orderby' => $orderby,
'order' => $order,
];
}
public function get_product_categories(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_product_cat',
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
]);
if (is_wp_error($terms)) {
return [];
}
return array_map(static function ($t) {
return [
'id' => $t->term_id,
'name' => $t->name,
'count' => $t->count,
'url' => get_term_link($t),
];
}, $terms);
}
/* ============================================================
* Merchant / Store Mode (§6.10)
* ============================================================ */
public function get_store_profile(int $author_id): array {
if ($author_id <= 0) return [];
$user = get_userdata($author_id);
if (!$user) return [];
$product_count = $this->count_author_products($author_id);
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
$author_show = !isset($shop_settings['general']['shop_author_show']) || !empty($shop_settings['general']['shop_author_show']);
if (!$author_show) return [];
$contact = $this->get_store_contact($author_id);
return [
'author_id' => $author_id,
'name' => (string) $user->display_name,
'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($author_id) : get_avatar_url($author_id),
'description' => (string) get_the_author_meta('description', $author_id),
'url' => get_author_posts_url($author_id),
'product_count' => $product_count,
'contact' => $contact,
'addresses' => $this->get_address_book($author_id),
];
}
public function get_store_products(int $author_id, int $page = 1, int $per_page = 12): array {
$query = new \WP_Query([
'post_type' => 'aksrt_product',
'author' => $author_id,
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
]);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
return [
'items' => $products,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
public function count_author_products(int $author_id): int {
global $wpdb;
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_product' AND post_status = 'publish' AND post_author = %d",
$author_id
));
}
private function get_store_contact(int $author_id): array {
$raw = get_user_meta($author_id, 'aksrt_shop_contact', true);
if (!is_array($raw)) return [];
$contacts = [];
foreach ($raw as $c) {
$name = sanitize_text_field((string) ($c['name'] ?? ''));
if ($name === '') continue;
$contacts[] = [
'name' => $name,
'icon' => sanitize_text_field((string) ($c['icon'] ?? '')),
'link' => esc_url_raw((string) ($c['link'] ?? '')),
'qr' => esc_url_raw((string) ($c['qr'] ?? '')),
];
}
return $contacts;
}
public function get_stats(): array {
$product_count = 0;
$post_type_obj = get_post_type_object('aksrt_product');
if ($post_type_obj) {
$counts = wp_count_posts('aksrt_product');
$product_count = (int) ($counts->publish ?? 0);
}
return [
'product_count' => $product_count,
'order_count' => class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['total'] : 0,
'revenue_total' => class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()['revenue'] : 0,
];
}
public function resolve_main_image(int $product_id): string {
return $this->product_service()->resolve_main_image($product_id);
}
public function build_product_card_view(int $product_id): array {
return $this->product_service()->build_product_card_view($product_id);
}
private function decorate_order_view(array $order, bool $with_detail = false): array {
$meta = $order['meta_payload'] ?? [];
$order_number = (string) ($order['order_number'] ?? '');
$order_items = $this->get_order_items($order_number);
$has_structured_items = !empty($order_items);
$product = $has_structured_items
? $this->build_product_card_view((int) ($order_items[0]['product_id'] ?? 0))
: $this->build_product_card_view((int) ($order['product_id'] ?? 0));
$shipping_records = class_exists('\AKSRT\Payment\Shipping') ? \AKSRT\Payment\Shipping::get_instance()->get_by_order($order_number) : [];
$after_sale_records = class_exists('\AKSRT\Payment\AfterSale') ? \AKSRT\Payment\AfterSale::get_instance()->get_list([
'limit' => 20,
'offset' => 0,
'order_number' => $order_number,
]) : ['items' => []];
$status = (string) ($order['status'] ?? \AKSRT\Payment\Order::STATUS_PENDING);
$subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount']);
$shipping_fee = (float) ($meta['shipping_fee'] ?? 0);
$discount = (float) ($meta['discount'] ?? 0);
$expires_at = (string) ($order['expires_at'] ?? '');
$payment_value = (string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance');
$view = [
'id' => (int) ($order['id'] ?? 0),
'order_number' => (string) ($order['order_number'] ?? ''),
'amount' => (float) ($order['amount'] ?? 0),
'status' => $status,
'status_label' => \AKSRT\Payment\Order::get_valid_statuses()[$status] ?? $status,
'product_name' => $product['title'] ?? __('商品订单', 'aksrt'),
'product' => $product,
'created_at' => (string) ($order['created_at'] ?? ''),
'paid_at' => (string) ($order['paid_at'] ?? ''),
'expires_at' => $expires_at,
'currency_symbol' => '¥',
'subtotal' => $subtotal,
'shipping_fee' => $shipping_fee,
'discount' => $discount,
'address' => $meta['address'] ?? [],
'payment_method' => $meta['payment_method'] ?? ['value' => $order['payment_method'] ?? '', 'label' => __('站内余额', 'aksrt')],
'coupon' => $meta['coupon'] ?? [],
'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []),
'shipping_method' => $meta['shipping_method'] ?? [],
'customer_note' => $meta['customer_note'] ?? '',
'pay_url' => '#',
'actions' => [
'can_pay' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true) && $payment_value !== 'manual_review',
'can_cancel' => in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true),
'can_after_sale' => in_array($status, [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true),
'can_receipt' => $status === \AKSRT\Payment\Order::STATUS_DELIVERED,
],
'time_left' => $this->get_order_countdown($expires_at, $status),
];
if ($with_detail) {
$view['shipping_records'] = $shipping_records;
$view['after_sale_records'] = $after_sale_records['items'] ?? [];
$view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_records['items'] ?? []);
}
return $view;
}
/**
* Shop-side timeline; keep behaviour aligned with UserOrders.
*
* @param array<string, mixed> $order
* @param array<int, array> $shipping_records
* @param array<int, array> $after_sale_records
* @return array<int, array{time:string,label:string,key?:string}>
*/
private function build_order_timeline(array $order, array $shipping_records, array $after_sale_records): array {
// Prefer the unified order-center builder when available.
if (class_exists('\AKSRT\Payment\UserOrders')) {
$view = \AKSRT\Payment\UserOrders::get_instance()->decorate_order_view($order, true);
if (is_array($view['timeline'] ?? null)) {
return $view['timeline'];
}
}
$timeline = [];
$created = (string) ($order['created_at'] ?? '');
if ($created !== '') {
$timeline[] = ['key' => 'created', 'time' => $created, 'label' => __('订单创建', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
$paid_like = [
\AKSRT\Payment\Order::STATUS_PAID,
\AKSRT\Payment\Order::STATUS_DELIVERED,
\AKSRT\Payment\Order::STATUS_COMPLETED,
\AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
\AKSRT\Payment\Order::STATUS_REFUNDED,
\AKSRT\Payment\Order::STATUS_REFUNDING,
];
if (!empty($order['paid_at']) || in_array($status, $paid_like, true)) {
$timeline[] = [
'key' => 'paid',
'time' => (string) ($order['paid_at'] ?? $created),
'label' => __('完成支付', 'aksrt'),
];
}
foreach ($shipping_records as $record) {
$timeline[] = [
'key' => 'shipping',
'time' => (string) ($record['created_at'] ?? ''),
'label' => sprintf(__('物流更新:%s', 'aksrt'), $record['tracking_no'] ?? ''),
];
}
foreach ($after_sale_records as $record) {
$timeline[] = [
'key' => 'after_sale',
'time' => (string) ($record['created_at'] ?? ''),
'label' => sprintf(__('售后申请:%s', 'aksrt'), $record['reason'] ?? ''),
];
}
usort($timeline, static function ($a, $b) {
return strcmp((string) ($a['time'] ?? ''), (string) ($b['time'] ?? ''));
});
return $timeline;
}
private function get_order_countdown(string $expires_at, string $status): int {
if ($expires_at === '' || !in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) {
return 0;
}
$left = strtotime($expires_at) - time();
return max(0, $left);
}
public function get_limit_buy_config(int $product_id): array {
return $this->product_service()->get_limit_buy_config($product_id);
}
public function get_product_services(int $product_id): array {
return $this->product_service()->get_product_services($product_id);
}
public function get_product_params(int $product_id): array {
return $this->product_service()->get_product_params($product_id);
}
public function get_address_book(int $user_id): array {
return $this->address_service()->get_address_book($user_id);
}
public function add_address(int $user_id, array $data): array {
return $this->address_service()->add_address($user_id, $data);
}
public function update_address(int $user_id, int $address_id, array $data): array {
return $this->address_service()->update_address($user_id, $address_id, $data);
}
public function delete_address(int $user_id, int $address_id): array {
return $this->address_service()->delete_address($user_id, $address_id);
}
public function set_default_address(int $user_id, int $address_id): array {
return $this->address_service()->set_default_address($user_id, $address_id);
}
public function get_payment_method_options(): array {
return $this->product_service()->get_payment_method_options();
}
public function get_shipping_method_options(): array {
return $this->product_service()->get_shipping_method_options();
}
/* ============================================================
* Product Variants / SKU
* ============================================================ */
public function get_variants(int $product_id): array {
return $this->product_service()->get_variants($product_id);
}
public function get_variant(int $variant_id): ?array {
return $this->product_service()->get_variant($variant_id);
}
}
-436
View File
@@ -1,436 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part2 {
public function save_variants(int $product_id, array $variants): array {
return $this->product_service()->save_variants($product_id, $variants);
}
public function delete_variant(int $variant_id): bool {
return $this->product_service()->delete_variant($variant_id);
}
public function has_variants(int $product_id): bool {
return $this->product_service()->has_variants($product_id);
}
/**
* Get the price range for a product (considering variants).
*/
public function get_price_range(int $product_id): array {
$base_price = (float) get_post_meta($product_id, '_aksrt_price', true);
$variants = $this->get_variants($product_id);
if (empty($variants)) {
return ['min' => $base_price, 'max' => $base_price, 'has_variants' => false];
}
$prices = array_column($variants, 'price');
$prices[] = $base_price;
$prices = array_filter($prices, fn($p) => $p > 0);
if (empty($prices)) {
return ['min' => 0, 'max' => 0, 'has_variants' => true];
}
return [
'min' => min($prices),
'max' => max($prices),
'has_variants' => true,
];
}
// ── R68: Fulfillment hook (§6.5) ──
/**
* aksrt_order_paid listener: deduct stock + increment sales.
* Registered via \AKSRT\Core\Setup::init().
*/
public function handle_order_paid(string $order_number, array $order): void {
if ($this->order_item_table() !== '') {
$items = $this->get_order_items($order_number);
if (!empty($items)) {
$this->handle_order_paid_enhanced($order_number, $order);
return;
}
}
$note_raw = (string) ($order['note'] ?? '');
$meta = json_decode($note_raw, true);
if (!is_array($meta)) {
$meta = [];
}
$items = $meta['items'] ?? [];
if (empty($items) && !empty($order['product_id'])) {
$items = [
[
'product_id' => (int) $order['product_id'],
'quantity' => 1,
],
];
}
foreach ($items as $item) {
$pid = (int) ($item['product_id'] ?? 0);
$qty = max(1, (int) ($item['quantity'] ?? 1));
if ($pid <= 0) {
continue;
}
// Deduct stock (skip if unlimited, i.e. stock <= -1)
$stock = (int) get_post_meta($pid, '_aksrt_stock', true);
if ($stock > 0) {
$new_stock = max(0, $stock - $qty);
update_post_meta($pid, '_aksrt_stock', $new_stock);
}
// Increment sales_volume
$sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true);
update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty);
}
/**
* Fires after shop fulfillment (stock + sales).
*
* @param string $order_number
* @param array $order
* @param array $items Normalized item list
*/
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
}
// ── R68: Confirm receipt (§6.5) ──
/**
* User confirms receipt: delivered -> completed.
*/
public function confirm_receipt(int $user_id, string $order_number): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if ($status !== \AKSRT\Payment\Order::STATUS_DELIVERED) {
return ['success' => false, 'message' => __('当前订单状态不可确认收货。', 'aksrt')];
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => __('用户确认收货。', 'aksrt'),
]);
return [
'success' => true,
'data' => [
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
/**
* Auto-confirm cron: complete orders delivered > receipt_max_day ago.
*/
public function run_auto_confirm_cron(): void {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = (int) ($shop['timing']['receipt_max_day'] ?? 15);
if ($max_day <= 0) {
$max_day = 15;
}
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days"));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin(['status' => \AKSRT\Payment\Order::STATUS_DELIVERED, 'limit' => 500]);
foreach ($orders as $order) {
$delivered_at = (string) ($order['delivered_at'] ?? '');
if ($delivered_at === '' || $delivered_at > $cutoff) {
continue;
}
$repo->update_status((string) $order['order_number'], \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => sprintf(__('系统自动确认收货(超过 %d 天)。', 'aksrt'), $max_day),
]);
}
}
// ── R68: Order CSV export (§6.5) ──
/**
* Export orders as CSV rows. Admin-only.
*/
public function export_orders_csv(array $args = []): array {
if (!class_exists('\AKSRT\Payment\OrderRepository')) {
return ['csv' => '', 'count' => 0];
}
$status = sanitize_key((string) ($args['status'] ?? ''));
$date_from = sanitize_text_field((string) ($args['date_from'] ?? ''));
$date_to = sanitize_text_field((string) ($args['date_to'] ?? ''));
$limit = max(1, min(5000, (int) ($args['limit'] ?? 1000)));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin(['status' => $status, 'limit' => $limit]);
if ($date_from !== '') {
$orders = array_filter($orders, function ($o) use ($date_from) {
return ($o['created_at'] ?? '') >= $date_from . ' 00:00:00';
});
}
if ($date_to !== '') {
$orders = array_filter($orders, function ($o) use ($date_to) {
return ($o['created_at'] ?? '') <= $date_to . ' 23:59:59';
});
}
$header = ['order_number', 'user_id', 'product_type', 'product_id', 'amount', 'currency', 'status', 'payment_method', 'created_at', 'paid_at'];
$rows = [implode(',', $header)];
foreach ($orders as $o) {
$rows[] = implode(',', [
(string) ($o['order_number'] ?? ''),
(int) ($o['user_id'] ?? 0),
(string) ($o['product_type'] ?? ''),
(int) ($o['product_id'] ?? 0),
(float) ($o['amount'] ?? 0),
(string) ($o['currency'] ?? 'CNY'),
(string) ($o['status'] ?? ''),
(string) ($o['payment_method'] ?? ''),
(string) ($o['created_at'] ?? ''),
(string) ($o['paid_at'] ?? ''),
]);
}
$bom = "\xEF\xBB\xBF";
$csv = $bom . implode("\r\n", $rows);
return ['csv' => $csv, 'count' => count($orders)];
}
// ── R68: Configurable shipping/payment (§6.11) ──
/**
* Get payment method options from settings, falling back to hardcoded defaults.
*/
public function get_payment_method_options_configurable(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return $this->get_payment_method_options();
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$enabled = (string) ($shop['product_defaults']['payment_methods'] ?? '');
if ($enabled === '') {
return $this->get_payment_method_options();
}
$all = $this->get_payment_method_options();
$keys = array_map('trim', explode(',', $enabled));
$result = [];
foreach ($keys as $k) {
$k = sanitize_key($k);
if (isset($all[$k])) {
$result[$k] = $all[$k];
}
}
return $result ?: $this->get_payment_method_options();
}
/**
* Get shipping method options from settings, falling back to hardcoded defaults.
*/
public function get_shipping_method_options_configurable(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return $this->get_shipping_method_options();
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$enabled = (string) ($shop['product_defaults']['shipping_methods'] ?? '');
$fee_override = (float) ($shop['product_defaults']['shipping_fee_fixed'] ?? 0);
if ($enabled === '' && $fee_override <= 0) {
return $this->get_shipping_method_options();
}
$all = $this->get_shipping_method_options();
if ($enabled !== '') {
$keys = array_map('trim', explode(',', $enabled));
$filtered = [];
foreach ($keys as $k) {
$k = sanitize_key($k);
if (isset($all[$k])) {
$filtered[$k] = $all[$k];
}
}
$all = $filtered ?: $all;
}
if ($fee_override > 0) {
foreach ($all as $k => &$v) {
if ($k !== 'download') {
$v['fee'] = $fee_override;
}
}
unset($v);
}
return $all;
}
/**
* Get after-sale reason options from settings.
*/
public function get_after_sale_reason_options(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return ['refund' => [], 'return' => []];
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$refund_raw = (string) ($shop['timing']['after_sale_reason_refund'] ?? '');
$return_raw = (string) ($shop['timing']['after_sale_reason_return'] ?? '');
return [
'refund' => $this->parse_reason_lines($refund_raw),
'return' => $this->parse_reason_lines($return_raw),
];
}
/**
* Parse newline-separated reason text into indexed array.
*/
private function parse_reason_lines(string $text): array {
if ($text === '') {
return [];
}
$lines = array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $text)));
$result = [];
foreach (array_values($lines) as $i => $line) {
$result[] = [
'id' => $i + 1,
'label' => $line,
];
}
return $result;
}
/* ============================================================
* Product Options (§6.2 选项组机制)
*
* Storage: post_meta '_aksrt_product_options' = [
* [ 'name' => '颜色', 'view_mode' => 'list', 'values' => [
* [ 'label' => '红色', 'price_change' => 0, 'image' => '' ],
* [ 'label' => '蓝色', 'price_change' => 10, 'image' => 'url' ],
* ]],
* ]
*
* options_key = "红色|M" (pipe-delimited selected labels per group)
* ============================================================ */
public function get_product_options(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_product_options', true);
if (!is_array($raw) || empty($raw)) {
return [];
}
return array_values(array_filter(array_map([$this, 'sanitize_option_group'], $raw)));
}
public function save_product_options(int $product_id, array $groups): void {
$sanitized = array_values(array_filter(array_map([$this, 'sanitize_option_group'], $groups)));
update_post_meta($product_id, '_aksrt_product_options', $sanitized);
update_post_meta($product_id, '_aksrt_has_options', count($sanitized) > 0 ? '1' : '0');
}
public function has_options(int $product_id): bool {
return get_post_meta($product_id, '_aksrt_has_options', true) === '1';
}
private function sanitize_option_group($group): array {
if (!is_array($group)) return [];
$name = sanitize_text_field((string) ($group['name'] ?? ''));
if ($name === '') return [];
$view_mode_raw = $group['view_mode'] ?? '';
$view_mode = in_array($view_mode_raw, ['list', 'img'], true) ? $view_mode_raw : 'list';
$values = [];
foreach ((array) ($group['values'] ?? []) as $v) {
$label = sanitize_text_field((string) ($v['label'] ?? ''));
if ($label === '') continue;
$values[] = [
'label' => $label,
'price_change' => round((float) ($v['price_change'] ?? 0), 2),
'image' => esc_url_raw((string) ($v['image'] ?? '')),
];
}
if (empty($values)) return [];
return ['name' => $name, 'view_mode' => $view_mode, 'values' => $values];
}
public function build_options_key(array $selections): string {
return implode('|', array_map('sanitize_text_field', $selections));
}
public function resolve_option_price(int $product_id, string $options_key): float {
$base = (float) get_post_meta($product_id, '_aksrt_price', true);
$start = (float) get_post_meta($product_id, '_aksrt_start_price', true);
if ($start > 0) {
$base = $start;
}
if ($options_key === '' || $options_key === '_default') {
return $base;
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
foreach ($groups as $i => $group) {
$selected_label = $selections[$i] ?? '';
foreach ($group['values'] as $val) {
if ($val['label'] === $selected_label) {
$base += $val['price_change'];
break;
}
}
}
return max(0, round($base, 2));
}
public function resolve_option_labels(int $product_id, string $options_key): string {
if ($options_key === '' || $options_key === '_default') {
return '';
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
$parts = [];
foreach ($groups as $i => $group) {
$sel = $selections[$i] ?? '';
if ($sel !== '') {
$parts[] = $group['name'] . ':' . $sel;
}
}
return implode(', ', $parts);
}
public function resolve_option_stock(int $product_id, string $options_key): int {
$stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') {
$stock_matrix = get_post_meta($product_id, '_aksrt_stock_opts', true);
if (is_array($stock_matrix) && isset($stock_matrix[$options_key])) {
return (int) $stock_matrix[$options_key];
}
return 0;
}
return (int) get_post_meta($product_id, '_aksrt_stock', true);
}
}
-399
View File
@@ -1,399 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
trait Part3 {
public function deduct_option_stock(int $product_id, string $options_key, int $qty): void {
$stock_type = (string) get_post_meta($product_id, '_aksrt_stock_type', true);
if ($stock_type === 'opts' && $options_key !== '' && $options_key !== '_default') {
$matrix = get_post_meta($product_id, '_aksrt_stock_opts', true);
if (is_array($matrix) && isset($matrix[$options_key])) {
$matrix[$options_key] = max(0, (int) $matrix[$options_key] - $qty);
update_post_meta($product_id, '_aksrt_stock_opts', $matrix);
}
return;
}
$stock = (int) get_post_meta($product_id, '_aksrt_stock', true);
if ($stock > 0) {
update_post_meta($product_id, '_aksrt_stock', max(0, $stock - $qty));
}
}
public function get_option_image(int $product_id, string $options_key): string {
if ($options_key === '' || $options_key === '_default') {
return '';
}
$groups = $this->get_product_options($product_id);
$selections = explode('|', $options_key);
foreach ($groups as $i => $group) {
$sel = $selections[$i] ?? '';
foreach ($group['values'] as $val) {
if ($val['label'] === $sel && !empty($val['image'])) {
return $val['image'];
}
}
}
return '';
}
/* ============================================================
* Auto-delivery (§6.2 自动发货)
*
* Storage: post_meta '_aksrt_auto_delivery' = [
* 'type' => 'fixed'|'invit_code'|'card_pass'|'opts',
* 'content' => '固定内容文本',
* 'opts' => [ options_key => '该选项的发货内容' ],
* ]
* ============================================================ */
public function get_auto_delivery_config(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_auto_delivery', true);
if (!is_array($raw)) {
return ['type' => '', 'content' => '', 'opts' => []];
}
return [
'type' => sanitize_key((string) ($raw['type'] ?? '')),
'content' => (string) ($raw['content'] ?? ''),
'opts' => is_array($raw['opts'] ?? null) ? $raw['opts'] : [],
];
}
public function execute_auto_delivery(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id = 0): string {
$config = $this->get_auto_delivery_config($product_id);
$type = $config['type'];
if ($type === 'fixed') {
return (string) $config['content'];
}
if ($type === 'opts') {
$key = ($options_key !== '' && $options_key !== '_default') ? $options_key : '_default';
return (string) ($config['opts'][$key] ?? $config['content']);
}
if ($type === 'card_pass') {
return $this->consume_card_keys($product_id, $options_key, $qty, $user_id, $order_number, $order_item_id);
}
if ($type === 'invit_code') {
return $this->create_invite_codes($qty, $user_id, $order_number);
}
return '';
}
private function auto_delivery_succeeded(int $product_id, string $content, int $qty): bool {
$config = $this->get_auto_delivery_config($product_id);
if (in_array($config['type'], ['card_pass', 'invit_code'], true)) {
$lines = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $content) ?: [])));
return count($lines) === max(1, $qty)
&& !in_array($content, [__('卡密已售罄', 'aksrt'), __('邀请系统不可用', 'aksrt')], true);
}
return trim($content) !== '';
}
private function consume_card_keys(int $product_id, string $options_key, int $qty, int $user_id, string $order_number, int $order_item_id): string {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('card_key');
if ($table === '' || $order_item_id <= 0 || $wpdb->query('START TRANSACTION') === false) return '';
$where_opt = '';
$params = [$product_id];
if ($options_key !== '' && $options_key !== '_default') {
$where_opt = ' AND options_key = %s';
$params[] = $options_key;
} else {
$where_opt = " AND (options_key = '' OR options_key = '_default')";
}
$params[] = $qty;
$keys = $wpdb->get_results($wpdb->prepare(
"SELECT id, card_code FROM {$table} WHERE product_id = %d AND status = 'available'{$where_opt} ORDER BY id ASC LIMIT %d FOR UPDATE",
...$params
), ARRAY_A);
if (count((array) $keys) !== $qty) {
$wpdb->query('ROLLBACK');
return __('卡密已售罄', 'aksrt');
}
$codes = [];
$ids = [];
foreach ($keys as $key) {
$codes[] = (string) $key['card_code'];
$ids[] = (int) $key['id'];
}
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$updated = $wpdb->query($wpdb->prepare(
"UPDATE {$table} SET status = 'used', order_number = %s, used_by = %d, used_at = %s WHERE status = 'available' AND id IN ({$placeholders})",
array_merge([$order_number, $user_id, current_time('mysql')], $ids)
));
$content = implode("\n", $codes);
$item_table = $this->order_item_table();
$persisted = $item_table !== '' ? $wpdb->update(
$item_table,
['auto_delivery_content' => $content],
['id' => $order_item_id, 'order_number' => $order_number],
['%s'],
['%d', '%s']
) : false;
if ($updated !== $qty || $persisted === false || $wpdb->query('COMMIT') === false) {
$wpdb->query('ROLLBACK');
return '';
}
return $content;
}
private function create_invite_codes(int $qty, int $user_id, string $order_number): string {
if (!class_exists('\AKSRT\Credits\Invite')) {
return __('邀请系统不可用', 'aksrt');
}
$invite = \AKSRT\Credits\Invite::get_instance();
$codes = [];
for ($i = 0; $i < $qty; $i++) {
$code = $invite->generate_code($user_id);
if ($code) {
$codes[] = $code;
}
}
return implode("\n", $codes);
}
/* ============================================================
* Order Items (§6.5 多商品订单子表)
* ============================================================ */
private function order_item_table(): string {
return \AKSRT\Core\DBMigrator::table('order_item');
}
public function save_order_items(string $order_number, array $items): void {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return;
foreach ($items as $item) {
$wpdb->insert($table, [
'order_number' => $order_number,
'product_id' => (int) ($item['product_id'] ?? 0),
'product_title' => sanitize_text_field((string) ($item['title'] ?? '')),
'options_key' => sanitize_text_field((string) ($item['options_key'] ?? '')),
'options_label' => sanitize_text_field((string) ($item['options_label'] ?? '')),
'quantity' => max(1, (int) ($item['quantity'] ?? 1)),
'unit_price' => (float) ($item['unit_price'] ?? $item['price'] ?? 0),
'total_price' => (float) ($item['total_price'] ?? $item['line_total'] ?? 0),
'discount_amount' => (float) ($item['discount_amount'] ?? 0),
'shipping_fee' => (float) ($item['shipping_fee'] ?? 0),
'pay_price' => (float) ($item['pay_price'] ?? $item['line_total'] ?? 0),
'shipping_type' => sanitize_key((string) ($item['shipping_type'] ?? 'express')),
'auto_delivery_content'=> ($item['auto_delivery_content'] ?? null),
'discount_hit' => isset($item['applied_discounts']) ? wp_json_encode($item['applied_discounts']) : null,
'gift_data' => isset($item['gifts']) ? wp_json_encode($item['gifts']) : null,
'image' => esc_url_raw((string) ($item['image'] ?? '')),
]);
}
}
public function get_order_items(string $order_number): array {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return [];
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table} WHERE order_number = %s ORDER BY id ASC",
$order_number
), ARRAY_A);
return array_map(static function ($row) {
return [
'id' => (int) $row['id'],
'product_id' => (int) $row['product_id'],
'product_title' => (string) $row['product_title'],
'options_key' => (string) $row['options_key'],
'options_label' => (string) $row['options_label'],
'quantity' => (int) $row['quantity'],
'unit_price' => (float) $row['unit_price'],
'total_price' => (float) $row['total_price'],
'discount_amount' => (float) $row['discount_amount'],
'shipping_fee' => (float) $row['shipping_fee'],
'pay_price' => (float) $row['pay_price'],
'shipping_type' => (string) $row['shipping_type'],
'auto_delivery_content' => $row['auto_delivery_content'],
'discount_hit' => json_decode($row['discount_hit'] ?? '', true) ?: [],
'gift_data' => json_decode($row['gift_data'] ?? '', true) ?: [],
'image' => (string) $row['image'],
];
}, (array) $rows);
}
/* ============================================================
* Enhanced Fulfillment (§6.5 履约链路)
* ============================================================ */
public function handle_order_paid_enhanced(string $order_number, array $order): void {
$items = $this->get_order_items($order_number);
if (empty($items)) {
$this->handle_order_paid($order_number, $order);
return;
}
$user_id = (int) ($order['user_id'] ?? 0);
$auto_delivery_parts = [];
$auto_delivery_successes = 0;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$qty = max(1, (int) $item['quantity']);
$opt_key = (string) $item['options_key'];
if ($pid <= 0) continue;
$this->deduct_option_stock($pid, $opt_key, $qty);
$sales = (int) get_post_meta($pid, '_aksrt_sales_volume', true);
update_post_meta($pid, '_aksrt_sales_volume', $sales + $qty);
$shipping_type = (string) get_post_meta($pid, '_aksrt_shipping_type', true);
if ($shipping_type === 'auto') {
$content = $this->execute_auto_delivery($pid, $opt_key, $qty, $user_id, $order_number, (int) $item['id']);
if ($content !== '') {
$auto_delivery_parts[] = $content;
$config = $this->get_auto_delivery_config($pid);
$persisted = $config['type'] === 'card_pass'
? true
: $this->update_order_item_delivery($item['id'], $content);
if ($persisted && $this->auto_delivery_succeeded($pid, $content, $qty)) {
$auto_delivery_successes++;
}
}
}
}
$this->process_rebate($order_number, $order, $items);
$this->process_gifts($order_number, $order, $items);
if (!empty($auto_delivery_parts)) {
$this->send_auto_delivery_notification($user_id, $order_number, $auto_delivery_parts);
}
if ($auto_delivery_successes === count($items) && class_exists('\AKSRT\Payment\OrderRepository')) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('自动发货订单已完成。', 'aksrt')]
);
}
do_action('aksrt_shop_fulfillment_done', $order_number, $order, $items);
}
private function update_order_item_delivery(int $item_id, string $content): bool {
global $wpdb;
$table = $this->order_item_table();
if ($table === '') return false;
$updated = $wpdb->update($table, ['auto_delivery_content' => $content], ['id' => $item_id], ['%s'], ['%d']);
if ($updated === false) return false;
if ($updated === 0) {
$stored = $wpdb->get_var($wpdb->prepare("SELECT auto_delivery_content FROM {$table} WHERE id = %d", $item_id));
return is_string($stored) && hash_equals($stored, $content);
}
return true;
}
private function send_auto_delivery_notification(int $user_id, string $order_number, array $parts): void {
if (!class_exists('AKSRT_Notification_Service')) return;
$content = sprintf(
__("订单 %s 已自动发货:\n\n%s", 'aksrt'),
$order_number,
implode("\n---\n", $parts)
);
AKSRT_Notification_Service::send($user_id, 'auto_delivery', $content, [
'order_number' => $order_number,
]);
if (function_exists('aksrt_send_event_email')) {
$user = get_userdata($user_id);
if ($user && !empty($user->user_email)) {
aksrt_send_event_email($user->user_email, sprintf(__('订单 %s 自动发货通知', 'aksrt'), $order_number), $content);
}
}
}
private function process_rebate(string $order_number, array $order, array $items): void {
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) return;
$referrer_id = 0;
if (class_exists('\AKSRT\Credits\Invite')) {
$referrer_id = \AKSRT\Credits\Invite::get_instance()->get_referrer($user_id);
}
if ($referrer_id <= 0) return;
$rebate_table = \AKSRT\Core\DBMigrator::table('rebate_log');
if ($rebate_table === '') return;
global $wpdb;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$pay_price = (float) $item['pay_price'];
if ($pid <= 0 || $pay_price <= 0) continue;
$rebate_rate = (float) get_post_meta($pid, '_aksrt_rebate_rate', true);
if ($rebate_rate <= 0 && class_exists('AKSRT\Shop\TermMeta')) {
$resolved = TermMeta::resolve_inheritance($pid, 'rebate');
$rebate_rate = (float) ($resolved ?: 0);
}
if ($rebate_rate <= 0) continue;
$amount = round($pay_price * $rebate_rate / 100, 2);
if ($amount <= 0) continue;
$wpdb->insert($rebate_table, [
'user_id' => $referrer_id,
'order_number' => $order_number,
'source_user_id' => $user_id,
'amount' => $amount,
'type' => 'order',
'status' => 'pending',
'created_at' => current_time('mysql'),
]);
}
}
private function process_gifts(string $order_number, array $order, array $items): void {
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) return;
$claim_key = '_aksrt_shop_gifts_' . sanitize_key($order_number);
if (!add_user_meta($user_id, $claim_key, 'processing', true)) return;
foreach ($items as $item) {
$gifts = $item['gift_data'] ?? [];
if (empty($gifts)) continue;
foreach ($gifts as $gift) {
$gift_type = (string) ($gift['type'] ?? '');
$gift_value = $gift['value'] ?? 0;
if ($gift_type === 'credits' && class_exists('\AKSRT\Credits\Credits') && (int) $gift_value > 0) {
\AKSRT\Credits\Credits::add((int) $gift_value, $user_id, 'shop_gift', sprintf(__('订单 %s 赠品', 'aksrt'), $order_number));
} elseif (in_array($gift_type, ['level_integral', 'experience'], true) && class_exists('\AKSRT\User\Level') && (int) $gift_value > 0) {
\AKSRT\User\Level::add($user_id, 'shop_gift', (int) $gift_value, true);
} elseif ($gift_type === 'vip_days' && (int) $gift_value > 0) {
$current = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$base = max($current, time());
update_user_meta($user_id, 'aksrt_vip_expires', $base + ((int) $gift_value * DAY_IN_SECONDS));
// 赠送 VIP 天数时确保至少 VIP1;不降级已有 VIP2。
$level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
if ($level < 1) {
update_user_meta($user_id, 'aksrt_vip_level', 1);
}
}
}
}
update_user_meta($user_id, $claim_key, '1');
}
}
-341
View File
@@ -1,341 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductMetaBox {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_product', [$this, 'save'], 10, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_commerce',
__('商品商业设置', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'high'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_product_meta_save', 'aksrt_product_meta_nonce');
$price = (float) get_post_meta($post->ID, '_aksrt_price', true);
$stock = (int) get_post_meta($post->ID, '_aksrt_stock', true);
$image = (string) get_post_meta($post->ID, '_aksrt_product_image', true);
$gallery = get_post_meta($post->ID, '_aksrt_product_gallery', true);
$gallery = is_array($gallery) ? $gallery : [];
$shipping_mode = (string) get_post_meta($post->ID, '_aksrt_shipping_mode', true);
$shipping_fee = (float) get_post_meta($post->ID, '_aksrt_shipping_fee', true);
$shipping_fee_type = (string) (get_post_meta($post->ID, '_aksrt_shipping_fee_type', true) ?: 'fixed');
$shipping_fee_free = (float) get_post_meta($post->ID, '_aksrt_shipping_fee_free_amount', true);
$shipping_delivery = (string) get_post_meta($post->ID, '_aksrt_shipping_delivery_desc', true);
$shipping_tip = (string) get_post_meta($post->ID, '_aksrt_shipping_tip', true);
$after_sale_policy = (string) get_post_meta($post->ID, '_aksrt_after_sale_policy', true);
$support_refund = get_post_meta($post->ID, '_aksrt_support_refund', true);
$support_refund = $support_refund === '' ? '1' : (string) $support_refund;
$service = (string) get_post_meta($post->ID, '_aksrt_service', true);
$service_tip = (string) get_post_meta($post->ID, '_aksrt_service_tip', true);
$limit_buy = get_post_meta($post->ID, '_aksrt_limit_buy', true);
$limit_buy = $limit_buy === '' ? -1 : (int) $limit_buy;
$limit_buy_note = (string) get_post_meta($post->ID, '_aksrt_limit_buy_note', true);
$stock_type = (string) (get_post_meta($post->ID, '_aksrt_stock_type', true) ?: 'all');
$pay_modo = (string) (get_post_meta($post->ID, '_aksrt_pay_modo', true) ?: '0');
$start_price = (float) get_post_meta($post->ID, '_aksrt_start_price', true);
$product_rebate = (string) get_post_meta($post->ID, '_aksrt_product_rebate', true);
$product_params = (string) get_post_meta($post->ID, '_aksrt_product_params', true);
$user_required = (string) get_post_meta($post->ID, '_aksrt_user_required', true);
$delivery_note = (string) get_post_meta($post->ID, '_aksrt_delivery_note', true);
$marketing = (string) get_post_meta($post->ID, '_aksrt_marketing_headline', true);
$shipping_modes = [
'' => __('默认(继承设置)', 'aksrt'),
'express' => __('快递物流', 'aksrt'),
'auto' => __('自动发货', 'aksrt'),
'manual' => __('手动发货', 'aksrt'),
'virtual' => __('虚拟商品', 'aksrt'),
];
$stock_types = [
'all' => __('统一库存', 'aksrt'),
'opts' => __('按选项/变体', 'aksrt'),
];
$fee_types = [
'free' => __('包邮', 'aksrt'),
'fixed' => __('固定运费', 'aksrt'),
'amount_free' => __('满额包邮', 'aksrt'),
];
?>
<style>
.aksrt-pmb-product .row { display:grid; grid-template-columns: 140px 1fr; gap: 10px 14px; margin: 0 0 12px; align-items:start; }
.aksrt-pmb-product .row label { font-weight:600; padding-top:6px; }
.aksrt-pmb-product input[type="text"],
.aksrt-pmb-product input[type="number"],
.aksrt-pmb-product input[type="url"],
.aksrt-pmb-product select,
.aksrt-pmb-product textarea { width:100%; max-width:560px; }
.aksrt-pmb-product .hint { color:#646970; font-size:12px; margin:4px 0 0; }
.aksrt-pmb-product .grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; max-width:720px; }
.aksrt-pmb-product h4 { margin:18px 0 10px; padding-top:12px; border-top:1px solid #dcdcde; }
</style>
<div class="aksrt-pmb-product">
<h4><?php esc_html_e('价格与库存', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_price"><?php esc_html_e('价格(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_price" name="aksrt_price" min="0" step="0.01" value="<?php echo esc_attr((string) $price); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock"><?php esc_html_e('库存', 'aksrt'); ?></label>
<input type="number" id="aksrt_stock" name="aksrt_stock" min="-1" step="1" value="<?php echo esc_attr((string) $stock); ?>">
<p class="hint"><?php esc_html_e('-1 表示无限库存。', 'aksrt'); ?></p>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_pay_modo"><?php esc_html_e('支付方式', 'aksrt'); ?></label>
<select id="aksrt_pay_modo" name="aksrt_pay_modo">
<option value="0" <?php selected($pay_modo, '0'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected($pay_modo, 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_start_price"><?php esc_html_e('划线价 / 起售价', 'aksrt'); ?></label>
<input type="number" id="aksrt_start_price" name="aksrt_start_price" min="0" step="0.01" value="<?php echo esc_attr((string) $start_price); ?>">
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_stock_type"><?php esc_html_e('库存类型', 'aksrt'); ?></label>
<select id="aksrt_stock_type" name="aksrt_stock_type">
<?php foreach ($stock_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($stock_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_limit_buy"><?php esc_html_e('限购数量', 'aksrt'); ?></label>
<input type="number" id="aksrt_limit_buy" name="aksrt_limit_buy" min="-1" step="1" value="<?php echo esc_attr((string) $limit_buy); ?>">
<p class="hint"><?php esc_html_e('-1 不限购。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_limit_buy_note"><?php esc_html_e('限购说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_limit_buy_note" name="aksrt_limit_buy_note" value="<?php echo esc_attr($limit_buy_note); ?>">
</div>
<div class="row">
<label for="aksrt_product_rebate"><?php esc_html_e('推广返佣', 'aksrt'); ?></label>
<div>
<input type="text" id="aksrt_product_rebate" name="aksrt_product_rebate" value="<?php echo esc_attr($product_rebate); ?>" placeholder="<?php esc_attr_e('如 10 或 10%', 'aksrt'); ?>">
<p class="hint"><?php esc_html_e('可填固定金额或百分比,具体解析由返佣服务处理。', 'aksrt'); ?></p>
</div>
</div>
<h4><?php esc_html_e('素材', 'aksrt'); ?></h4>
<div class="row">
<label for="aksrt_product_image"><?php esc_html_e('主图 URL', 'aksrt'); ?></label>
<div>
<input type="url" id="aksrt_product_image" name="aksrt_product_image" value="<?php echo esc_attr($image); ?>" placeholder="https://">
<p class="hint"><?php esc_html_e('也可使用特色图片;此处优先作为商品主图。', 'aksrt'); ?></p>
</div>
</div>
<div class="row">
<label for="aksrt_product_gallery"><?php esc_html_e('图集 URL', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_gallery" name="aksrt_product_gallery" rows="4" placeholder="<?php esc_attr_e('每行一个图片 URL', 'aksrt'); ?>"><?php echo esc_textarea(implode("\n", $gallery)); ?></textarea>
</div>
</div>
<div class="row">
<label for="aksrt_product_params"><?php esc_html_e('商品参数', 'aksrt'); ?></label>
<div>
<textarea id="aksrt_product_params" name="aksrt_product_params" rows="4" placeholder="<?php esc_attr_e('每行一条参数,如 品牌:AKSRT', 'aksrt'); ?>"><?php echo esc_textarea($product_params); ?></textarea>
</div>
</div>
<h4><?php esc_html_e('发货与售后', 'aksrt'); ?></h4>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_mode"><?php esc_html_e('发货方式', 'aksrt'); ?></label>
<select id="aksrt_shipping_mode" name="aksrt_shipping_mode">
<?php foreach ($shipping_modes as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_mode, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_type"><?php esc_html_e('运费类型', 'aksrt'); ?></label>
<select id="aksrt_shipping_fee_type" name="aksrt_shipping_fee_type">
<?php foreach ($fee_types as $key => $label) : ?>
<option value="<?php echo esc_attr($key); ?>" <?php selected($shipping_fee_type, $key); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="grid-2">
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee"><?php esc_html_e('运费金额(元)', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee" name="aksrt_shipping_fee" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee); ?>">
</div>
<div class="row" style="grid-template-columns:1fr;">
<label for="aksrt_shipping_fee_free_amount"><?php esc_html_e('满额包邮门槛', 'aksrt'); ?></label>
<input type="number" id="aksrt_shipping_fee_free_amount" name="aksrt_shipping_fee_free_amount" min="0" step="0.01" value="<?php echo esc_attr((string) $shipping_fee_free); ?>">
</div>
</div>
<div class="row">
<label for="aksrt_shipping_delivery_desc"><?php esc_html_e('发货时效说明', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_delivery_desc" name="aksrt_shipping_delivery_desc" value="<?php echo esc_attr($shipping_delivery); ?>" placeholder="<?php esc_attr_e('如 付款后 24 小时内发货', 'aksrt'); ?>">
</div>
<div class="row">
<label for="aksrt_shipping_tip"><?php esc_html_e('物流提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_shipping_tip" name="aksrt_shipping_tip" value="<?php echo esc_attr($shipping_tip); ?>">
</div>
<div class="row">
<label for="aksrt_after_sale_policy"><?php esc_html_e('售后说明', 'aksrt'); ?></label>
<textarea id="aksrt_after_sale_policy" name="aksrt_after_sale_policy" rows="3"><?php echo esc_textarea($after_sale_policy); ?></textarea>
</div>
<div class="row">
<label for="aksrt_support_refund"><?php esc_html_e('支持退款', 'aksrt'); ?></label>
<div>
<label>
<input type="checkbox" id="aksrt_support_refund" name="aksrt_support_refund" value="1" <?php checked($support_refund, '1'); ?>>
<?php esc_html_e('允许申请退款/售后', 'aksrt'); ?>
</label>
</div>
</div>
<div class="row">
<label for="aksrt_service"><?php esc_html_e('服务承诺', 'aksrt'); ?></label>
<textarea id="aksrt_service" name="aksrt_service" rows="3" placeholder="<?php esc_attr_e('每行一条服务承诺', 'aksrt'); ?>"><?php echo esc_textarea($service); ?></textarea>
</div>
<div class="row">
<label for="aksrt_service_tip"><?php esc_html_e('服务提示', 'aksrt'); ?></label>
<input type="text" id="aksrt_service_tip" name="aksrt_service_tip" value="<?php echo esc_attr($service_tip); ?>">
</div>
<div class="row">
<label for="aksrt_user_required"><?php esc_html_e('下单需填信息', 'aksrt'); ?></label>
<textarea id="aksrt_user_required" name="aksrt_user_required" rows="3" placeholder="<?php esc_attr_e('每行一项,如 QQ / 游戏角色名', 'aksrt'); ?>"><?php echo esc_textarea($user_required); ?></textarea>
</div>
<div class="row">
<label for="aksrt_delivery_note"><?php esc_html_e('发货备注', 'aksrt'); ?></label>
<input type="text" id="aksrt_delivery_note" name="aksrt_delivery_note" value="<?php echo esc_attr($delivery_note); ?>">
</div>
<div class="row">
<label for="aksrt_marketing_headline"><?php esc_html_e('营销标题', 'aksrt'); ?></label>
<input type="text" id="aksrt_marketing_headline" name="aksrt_marketing_headline" value="<?php echo esc_attr($marketing); ?>">
</div>
<p class="hint">
<?php esc_html_e('SKU 变体请使用下方「商品变体」面板;列表列已显示价格/库存/销量。', 'aksrt'); ?>
</p>
</div>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_meta_nonce'])), 'aksrt_product_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product') {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
$price = isset($_POST['aksrt_price']) ? max(0, (float) wp_unslash($_POST['aksrt_price'])) : 0;
$stock = isset($_POST['aksrt_stock']) ? (int) wp_unslash($_POST['aksrt_stock']) : 0;
if ($stock < -1) {
$stock = -1;
}
$image = isset($_POST['aksrt_product_image']) ? esc_url_raw((string) wp_unslash($_POST['aksrt_product_image'])) : '';
$gallery_raw = isset($_POST['aksrt_product_gallery']) ? (string) wp_unslash($_POST['aksrt_product_gallery']) : '';
$gallery = array_values(array_filter(array_map(
static function ($line) {
return esc_url_raw(trim((string) $line));
},
preg_split('/\r\n|\r|\n/', $gallery_raw) ?: []
)));
$shipping_mode = isset($_POST['aksrt_shipping_mode']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_mode'])) : '';
$shipping_fee = isset($_POST['aksrt_shipping_fee']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee'])) : 0;
$shipping_fee_type = isset($_POST['aksrt_shipping_fee_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_shipping_fee_type'])) : 'fixed';
if (!in_array($shipping_fee_type, ['free', 'fixed', 'amount_free'], true)) {
$shipping_fee_type = 'fixed';
}
$shipping_fee_free = isset($_POST['aksrt_shipping_fee_free_amount']) ? max(0, (float) wp_unslash($_POST['aksrt_shipping_fee_free_amount'])) : 0;
$shipping_delivery = isset($_POST['aksrt_shipping_delivery_desc']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_delivery_desc'])) : '';
$shipping_tip = isset($_POST['aksrt_shipping_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_shipping_tip'])) : '';
$policy = isset($_POST['aksrt_after_sale_policy']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_after_sale_policy'])) : '';
$support_refund = !empty($_POST['aksrt_support_refund']) ? '1' : '0';
$service = isset($_POST['aksrt_service']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_service'])) : '';
$service_tip = isset($_POST['aksrt_service_tip']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_service_tip'])) : '';
$limit_buy = isset($_POST['aksrt_limit_buy']) ? max(-1, min(9999, (int) wp_unslash($_POST['aksrt_limit_buy']))) : -1;
$limit_buy_note = isset($_POST['aksrt_limit_buy_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_limit_buy_note'])) : '';
$stock_type = isset($_POST['aksrt_stock_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_stock_type'])) : 'all';
if (!in_array($stock_type, ['all', 'opts'], true)) {
$stock_type = 'all';
}
$pay_modo = isset($_POST['aksrt_pay_modo']) ? sanitize_key((string) wp_unslash($_POST['aksrt_pay_modo'])) : '0';
if (!in_array($pay_modo, ['0', 'points'], true)) {
$pay_modo = '0';
}
$start_price = isset($_POST['aksrt_start_price']) ? max(0, (float) wp_unslash($_POST['aksrt_start_price'])) : 0;
$product_rebate = isset($_POST['aksrt_product_rebate']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_product_rebate'])) : '';
$product_params = isset($_POST['aksrt_product_params']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_product_params'])) : '';
$user_required = isset($_POST['aksrt_user_required']) ? sanitize_textarea_field((string) wp_unslash($_POST['aksrt_user_required'])) : '';
$delivery_note = isset($_POST['aksrt_delivery_note']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_delivery_note'])) : '';
$marketing = isset($_POST['aksrt_marketing_headline']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_marketing_headline'])) : '';
update_post_meta($post_id, '_aksrt_price', $price);
update_post_meta($post_id, '_aksrt_stock', $stock);
update_post_meta($post_id, '_aksrt_product_image', $image);
update_post_meta($post_id, '_aksrt_product_gallery', $gallery);
update_post_meta($post_id, '_aksrt_shipping_mode', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_type', $shipping_mode);
update_post_meta($post_id, '_aksrt_shipping_fee', $shipping_fee);
update_post_meta($post_id, '_aksrt_shipping_fee_type', $shipping_fee_type);
update_post_meta($post_id, '_aksrt_shipping_fee_free_amount', $shipping_fee_free);
update_post_meta($post_id, '_aksrt_shipping_delivery_desc', $shipping_delivery);
update_post_meta($post_id, '_aksrt_shipping_tip', $shipping_tip);
update_post_meta($post_id, '_aksrt_after_sale_policy', $policy);
update_post_meta($post_id, '_aksrt_support_refund', $support_refund);
update_post_meta($post_id, '_aksrt_service', $service);
update_post_meta($post_id, '_aksrt_service_tip', $service_tip);
update_post_meta($post_id, '_aksrt_limit_buy', $limit_buy);
update_post_meta($post_id, '_aksrt_limit_buy_note', $limit_buy_note);
update_post_meta($post_id, '_aksrt_stock_type', $stock_type);
update_post_meta($post_id, '_aksrt_pay_modo', $pay_modo);
update_post_meta($post_id, '_aksrt_start_price', $start_price);
update_post_meta($post_id, '_aksrt_product_rebate', $product_rebate);
update_post_meta($post_id, '_aksrt_product_params', $product_params);
update_post_meta($post_id, '_aksrt_user_required', $user_required);
update_post_meta($post_id, '_aksrt_delivery_note', $delivery_note);
update_post_meta($post_id, '_aksrt_marketing_headline', $marketing);
// Keep sales counter present for list columns.
if (get_post_meta($post_id, '_aksrt_sales', true) === '') {
update_post_meta($post_id, '_aksrt_sales', 0);
}
}
}
-512
View File
@@ -1,512 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductReview {
private static $instance = null;
const COMMENT_TYPE = 'aksrt_product_review';
const RATING_LEVELS = [
5 => '超赞',
4 => '很棒',
3 => '不错',
2 => '一般',
1 => '较差',
0 => '很差',
];
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('aksrt_daily_cron', [$this, 'run_auto_positive_cron']);
}
public function submit_review(int $user_id, string $order_number, int $product_id, array $data): array {
if ($user_id <= 0 || $order_number === '' || $product_id <= 0) {
return ['success' => false, 'message' => __('参数错误', 'aksrt')];
}
if (!$this->can_review($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')];
}
if ($this->has_reviewed($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('已评价过该商品', 'aksrt')];
}
$rating_product = max(0, min(5, (int) ($data['rating_product'] ?? 5)));
$rating_service = max(0, min(5, (int) ($data['rating_service'] ?? 5)));
$rating_shipping = max(0, min(5, (int) ($data['rating_shipping'] ?? 5)));
$average = round(($rating_product + $rating_service + $rating_shipping) / 3, 1);
$content = sanitize_textarea_field((string) ($data['content'] ?? ''));
$images = [];
$max_images = $this->get_max_images();
foreach ((array) ($data['images'] ?? []) as $img) {
if (count($images) >= $max_images) {
break;
}
$stored = $this->normalize_review_image((string) $img, $user_id);
if ($stored !== '') {
$images[] = $stored;
}
}
$comment_id = wp_insert_comment([
'comment_post_ID' => $product_id,
'user_id' => $user_id,
'comment_content' => $content ?: __('好评!', 'aksrt'),
'comment_type' => self::COMMENT_TYPE,
'comment_approved' => 1,
]);
if (!$comment_id) {
return ['success' => false, 'message' => __('评价提交失败', 'aksrt')];
}
update_comment_meta($comment_id, '_aksrt_order_number', $order_number);
update_comment_meta($comment_id, '_aksrt_rating_product', $rating_product);
update_comment_meta($comment_id, '_aksrt_rating_service', $rating_service);
update_comment_meta($comment_id, '_aksrt_rating_shipping', $rating_shipping);
update_comment_meta($comment_id, '_aksrt_rating_average', $average);
if (!empty($images)) {
update_comment_meta($comment_id, '_aksrt_review_images', $images);
}
$scores = $this->refresh_product_scores($product_id);
update_post_meta($product_id, '_aksrt_score_average', (float) ($scores['average'] ?? $average));
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
$status = (string) ($order['status'] ?? '');
if ($status === \AKSRT\Payment\Order::STATUS_PENDING_REVIEW) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('用户已评价', 'aksrt')]
);
}
}
return ['success' => true, 'comment_id' => $comment_id];
}
public function can_review(int $user_id, string $order_number, int $product_id): bool {
if (!class_exists('\AKSRT\Payment\OrderRepository')) return false;
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) return false;
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [
\AKSRT\Payment\Order::STATUS_COMPLETED,
\AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
\AKSRT\Payment\Order::STATUS_DELIVERED,
], true)) {
return false;
}
if (class_exists('\AKSRT\Payment\AfterSale')) {
$tickets = \AKSRT\Payment\AfterSale::get_instance()->get_list([
'order_number' => $order_number,
'limit' => 1,
]);
foreach (($tickets['items'] ?? []) as $ticket) {
if (in_array(($ticket['status'] ?? ''), ['pending', 'processing'], true)) {
return false;
}
}
}
return true;
}
public function has_reviewed(int $user_id, string $order_number, int $product_id): bool {
$comments = get_comments([
'post_id' => $product_id,
'user_id' => $user_id,
'type' => self::COMMENT_TYPE,
'number' => 1,
'meta_query' => [
['key' => '_aksrt_order_number', 'value' => $order_number],
],
]);
return !empty($comments);
}
public function get_reviews(int $product_id, array $args = []): array {
$page = max(1, (int) ($args['page'] ?? 1));
$per_page = max(1, min(50, (int) ($args['per_page'] ?? 10)));
$filter = sanitize_key((string) ($args['filter'] ?? ''));
$query_args = [
'post_id' => $product_id,
'type' => self::COMMENT_TYPE,
'status' => 'approve',
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
'orderby' => 'comment_date',
'order' => 'DESC',
];
if ($filter === 'good') {
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 4, 'compare' => '>=', 'type' => 'NUMERIC']];
} elseif ($filter === 'bad') {
$query_args['meta_query'] = [['key' => '_aksrt_rating_average', 'value' => 3, 'compare' => '<', 'type' => 'NUMERIC']];
} elseif ($filter === 'has_image') {
$query_args['meta_query'] = [['key' => '_aksrt_review_images', 'compare' => 'EXISTS']];
}
$comments = get_comments($query_args);
$total = (int) get_comments(array_merge($query_args, ['count' => true, 'number' => 0, 'offset' => 0]));
$items = [];
foreach ($comments as $comment) {
$cid = (int) $comment->comment_ID;
$items[] = [
'id' => $cid,
'user_id' => (int) $comment->user_id,
'user_name' => (string) $comment->comment_author,
'user_avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url((int) $comment->user_id) : get_avatar_url((int) $comment->user_id),
'content' => (string) $comment->comment_content,
'date' => (string) $comment->comment_date,
'rating_product' => (int) get_comment_meta($cid, '_aksrt_rating_product', true),
'rating_service' => (int) get_comment_meta($cid, '_aksrt_rating_service', true),
'rating_shipping' => (int) get_comment_meta($cid, '_aksrt_rating_shipping', true),
'rating_average' => (float) get_comment_meta($cid, '_aksrt_rating_average', true),
'images' => (array) (get_comment_meta($cid, '_aksrt_review_images', true) ?: []),
'order_number' => (string) get_comment_meta($cid, '_aksrt_order_number', true),
];
}
return ['items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $per_page];
}
public function get_product_scores(int $product_id): array {
$cached = get_post_meta($product_id, '_aksrt_review_scores', true);
if (is_array($cached)) {
return $cached;
}
return $this->refresh_product_scores($product_id);
}
public function refresh_product_scores(int $product_id): array {
global $wpdb;
$results = $wpdb->get_row($wpdb->prepare(
"SELECT COUNT(*) as total,
AVG(CAST(m.meta_value AS DECIMAL(3,1))) as average
FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
$product_id, self::COMMENT_TYPE
), ARRAY_A);
$total = (int) ($results['total'] ?? 0);
$average = $total > 0 ? round((float) ($results['average'] ?? 0), 1) : 0;
$good_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
AND CAST(m.meta_value AS DECIMAL(3,1)) >= 4",
$product_id, self::COMMENT_TYPE
));
$bad_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_rating_average'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'
AND CAST(m.meta_value AS DECIMAL(3,1)) < 3",
$product_id, self::COMMENT_TYPE
));
$has_image = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
JOIN {$wpdb->commentmeta} m ON c.comment_ID = m.comment_id AND m.meta_key = '_aksrt_review_images'
WHERE c.comment_post_ID = %d AND c.comment_type = %s AND c.comment_approved = '1'",
$product_id, self::COMMENT_TYPE
));
$scores = [
'total' => $total,
'average' => $average,
'good' => $good_count,
'bad' => $bad_count,
'has_image' => $has_image,
'level' => self::RATING_LEVELS[min(5, max(0, (int) round($average)))] ?? '',
];
update_post_meta($product_id, '_aksrt_review_scores', $scores);
return $scores;
}
public function run_auto_positive_cron(): void {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15));
$cutoff = date('Y-m-d H:i:s', strtotime("-{$max_day} days"));
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$orders = $repo->get_orders_admin([
'status' => \AKSRT\Payment\Order::STATUS_PENDING_REVIEW,
'limit' => 200,
]);
foreach ($orders as $order) {
$completed_at = (string) ($order['completed_at'] ?? $order['delivered_at'] ?? '');
if ($completed_at === '' || $completed_at > $cutoff) {
continue;
}
$user_id = (int) ($order['user_id'] ?? 0);
$order_number = (string) ($order['order_number'] ?? '');
if ($user_id <= 0 || $order_number === '') continue;
$items = class_exists('AKSRT\\Shop\\Shop')
? Shop::get_instance()->get_order_items($order_number)
: [];
$product_ids = array_column($items, 'product_id');
if (empty($product_ids) && !empty($order['product_id'])) {
$product_ids = [(int) $order['product_id']];
}
foreach ($product_ids as $pid) {
if (!$this->has_reviewed($user_id, $order_number, (int) $pid)) {
$this->submit_review($user_id, $order_number, (int) $pid, [
'rating_product' => 5,
'rating_service' => 5,
'rating_shipping' => 5,
'content' => __('系统默认好评', 'aksrt'),
]);
}
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_COMPLETED, [
'note' => sprintf(__('超过 %d 天未评价,系统自动好评。', 'aksrt'), $max_day),
]);
}
}
/**
* Remaining seconds until auto-positive review, or 0 if unknown/closed.
*/
public function get_auto_review_remaining_seconds(string $order_number): int {
if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Settings\Settings')) {
return 0;
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order) {
return 0;
}
$anchor = (string) ($order['delivered_at'] ?? $order['completed_at'] ?? $order['updated_at'] ?? $order['created_at'] ?? '');
if ($anchor === '') {
return 0;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$max_day = max(1, (int) ($shop['timing']['comment_max_day'] ?? 15));
$deadline = strtotime($anchor . ' +' . $max_day . ' days');
if (!$deadline) {
return 0;
}
$remain = $deadline - time();
return $remain > 0 ? (int) $remain : 0;
}
/**
* Whether shipping score row should be shown for this order.
*/
public function order_needs_shipping_score(array $order): bool {
$method = is_array($order['shipping_method'] ?? null) ? $order['shipping_method'] : [];
$value = strtolower((string) ($method['value'] ?? $method['type'] ?? $order['delivery_type'] ?? ''));
if ($value === '') {
// Prefer showing logistics when shipping records or fee exist.
if (!empty($order['shipping_records']) || !empty($order['shipping']['count'])) {
return true;
}
$fee = (float) ($order['shipping_fee'] ?? ($order['shipping_method']['fee'] ?? 0));
return $fee > 0;
}
if (in_array($value, ['virtual', 'auto', 'none', 'offline', 'self', 'pickup', 'no_shipping'], true)) {
return false;
}
return true;
}
/**
* Build order review modal HTML (zibll order_comment_modal parity).
*
* @return array{success:bool,html?:string,message?:string}
*/
public function get_review_modal_html(int $user_id, string $order_number): array {
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
}
if ($order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在', 'aksrt')];
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order || (int) ($order['user_id'] ?? 0) !== $user_id) {
return ['success' => false, 'message' => __('订单数据异常', 'aksrt')];
}
$product_id = 0;
$items = class_exists('\AKSRT\Shop\Shop')
? Shop::get_instance()->get_order_items($order_number)
: [];
if (!empty($items[0]['product_id'])) {
$product_id = (int) $items[0]['product_id'];
} elseif (!empty($order['product_id'])) {
$product_id = (int) $order['product_id'];
}
if ($product_id <= 0) {
return ['success' => false, 'message' => __('订单商品无效', 'aksrt')];
}
if ($this->has_reviewed($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('订单已评价', 'aksrt')];
}
if (!$this->can_review($user_id, $order_number, $product_id)) {
return ['success' => false, 'message' => __('当前订单不可评价', 'aksrt')];
}
$shop = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_shop_settings()
: [];
$placeholder = (string) ($shop['timing']['comment_placeholder'] ?? '');
if ($placeholder === '') {
$placeholder = __('展开说说对商品的看法吧~', 'aksrt');
}
$img_num = max(0, min(9, (int) ($shop['timing']['comment_img_num'] ?? 3)));
$need_shipping = $this->order_needs_shipping_score($order);
$remain = $this->get_auto_review_remaining_seconds($order_number);
$score_html = '';
$score_html .= $this->render_score_row(__('商品评分', 'aksrt'), 'product');
if ($need_shipping) {
$score_html .= $this->render_score_row(__('物流服务', 'aksrt'), 'shipping');
}
$score_html .= $this->render_score_row(__('服务态度', 'aksrt'), 'service');
$img_html = '';
if ($img_num > 0) {
$img_html = '<div class="form-upload mb10">';
$img_html .= '<div class="em09 muted-2-color mb6">' . esc_html__('图片', 'aksrt') . '</div>';
$img_html .= '<div class="preview" data-aksrt-review-preview data-max="' . esc_attr((string) $img_num) . '">';
$img_html .= '<div class="add" data-aksrt-review-add role="button" tabindex="0" aria-label="' . esc_attr__('添加图片', 'aksrt') . '"></div>';
$img_html .= '</div>';
$img_html .= '<input class="hide" type="file" data-aksrt-review-file multiple accept="image/gif,image/jpeg,image/jpg,image/png,image/webp" />';
$img_html .= '</div>';
}
$title = '<b>' . esc_html__('评价订单', 'aksrt') . '</b>';
if ($remain > 0) {
$title .= '<span class="px12 muted-2-color ml6">'
. '<span data-aksrt-review-countdown="' . esc_attr((string) $remain) . '">' . esc_html($this->format_countdown($remain)) . '</span>'
. esc_html__('后自动好评', 'aksrt')
. '</span>';
}
$html = '<div class="touch border-title">' . $title . '</div>';
$html .= '<button type="button" class="close abs-close aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">&times;</button>';
$html .= '<form class="order-comment-from form-upload" data-aksrt-order-review-form data-order-number="' . esc_attr($order_number) . '" data-product-id="' . esc_attr((string) $product_id) . '" data-need-shipping="' . ($need_shipping ? '1' : '0') . '">';
$html .= '<div class="mb20">' . $score_html . '</div>';
$html .= '<div class="mb20"><textarea class="form-control" name="comment" rows="4" placeholder="' . esc_attr($placeholder) . '"></textarea></div>';
$html .= $img_html;
$html .= '<div class="review-error" data-aksrt-review-error hidden></div>';
$html .= '<div class="but-average modal-buts">';
$html .= '<button type="button" class="but" data-aksrt-remote-modal-close>' . esc_html__('取消', 'aksrt') . '</button>';
$html .= '<button type="submit" class="but c-blue btn-primary" data-aksrt-review-submit><i class="fa fa-check" aria-hidden="true"></i> ' . esc_html__('确认提交', 'aksrt') . '</button>';
$html .= '</div></form>';
return [
'success' => true,
'html' => $html,
'product_id' => $product_id,
'need_shipping' => $need_shipping,
'img_max' => $img_num,
'remain_seconds' => $remain,
];
}
private function render_score_row(string $label, string $key): string {
return '<div class="mb10 shop-score-box flex ac" data-aksrt-score-box data-score-key="' . esc_attr($key) . '" score-value="0">'
. '<div class="em09 muted-2-color score-title">' . esc_html($label) . '</div>'
. '<div class="score-icon em16 mr10 ml20" style="height:30px;"></div>'
. '<div class="score-text em09 muted-2-color"></div>'
. '<input class="score-input" type="hidden" name="score[' . esc_attr($key) . ']" value="0">'
. '</div>';
}
private function format_countdown(int $seconds): string {
$seconds = max(0, $seconds);
$d = intdiv($seconds, 86400);
$h = intdiv($seconds % 86400, 3600);
$m = intdiv($seconds % 3600, 60);
$s = $seconds % 60;
if ($d > 0) {
return sprintf('%d天%02d:%02d:%02d', $d, $h, $m, $s);
}
return sprintf('%02d:%02d:%02d', $h, $m, $s);
}
/**
* Accept https URL or data:image/*;base64,... and return a public URL.
*/
private function normalize_review_image(string $raw, int $user_id): string {
$raw = trim($raw);
if ($raw === '') {
return '';
}
if (preg_match('#^https?://#i', $raw)) {
return esc_url_raw($raw);
}
if (!preg_match('#^data:image/(png|jpe?g|gif|webp);base64,#i', $raw, $m)) {
return '';
}
$binary = base64_decode(substr($raw, strpos($raw, ',') + 1), true);
if ($binary === false || $binary === '') {
return '';
}
if (strlen($binary) > 5 * 1024 * 1024) {
return '';
}
$ext = strtolower($m[1]);
if ($ext === 'jpeg') {
$ext = 'jpg';
}
$filename = 'review-' . $user_id . '-' . wp_generate_password(8, false) . '.' . $ext;
$upload = wp_upload_bits($filename, null, $binary);
if (!empty($upload['error']) || empty($upload['url'])) {
return '';
}
return esc_url_raw((string) $upload['url']);
}
private function get_max_images(): int {
if (!class_exists('\AKSRT\Settings\Settings')) {
return 9;
}
$shop = \AKSRT\Settings\Settings::get_shop_settings();
return max(0, min(20, (int) ($shop['timing']['comment_img_num'] ?? 9)));
}
}
-356
View File
@@ -1,356 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductService {
private Shop $shop;
public function __construct(Shop $shop) {
$this->shop = $shop;
}
/* ============================================================
* Product meta helpers
* ============================================================ */
public function get_term_options(string $taxonomy): array {
$terms = get_terms([
'taxonomy' => $taxonomy,
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
return array_map(static function ($term) {
return [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
];
}, $terms);
}
public function get_product_gallery(int $product_id): array {
$gallery = get_post_meta($product_id, '_aksrt_product_gallery', true);
if (!is_array($gallery)) {
return [];
}
return array_values(array_filter(array_map('esc_url_raw', $gallery)));
}
public function get_product_shipping_profile(int $product_id): array {
return [
'mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
'fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
'delivery_note' => (string) get_post_meta($product_id, '_aksrt_delivery_note', true),
];
}
public function get_product_after_sale_profile(int $product_id): array {
return [
'policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
];
}
public function get_product_marketing_profile(int $product_id): array {
return [
'headline' => (string) get_post_meta($product_id, '_aksrt_marketing_headline', true),
'shipping_tip' => (string) get_post_meta($product_id, '_aksrt_shipping_tip', true),
'service_tip' => (string) get_post_meta($product_id, '_aksrt_service_tip', true),
];
}
public function get_payment_method_options(): array {
$methods = [];
if (class_exists('\AKSRT\Payment\Gateway')) {
foreach (\AKSRT\Payment\Gateway::get_instance()->get_all() as $id => $gateway) {
if (!method_exists($gateway, 'is_configured') || !$gateway->is_configured()) {
continue;
}
$methods[$id] = [
'value' => $id,
'label' => method_exists($gateway, 'get_name') ? (string) $gateway->get_name() : $id,
'description' => $this->gateway_method_description((string) $id),
];
}
}
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled()) {
$methods['points'] = [
'value' => 'points',
'label' => __('积分兑换', 'aksrt'),
'description' => __('使用积分购买积分商品。', 'aksrt'),
];
}
// Fallback only when no configured gateways (and no points).
if (empty($methods)) {
$methods['manual_review'] = [
'value' => 'manual_review',
'label' => __('人工确认', 'aksrt'),
'description' => __('暂无可用支付网关,保留待支付订单由管理员确认。', 'aksrt'),
];
}
return $methods;
}
private function gateway_method_description(string $id): string {
switch ($id) {
case 'balance':
return __('即时完成支付并写回订单状态。', 'aksrt');
case 'alipay':
return __('跳转支付宝完成支付。', 'aksrt');
case 'wechatpay':
return __('微信扫码或跳转完成支付。', 'aksrt');
case 'paypal':
return __('跳转 PayPal 完成支付。', 'aksrt');
case 'stripe':
return __('跳转 Stripe Checkout 完成支付。', 'aksrt');
case 'epay_alipay':
return __('经易支付跳转支付宝完成支付。', 'aksrt');
case 'epay_wxpay':
return __('经易支付跳转微信完成支付。', 'aksrt');
default:
return '';
}
}
public function get_shipping_method_options(): array {
return [
'express' => [
'value' => 'express',
'label' => __('快递配送', 'aksrt'),
'description' => __('标准快递,支持物流跟踪。', 'aksrt'),
'fee' => 12.0,
],
'ems' => [
'value' => 'ems',
'label' => __('EMS', 'aksrt'),
'description' => __('偏远地区可选。', 'aksrt'),
'fee' => 18.0,
],
'download' => [
'value' => 'download',
'label' => __('自动发货', 'aksrt'),
'description' => __('适用于数字商品。', 'aksrt'),
'fee' => 0.0,
],
];
}
public function get_limit_buy_config(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_limit_buy_config', true);
if (!is_array($raw)) {
$legacy = (int) get_post_meta($product_id, '_aksrt_limit_buy', true);
return ['default' => $legacy > 0 ? $legacy : -1, 'desc' => ''];
}
return [
'default' => (int) ($raw['default'] ?? -1),
'vip' => (int) ($raw['vip'] ?? -1),
'vip2' => (int) ($raw['vip2'] ?? -1),
'auth' => (int) ($raw['auth'] ?? -1),
'desc' => sanitize_text_field((string) ($raw['desc'] ?? '')),
];
}
public function get_product_services(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_services', true);
if (!is_array($raw)) return [];
return array_values(array_filter(array_map(static function ($s) {
$name = sanitize_text_field((string) ($s['name'] ?? ''));
if ($name === '') return null;
return [
'name' => $name,
'desc' => sanitize_text_field((string) ($s['desc'] ?? '')),
'icon' => esc_url_raw((string) ($s['icon'] ?? '')),
];
}, $raw)));
}
public function get_product_params(int $product_id): array {
$raw = get_post_meta($product_id, '_aksrt_product_params', true);
if (!is_array($raw)) return [];
return array_values(array_filter(array_map(static function ($p) {
$name = sanitize_text_field((string) ($p['name'] ?? ''));
if ($name === '') return null;
return [
'name' => $name,
'value' => sanitize_text_field((string) ($p['value'] ?? '')),
];
}, $raw)));
}
/**
* Resolve product main image with 3-tier fallback:
* 1. _aksrt_product_image (custom main image)
* 2. WordPress featured image (post thumbnail)
* 3. First gallery image from _aksrt_product_gallery
*/
public function resolve_main_image(int $product_id): string {
$custom = (string) get_post_meta($product_id, '_aksrt_product_image', true);
if ($custom !== '') {
return esc_url_raw($custom);
}
$thumb = get_the_post_thumbnail_url($product_id, 'large');
if ($thumb) {
return (string) $thumb;
}
$gallery = get_post_meta($product_id, '_aksrt_product_gallery', true);
if (is_array($gallery) && !empty($gallery)) {
$first = reset($gallery);
if ($first) {
return esc_url_raw((string) $first);
}
}
return '';
}
public function build_product_card_view(int $product_id): array {
$post = get_post($product_id);
if (!$post || $post->post_type !== 'aksrt_product') {
return [];
}
$stock_raw = (int) get_post_meta($product_id, '_aksrt_stock', true);
$is_unlimited = $stock_raw <= -1;
return [
'id' => $product_id,
'title' => (string) get_the_title($product_id),
'excerpt' => (string) get_the_excerpt($product_id),
'price' => (float) get_post_meta($product_id, '_aksrt_price', true),
'image' => $this->resolve_main_image($product_id),
'stock' => $stock_raw,
'is_unlimited' => $is_unlimited,
'sales' => (int) get_post_meta($product_id, '_aksrt_sales', true),
'status' => (string) get_post_status($product_id),
'created' => get_the_date('Y-m-d H:i', $product_id),
'permalink' => get_permalink($product_id) ?: '',
];
}
/* ============================================================
* Product Variants / SKU
* ============================================================ */
/**
* Get variants table name.
*/
public function variants_table(): string {
global $wpdb;
return $wpdb->prefix . 'aksrt_product_variant';
}
/**
* Get all variants for a product.
*/
public function get_variants(int $product_id): array {
global $wpdb;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->variants_table()} WHERE product_id = %d ORDER BY sort_order ASC, id ASC",
$product_id
), ARRAY_A);
if (!$rows) return [];
return array_map(function ($row) {
$row['id'] = (int) $row['id'];
$row['product_id'] = (int) $row['product_id'];
$row['price'] = (float) $row['price'];
$row['stock'] = (int) $row['stock'];
$row['sort_order'] = (int) $row['sort_order'];
$row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: [];
return $row;
}, $rows);
}
/**
* Get a single variant by ID.
*/
public function get_variant(int $variant_id): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->variants_table()} WHERE id = %d",
$variant_id
), ARRAY_A);
if (!$row) return null;
$row['id'] = (int) $row['id'];
$row['product_id'] = (int) $row['product_id'];
$row['price'] = (float) $row['price'];
$row['stock'] = (int) $row['stock'];
$row['attributes'] = json_decode($row['attributes'] ?? '[]', true) ?: [];
return $row;
}
/**
* Save variants for a product (replace all).
*/
public function save_variants(int $product_id, array $variants): array {
global $wpdb;
$table = $this->variants_table();
// Delete existing variants
$wpdb->delete($table, ['product_id' => $product_id]);
$saved = [];
foreach ($variants as $index => $v) {
$data = [
'product_id' => $product_id,
'sku' => sanitize_text_field((string) ($v['sku'] ?? '')),
'attributes' => wp_json_encode($v['attributes'] ?? []),
'price' => max(0, (float) ($v['price'] ?? 0)),
'stock' => max(0, (int) ($v['stock'] ?? 0)),
'image' => esc_url_raw((string) ($v['image'] ?? '')),
'sort_order' => $index,
'created_at' => current_time('mysql', true),
];
$wpdb->insert($table, $data);
$saved[] = array_merge($data, ['id' => (int) $wpdb->insert_id]);
}
// Update product meta flag
update_post_meta($product_id, '_aksrt_has_variants', count($saved) > 0 ? '1' : '0');
return $saved;
}
/**
* Delete a single variant.
*/
public function delete_variant(int $variant_id): bool {
global $wpdb;
$variant = $this->get_variant($variant_id);
if (!$variant) return false;
$result = $wpdb->delete($this->variants_table(), ['id' => $variant_id]);
if ($result) {
// Update product meta flag
$remaining = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->variants_table()} WHERE product_id = %d",
$variant['product_id']
));
update_post_meta($variant['product_id'], '_aksrt_has_variants', $remaining > 0 ? '1' : '0');
}
return (bool) $result;
}
/**
* Check if a product has variants.
*/
public function has_variants(int $product_id): bool {
return get_post_meta($product_id, '_aksrt_has_variants', true) === '1';
}
}
-188
View File
@@ -1,188 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class ProductVariantsMetaBox {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_product', [$this, 'save'], 30, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_product_variants',
__('SKU 变体', 'aksrt'),
[$this, 'render'],
'aksrt_product',
'normal',
'default'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_product_variants_save', 'aksrt_product_variants_nonce');
$variants = [];
if (class_exists('AKSRT\\Shop\\Shop')) {
$variants = Shop::get_instance()->get_variants((int) $post->ID);
}
if (!$variants) {
$variants = [[
'sku' => '',
'attributes' => [],
'price' => 0,
'stock' => 0,
'image' => '',
]];
}
?>
<style>
.aksrt-variants-table { width:100%; border-collapse:collapse; }
.aksrt-variants-table th, .aksrt-variants-table td { border:1px solid #dcdcde; padding:6px; vertical-align:top; }
.aksrt-variants-table input { width:100%; }
.aksrt-variants-table .col-sku { width:14%; }
.aksrt-variants-table .col-attrs { width:28%; }
.aksrt-variants-table .col-price, .aksrt-variants-table .col-stock { width:12%; }
.aksrt-variants-table .col-image { width:28%; }
.aksrt-variants-hint { color:#646970; font-size:12px; margin:8px 0; }
</style>
<p class="aksrt-variants-hint">
<?php esc_html_e('属性格式:名称:值,多项用分号分隔,例如 颜色:红;尺寸:L。保存时会整体覆盖该商品变体列表。', 'aksrt'); ?>
</p>
<table class="aksrt-variants-table" id="aksrt-variants-table">
<thead>
<tr>
<th class="col-sku"><?php esc_html_e('SKU', 'aksrt'); ?></th>
<th class="col-attrs"><?php esc_html_e('属性', 'aksrt'); ?></th>
<th class="col-price"><?php esc_html_e('价格', 'aksrt'); ?></th>
<th class="col-stock"><?php esc_html_e('库存', 'aksrt'); ?></th>
<th class="col-image"><?php esc_html_e('图片 URL', 'aksrt'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($variants as $i => $row) :
$attrs = $row['attributes'] ?? [];
$attrs_text = '';
if (is_array($attrs)) {
$parts = [];
foreach ($attrs as $k => $v) {
if (is_int($k) && is_array($v)) {
$parts[] = ($v['name'] ?? $v['key'] ?? '') . ':' . ($v['value'] ?? '');
} else {
$parts[] = $k . ':' . $v;
}
}
$attrs_text = implode(';', array_filter($parts));
} elseif (is_string($attrs)) {
$attrs_text = $attrs;
}
?>
<tr>
<td><input type="text" name="aksrt_variant_sku[]" value="<?php echo esc_attr((string) ($row['sku'] ?? '')); ?>"></td>
<td><input type="text" name="aksrt_variant_attrs[]" value="<?php echo esc_attr($attrs_text); ?>" placeholder="颜色:红;尺寸:L"></td>
<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="<?php echo esc_attr((string) ($row['price'] ?? 0)); ?>"></td>
<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="<?php echo esc_attr((string) ($row['stock'] ?? 0)); ?>"></td>
<td><input type="url" name="aksrt_variant_image[]" value="<?php echo esc_attr((string) ($row['image'] ?? '')); ?>"></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p>
<button type="button" class="button" id="aksrt-add-variant-row"><?php esc_html_e('添加变体行', 'aksrt'); ?></button>
</p>
<script>
(function(){
var btn = document.getElementById('aksrt-add-variant-row');
var tbody = document.querySelector('#aksrt-variants-table tbody');
if (!btn || !tbody) return;
btn.addEventListener('click', function(){
var tr = document.createElement('tr');
tr.innerHTML = '<td><input type="text" name="aksrt_variant_sku[]" value=""></td>' +
'<td><input type="text" name="aksrt_variant_attrs[]" value="" placeholder="颜色:红;尺寸:L"></td>' +
'<td><input type="number" step="0.01" min="0" name="aksrt_variant_price[]" value="0"></td>' +
'<td><input type="number" step="1" min="0" name="aksrt_variant_stock[]" value="0"></td>' +
'<td><input type="url" name="aksrt_variant_image[]" value=""></td>';
tbody.appendChild(tr);
});
})();
</script>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_product_variants_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_product_variants_nonce'])), 'aksrt_product_variants_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_product' || !current_user_can('edit_post', $post_id)) {
return;
}
if (!class_exists('AKSRT\\Shop\\Shop')) {
return;
}
$skus = isset($_POST['aksrt_variant_sku']) && is_array($_POST['aksrt_variant_sku']) ? wp_unslash($_POST['aksrt_variant_sku']) : [];
$attrs = isset($_POST['aksrt_variant_attrs']) && is_array($_POST['aksrt_variant_attrs']) ? wp_unslash($_POST['aksrt_variant_attrs']) : [];
$prices = isset($_POST['aksrt_variant_price']) && is_array($_POST['aksrt_variant_price']) ? wp_unslash($_POST['aksrt_variant_price']) : [];
$stocks = isset($_POST['aksrt_variant_stock']) && is_array($_POST['aksrt_variant_stock']) ? wp_unslash($_POST['aksrt_variant_stock']) : [];
$images = isset($_POST['aksrt_variant_image']) && is_array($_POST['aksrt_variant_image']) ? wp_unslash($_POST['aksrt_variant_image']) : [];
$variants = [];
$count = max(count($skus), count($attrs), count($prices), count($stocks), count($images));
for ($i = 0; $i < $count; $i++) {
$sku = sanitize_text_field((string) ($skus[$i] ?? ''));
$attr_raw = sanitize_text_field((string) ($attrs[$i] ?? ''));
$price = max(0, (float) ($prices[$i] ?? 0));
$stock = max(0, (int) ($stocks[$i] ?? 0));
$image = esc_url_raw((string) ($images[$i] ?? ''));
if ($sku === '' && $attr_raw === '' && $price <= 0 && $stock <= 0 && $image === '') {
continue;
}
$attributes = [];
if ($attr_raw !== '') {
foreach (preg_split('/\s*;\s*/', $attr_raw) ?: [] as $pair) {
if ($pair === '' || !str_contains($pair, ':')) {
continue;
}
[$k, $v] = array_map('trim', explode(':', $pair, 2));
if ($k !== '') {
$attributes[$k] = $v;
}
}
}
$variants[] = [
'sku' => $sku,
'attributes' => $attributes,
'price' => $price,
'stock' => $stock,
'image' => $image,
];
}
Shop::get_instance()->save_variants($post_id, $variants);
}
}
-397
View File
@@ -1,397 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class Shop {
use Part1;
use Part2;
use Part3;
private static $instance = null;
private ?CartService $cart_svc = null;
private ?CheckoutService $checkout_svc = null;
private ?AddressService $address_svc = null;
private ?ProductService $product_svc = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function cart_service(): CartService {
if (!$this->cart_svc) {
$this->cart_svc = new CartService($this);
}
return $this->cart_svc;
}
private function checkout_service(): CheckoutService {
if (!$this->checkout_svc) {
$this->checkout_svc = new CheckoutService($this);
}
return $this->checkout_svc;
}
private function address_service(): AddressService {
if (!$this->address_svc) {
$this->address_svc = new AddressService($this);
}
return $this->address_svc;
}
private function product_service(): ProductService {
if ($this->product_svc === null) {
$this->product_svc = new ProductService($this);
}
return $this->product_svc;
}
public function get_config(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_shop_settings();
}
return ['enabled' => false];
}
public function get_products(array $args = []): array {
$limit = max(1, (int) ($args['limit'] ?? 20));
$offset = max(0, (int) ($args['offset'] ?? 0));
$search = sanitize_text_field((string) ($args['search'] ?? ''));
$status = sanitize_key((string) ($args['status'] ?? ''));
$query_args = [
'post_type' => 'aksrt_product',
'posts_per_page' => $limit,
'offset' => $offset,
'post_status' => $status !== '' ? $status : ['publish', 'draft', 'pending'],
'orderby' => 'date',
'order' => 'DESC',
];
if ($search !== '') {
$query_args['s'] = $search;
}
$query = new \WP_Query($query_args);
$products = [];
while ($query->have_posts()) {
$query->the_post();
$products[] = $this->build_product_card_view(get_the_ID());
}
wp_reset_postdata();
return $products;
}
public function get_product_detail_view(int $product_id): array {
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return [];
}
$detail = $this->build_product_card_view($product_id);
$detail['content'] = (string) $product->post_content;
$detail['excerpt'] = (string) $product->post_excerpt;
$detail['permalink'] = get_permalink($product_id) ?: '';
$detail['status'] = (string) get_post_status($product_id);
$detail['gallery'] = $this->product_service()->get_product_gallery($product_id);
$detail['categories'] = wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'all']) ?: [];
$detail['tags'] = wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'all']) ?: [];
$detail['shipping'] = $this->product_service()->get_product_shipping_profile($product_id);
$detail['after_sale'] = $this->product_service()->get_product_after_sale_profile($product_id);
$detail['marketing'] = $this->product_service()->get_product_marketing_profile($product_id);
$detail['checkout_hint'] = $this->checkout_service()->get_checkout_hint($detail);
$detail['purchase_enabled'] = ($detail['is_unlimited'] || $detail['stock'] > 0) && $detail['status'] === 'publish';
$detail['default_quantity'] = 1;
$detail['options'] = $this->get_product_options($product_id);
$detail['has_options'] = $this->has_options($product_id);
$detail['price_range'] = $this->get_price_range($product_id);
$detail['pay_modo'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0';
$detail['start_price'] = (float) get_post_meta($product_id, '_aksrt_start_price', true);
$detail['shipping_type'] = sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_type', true)) ?: 'express';
$detail['auto_delivery'] = $this->get_auto_delivery_config($product_id);
$detail['limit_buy'] = $this->get_limit_buy_config($product_id);
$detail['services'] = $this->get_product_services($product_id);
$detail['params'] = $this->get_product_params($product_id);
return $detail;
}
public function get_product_edit_view(int $user_id, int $product_id = 0): array {
$view = [
'allowed' => true,
'is_edit' => false,
'product' => [
'id' => 0,
'title' => '',
'price' => 0,
'stock' => 0,
'excerpt' => '',
'content' => '',
'image' => '',
'gallery' => [],
'categories' => [],
'tags' => [],
'shipping_mode' => 'express',
'shipping_fee' => 0,
'after_sale_policy' => '',
'support_refund' => true,
'status' => 'draft',
'options' => [],
'pay_modo' => '0',
'start_price' => 0,
'product_video' => [],
'product_params' => '',
],
'taxonomy' => [
'categories' => $this->product_service()->get_term_options('aksrt_product_cat'),
'tags' => $this->product_service()->get_term_options('aksrt_product_tag'),
],
'shipping_modes' => $this->get_shipping_method_options(),
'status_options' => [
['value' => 'draft', 'label' => __('草稿', 'aksrt')],
['value' => 'publish', 'label' => __('发布', 'aksrt')],
['value' => 'pending', 'label' => __('待审核', 'aksrt')],
],
];
if ($product_id <= 0) {
return $view;
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'aksrt_product') {
return $view;
}
$is_owner = (int) $product->post_author === $user_id || current_user_can('manage_options');
if (!$is_owner) {
$view['allowed'] = false;
return $view;
}
$view['is_edit'] = true;
$videos = get_post_meta($product_id, '_aksrt_product_video', true);
if (!is_array($videos)) {
$videos = [];
}
$view['product'] = [
'id' => $product_id,
'title' => (string) $product->post_title,
'price' => (float) get_post_meta($product_id, '_aksrt_price', true),
'stock' => (int) get_post_meta($product_id, '_aksrt_stock', true),
'excerpt' => (string) $product->post_excerpt,
'content' => (string) $product->post_content,
'image' => $this->resolve_main_image($product_id),
'gallery' => $this->product_service()->get_product_gallery($product_id),
'categories' => wp_get_post_terms($product_id, 'aksrt_product_cat', ['fields' => 'ids']) ?: [],
'tags' => wp_get_post_terms($product_id, 'aksrt_product_tag', ['fields' => 'names']) ?: [],
'shipping_mode' => sanitize_key((string) get_post_meta($product_id, '_aksrt_shipping_mode', true)) ?: 'express',
'shipping_fee' => (float) get_post_meta($product_id, '_aksrt_shipping_fee', true),
'after_sale_policy' => (string) get_post_meta($product_id, '_aksrt_after_sale_policy', true),
'support_refund' => get_post_meta($product_id, '_aksrt_support_refund', true) !== '0',
'status' => (string) get_post_status($product_id),
'options' => method_exists($this, 'get_product_options') ? $this->get_product_options($product_id) : [],
'pay_modo' => sanitize_key((string) get_post_meta($product_id, '_aksrt_pay_modo', true)) ?: '0',
'start_price' => (float) get_post_meta($product_id, '_aksrt_start_price', true),
'product_video' => array_values(array_filter(array_map('esc_url_raw', $videos))),
'product_params' => (string) get_post_meta($product_id, '_aksrt_product_params', true),
];
return $view;
}
public function get_cart_page_view(int $user_id): array {
$pending_orders = $this->get_order_center_view($user_id, [
'page' => 1,
'per_page' => 5,
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
]);
return [
'payment_methods' => $this->get_payment_method_options(),
'shipping_methods' => $this->get_shipping_method_options(),
'address_book' => $this->get_address_book($user_id),
'coupon_enabled' => class_exists('\AKSRT\Payment\Coupon'),
'currency_symbol' => '¥',
'pending_orders' => $pending_orders['items'] ?? [],
'cart_items' => $this->get_cart($user_id),
'cart_count' => $this->get_cart_count($user_id),
];
}
public function get_cart(int $user_id): array {
return $this->cart_service()->get_cart($user_id);
}
public function get_cart_count(int $user_id): int {
return $this->cart_service()->get_cart_count($user_id);
}
public function add_to_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
return $this->cart_service()->add_to_cart($user_id, $product_id, $options_string, $count);
}
public function reduce_from_cart(int $user_id, int $product_id, string $options_string = '', int $count = 1): array {
return $this->cart_service()->reduce_from_cart($user_id, $product_id, $options_string, $count);
}
public function remove_from_cart(int $user_id, int $product_id, string $options_string = ''): array {
return $this->cart_service()->remove_from_cart($user_id, $product_id, $options_string);
}
public function remove_multi_from_cart(int $user_id, array $items): array {
return $this->cart_service()->remove_multi_from_cart($user_id, $items);
}
public function empty_cart(int $user_id): array {
return $this->cart_service()->empty_cart($user_id);
}
public function validate_cart_item(int $product_id, string $options_string = '') {
return $this->cart_service()->validate_cart_item($product_id, $options_string);
}
public function get_order_center_view(int $user_id, array $args = []): array {
if (class_exists('\AKSRT\Payment\UserOrders')) {
return \AKSRT\Payment\UserOrders::get_instance()->get_order_center_view($user_id, $args);
}
return ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []];
}
public function get_order_detail_view(int $user_id, string $order_number): ?array {
if (class_exists('\AKSRT\Payment\UserOrders')) {
return \AKSRT\Payment\UserOrders::get_instance()->get_order_detail_view($user_id, $order_number);
}
return null;
}
public function preview_checkout(int $user_id, array $payload): array {
return $this->checkout_service()->preview_checkout($user_id, $payload);
}
public function create_checkout_order(int $user_id, array $payload): array {
return $this->checkout_service()->create_checkout_order($user_id, $payload);
}
public function pay_order(int $user_id, string $order_number, string $payment_method): array {
return $this->checkout_service()->pay_order($user_id, $order_number, $payment_method);
}
public function get_user_favorites(int $user_id, int $page = 1, int $per_page = 20): array {
if ($user_id <= 0) {
return ['items' => [], 'total' => 0];
}
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
if (!is_array($fav_ids) || empty($fav_ids)) {
return ['items' => [], 'total' => 0];
}
$total = count($fav_ids);
$paged = array_slice(array_reverse($fav_ids), ($page - 1) * $per_page, $per_page);
$items = [];
foreach ($paged as $pid) {
$card = $this->build_product_card_view((int) $pid);
if (!empty($card)) {
$items[] = $card;
}
}
return ['items' => $items, 'total' => $total];
}
public function toggle_favorite(int $user_id, int $product_id): array {
if ($user_id <= 0 || $product_id <= 0) {
return ['success' => false];
}
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
if (!is_array($fav_ids)) {
$fav_ids = [];
}
$key = array_search($product_id, $fav_ids, true);
if ($key !== false) {
unset($fav_ids[$key]);
$fav_ids = array_values($fav_ids);
$is_fav = false;
} else {
$fav_ids[] = $product_id;
$is_fav = true;
}
update_user_meta($user_id, 'aksrt_product_favorites', $fav_ids);
$count = (int) get_post_meta($product_id, '_aksrt_favorite_count', true);
update_post_meta($product_id, '_aksrt_favorite_count', max(0, $count + ($is_fav ? 1 : -1)));
if ($is_fav) {
do_action('aksrt_product_favorited', $user_id, $product_id);
} else {
do_action('aksrt_product_unfavorited', $user_id, $product_id);
}
return ['success' => true, 'is_favorite' => $is_fav, 'count' => count($fav_ids)];
}
public function is_favorite(int $user_id, int $product_id): bool {
if ($user_id <= 0) return false;
$fav_ids = get_user_meta($user_id, 'aksrt_product_favorites', true);
return is_array($fav_ids) && in_array($product_id, $fav_ids, true);
}
public function re_purchase(int $user_id, string $order_number): array {
$items = $this->get_order_items($order_number);
if (empty($items)) {
return ['success' => false, 'message' => __('订单无商品记录', 'aksrt')];
}
$added = 0;
foreach ($items as $item) {
$pid = (int) $item['product_id'];
$qty = max(1, (int) $item['quantity']);
$opt_key = (string) $item['options_key'];
$result = $this->add_to_cart($user_id, $pid, $opt_key, $qty);
if (!empty($result['success'])) {
$added++;
}
}
return ['success' => $added > 0, 'added' => $added, 'cart_count' => $this->get_cart_count($user_id)];
}
public function cancel_order(int $user_id, string $order_number): array {
if ($user_id <= 0 || $order_number === '' || !class_exists('\AKSRT\Payment\OrderRepository')) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get_user_order($user_id, $order_number);
if (!$order) {
return ['success' => false, 'message' => __('订单不存在。', 'aksrt')];
}
$status = (string) ($order['status'] ?? '');
if (!in_array($status, [\AKSRT\Payment\Order::STATUS_PENDING, \AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_EXPIRED], true)) {
return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')];
}
$repo->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
'force' => true,
'note' => __('用户在前台取消订单。', 'aksrt'),
]);
return [
'success' => true,
'data' => [
'order' => $this->get_order_detail_view($user_id, $order_number),
],
];
}
}
-748
View File
@@ -1,748 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Shop;
defined('ABSPATH') || exit;
class TermMeta {
private static $instance = null;
/** @var string Meta key for product_cat/product_tag unified storage */
const META_KEY = 'aksrt_shop_term_meta';
/** @var string Meta key for shop_discount unified storage */
const DISCOUNT_META_KEY = 'aksrt_shop_discount_meta';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Register hooks.
*/
public function init(): void {
add_action('init', [$this, 'register_discount_taxonomy'], 5);
add_action('init', [$this, 'register_term_meta_fields'], 10);
// Native term UI for shop_discount (zibll-style admin ops).
add_action('shop_discount_add_form_fields', [$this, 'render_discount_add_fields']);
add_action('shop_discount_edit_form_fields', [$this, 'render_discount_edit_fields'], 10, 1);
add_action('created_shop_discount', [$this, 'save_discount_term_fields']);
add_action('edited_shop_discount', [$this, 'save_discount_term_fields']);
// Product category inheritance fields.
add_action('aksrt_product_cat_add_form_fields', [$this, 'render_cat_add_fields']);
add_action('aksrt_product_cat_edit_form_fields', [$this, 'render_cat_edit_fields'], 10, 1);
add_action('created_aksrt_product_cat', [$this, 'save_cat_term_fields']);
add_action('edited_aksrt_product_cat', [$this, 'save_cat_term_fields']);
// Product tag display fields.
add_action('aksrt_product_tag_add_form_fields', [$this, 'render_tag_add_fields']);
add_action('aksrt_product_tag_edit_form_fields', [$this, 'render_tag_edit_fields'], 10, 1);
add_action('created_aksrt_product_tag', [$this, 'save_tag_term_fields']);
add_action('edited_aksrt_product_tag', [$this, 'save_tag_term_fields']);
}
// ─── Taxonomy Registration ───────────────────────────────────
/**
* Register the shop_discount taxonomy for discount campaigns.
*/
public function register_discount_taxonomy(): void {
if (taxonomy_exists('shop_discount')) {
return;
}
$shop_enabled = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$shop_settings = \AKSRT\Settings\Settings::get_shop_settings();
$shop_enabled = !empty($shop_settings['enabled']);
}
$shop_show_ui = true;
$shop_show_menu = is_admin() ? $shop_enabled : false;
register_taxonomy('shop_discount', 'aksrt_product', [
'labels' => [
'name' => __('优惠活动', 'aksrt'),
'singular_name' => __('优惠活动', 'aksrt'),
'search_items' => __('搜索优惠活动', 'aksrt'),
'all_items' => __('所有优惠活动', 'aksrt'),
'edit_item' => __('编辑优惠活动', 'aksrt'),
'add_new_item' => __('新增优惠活动', 'aksrt'),
'menu_name' => __('优惠活动', 'aksrt'),
],
'public' => true,
'hierarchical' => false,
'show_ui' => $shop_show_ui,
'show_in_menu' => $shop_show_menu,
'show_in_rest' => true,
'show_admin_column' => true,
'rewrite' => ['slug' => 'shop-discount', 'with_front' => false],
]);
}
// ─── Term Meta Registration ──────────────────────────────────
/**
* Register all term meta fields for REST API exposure.
*/
public function register_term_meta_fields(): void {
$auth = function () {
return current_user_can('manage_categories');
};
// ── aksrt_product_cat ──
$cat_fields = $this->get_cat_field_definitions();
foreach ($cat_fields as $key => $def) {
register_term_meta('aksrt_product_cat', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
// ── aksrt_product_tag ──
$tag_fields = $this->get_tag_field_definitions();
foreach ($tag_fields as $key => $def) {
register_term_meta('aksrt_product_tag', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
// ── shop_discount ──
$discount_fields = $this->get_discount_field_definitions();
foreach ($discount_fields as $key => $def) {
register_term_meta('shop_discount', $key, [
'type' => $def['type'],
'description' => $def['description'] ?? $key,
'single' => true,
'default' => $def['default'] ?? '',
'show_in_rest' => true,
'sanitize_callback' => $def['sanitize'] ?? 'sanitize_text_field',
'auth_callback' => $auth,
]);
}
}
// ─── Field Definitions ───────────────────────────────────────
/**
* Product category term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_cat_field_definitions(): array {
return [
'shop_cat_limit_buy' => [
'type' => 'integer', 'description' => '分类级限购 (-1 不限)', 'default' => -1,
'sanitize' => function ($v) { return max(-1, min(9999, (int) $v)); },
],
'shop_cat_shipping_fee' => [
'type' => 'string', 'description' => '分类级运费类型', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'free', 'fixed', 'amount'], true) ? $v : '';
},
],
'shop_cat_guest_buy' => [
'type' => 'string', 'description' => '分类级免登录购买', 'default' => '',
],
'shop_cat_email_fill' => [
'type' => 'string', 'description' => '分类级邮箱必留', 'default' => '',
],
'shop_cat_after_sale' => [
'type' => 'string', 'description' => '分类级售后政策', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'refund', 'refund_return', 'none'], true) ? $v : '';
},
],
'shop_cat_rebate' => [
'type' => 'string', 'description' => '分类级返佣覆盖', 'default' => '',
],
'shop_cat_service' => [
'type' => 'string', 'description' => '分类级服务保障', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_cat_single_tab' => [
'type' => 'string', 'description' => '分类级自定义 TAB', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_cat_content_after' => [
'type' => 'string', 'description' => '分类级详情页底部内容', 'default' => '',
'sanitize' => 'wp_kses_post',
],
'shop_cat_content_layout' => [
'type' => 'string', 'description' => '分类级内容布局', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'adapt', 'full', 'sidebar'], true) ? $v : '';
},
],
'shop_cat_content_show_bg' => [
'type' => 'string', 'description' => '分类级详情背景盒子', 'default' => '',
],
'shop_cat_list_style' => [
'type' => 'string', 'description' => '分类页列表样式', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'default', 'card-lg', 'card-sm'], true) ? $v : '';
},
],
];
}
/**
* Product tag term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_tag_field_definitions(): array {
return [
'shop_tag_priority' => [
'type' => 'integer', 'description' => '标签优先级(影响显示顺序)', 'default' => 0,
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
],
'shop_tag_class' => [
'type' => 'string', 'description' => '标签颜色 class', 'default' => '',
'sanitize' => 'sanitize_html_class',
],
'shop_tag_is_important' => [
'type' => 'string', 'description' => '重点标签(商品价格位着重显示)', 'default' => '',
],
];
}
/**
* Shop discount term meta field definitions.
*
* @return array<string, array{type:string, description:string, default:mixed, sanitize?:callable}>
*/
private function get_discount_field_definitions(): array {
return [
'shop_discount_small_badge' => [
'type' => 'string', 'description' => '小标签文案', 'default' => '',
],
'shop_discount_priority' => [
'type' => 'integer', 'description' => '优先级(数字越大越优先)', 'default' => 0,
'sanitize' => function ($v) { return max(0, min(9999, (int) $v)); },
],
'shop_discount_is_important' => [
'type' => 'string', 'description' => '重点活动', 'default' => '',
],
'shop_discount_important_class' => [
'type' => 'string', 'description' => '重点活动背景色 class', 'default' => '',
'sanitize' => 'sanitize_html_class',
],
'shop_discount_scope' => [
'type' => 'string', 'description' => '优惠范围', 'default' => 'item',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['item', 'product', 'author', 'order'], true) ? $v : 'item';
},
],
'shop_discount_type' => [
'type' => 'string', 'description' => '优惠类型', 'default' => 'reduction',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['reduction', 'discount', 'gift'], true) ? $v : 'reduction';
},
],
'shop_discount_value' => [
'type' => 'string', 'description' => '优惠数值(立减金额/折扣 0.01-9.99)', 'default' => '0',
'sanitize' => function ($v) { return sanitize_text_field((string) $v); },
],
'shop_discount_gift_config' => [
'type' => 'string', 'description' => '赠品配置 JSON', 'default' => '',
'sanitize' => 'sanitize_textarea_field',
],
'shop_discount_price_limit' => [
'type' => 'string', 'description' => '金额门槛', 'default' => '0',
'sanitize' => function ($v) { return max(0, (float) $v); },
],
'shop_discount_user_limit' => [
'type' => 'string', 'description' => '用户身份限制', 'default' => '',
'sanitize' => function ($v) {
$v = sanitize_key((string) $v);
return in_array($v, ['', 'vip', 'vip_2', 'auth'], true) ? $v : '';
},
],
'shop_discount_start' => [
'type' => 'string', 'description' => '活动开始时间', 'default' => '',
'sanitize' => 'sanitize_text_field',
],
'shop_discount_end' => [
'type' => 'string', 'description' => '活动结束时间', 'default' => '',
'sanitize' => 'sanitize_text_field',
],
'shop_discount_countdown' => [
'type' => 'string', 'description' => '倒计时显示', 'default' => '',
],
];
}
// ─── Getters ─────────────────────────────────────────────────
/**
* Get all shop term meta for a product category.
*
* @param int $term_id
* @return array
*/
public static function get_cat_meta(int $term_id): array {
$defaults = [
'limit_buy' => -1,
'shipping_fee' => '',
'guest_buy' => '',
'email_fill' => '',
'after_sale' => '',
'rebate' => '',
'service' => '',
'single_tab' => '',
'content_after' => '',
'content_layout' => '',
'content_show_bg' => '',
'list_style' => '',
];
$result = [];
foreach ($defaults as $key => $default) {
$meta_key = "shop_cat_{$key}";
$val = get_term_meta($term_id, $meta_key, true);
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
}
return $result;
}
/**
* Get all shop term meta for a product tag.
*
* @param int $term_id
* @return array
*/
public static function get_tag_meta(int $term_id): array {
$priority = get_term_meta($term_id, 'shop_tag_priority', true);
$class = get_term_meta($term_id, 'shop_tag_class', true);
$is_important = get_term_meta($term_id, 'shop_tag_is_important', true);
return [
'priority' => $priority !== '' ? (int) $priority : 0,
'class' => is_string($class) ? $class : '',
'is_important' => !empty($is_important),
];
}
/**
* Get all shop discount term meta.
*
* @param int $term_id
* @return array
*/
public static function get_discount_meta(int $term_id): array {
$defaults = [
'small_badge' => '',
'priority' => 0,
'is_important' => false,
'important_class' => '',
'scope' => 'item',
'type' => 'reduction',
'value' => '0',
'gift_config' => '',
'price_limit' => '0',
'user_limit' => '',
'start' => '',
'end' => '',
'countdown' => '',
];
$result = [];
foreach ($defaults as $key => $default) {
$meta_key = "shop_discount_{$key}";
$val = get_term_meta($term_id, $meta_key, true);
$result[$key] = ($val !== '' && $val !== false) ? $val : $default;
}
$result['priority'] = (int) $result['priority'];
$result['is_important'] = !empty($result['is_important']);
$result['price_limit'] = (float) $result['price_limit'];
return $result;
}
/**
* Resolve parameter inheritance: product → category → theme settings.
*
* Returns the effective value for a given parameter key by checking:
* 1. Product-level override (from post meta)
* 2. Category-level override (from term meta)
* 3. Theme-level default (from \AKSRT\Settings\Settings::get_shop_settings())
*
* @param int $product_id Product post ID
* @param string $param_key Parameter key (e.g. 'limit_buy', 'shipping_fee')
* @param mixed $product_value Product-level value (null if not set)
* @return mixed
*/
public static function resolve_inheritance(int $product_id, string $param_key, $product_value = null) {
// 1. Product-level override
if ($product_value !== null && $product_value !== '' && $product_value !== -1) {
return $product_value;
}
// 2. Category-level override
$terms = get_the_terms($product_id, 'aksrt_product_cat');
if (is_array($terms) && !empty($terms)) {
$cat_meta_key = "shop_cat_{$param_key}";
foreach ($terms as $term) {
$val = get_term_meta($term->term_id, $cat_meta_key, true);
if ($val !== '' && $val !== false && $val !== -1) {
return $val;
}
}
}
// 3. Theme-level default
$shop = \AKSRT\Settings\Settings::get_shop_settings();
$defaults = $shop['product_defaults'] ?? [];
return $defaults[$param_key] ?? $product_value;
}
/**
* Add form fields for shop_discount taxonomy.
*/
public function render_discount_add_fields(): void {
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$default = $def['default'] ?? '';
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html($label) . '</label>';
$this->render_discount_field_control($key, $default, false);
echo '</div>';
}
}
/**
* Edit form fields for shop_discount taxonomy.
*/
public function render_discount_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
$label = (string) ($def['description'] ?? $key);
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field">';
echo '<th scope="row"><label for="' . esc_attr($key) . '">' . esc_html($label) . '</label></th>';
echo '<td>';
$this->render_discount_field_control($key, $value, true);
echo '</td></tr>';
}
}
/**
* @param string|int|float $value
*/
private function render_discount_field_control(string $key, $value, bool $edit): void {
$id = esc_attr($key);
$name = esc_attr($key);
$val = (string) $value;
if ($key === 'shop_discount_type') {
$opts = [
'reduction' => __('立减', 'aksrt'),
'discount' => __('折扣', 'aksrt'),
'gift' => __('赠品', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_scope') {
$opts = [
'item' => __('单品', 'aksrt'),
'product' => __('商品', 'aksrt'),
'author' => __('商家', 'aksrt'),
'order' => __('整单', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_discount_user_limit') {
$opts = [
'' => __('不限', 'aksrt'),
'vip' => __('VIP1', 'aksrt'),
'vip_2' => __('VIP2', 'aksrt'),
'auth' => __('已认证', 'aksrt'),
];
echo '<select name="' . $name . '" id="' . $id . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
echo '<label><input type="checkbox" name="' . $name . '" id="' . $id . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
if ($key === 'shop_discount_gift_config') {
echo '<textarea name="' . $name . '" id="' . $id . '" rows="3" class="large-text">' . esc_textarea($val) . '</textarea>';
return;
}
$type = in_array($key, ['shop_discount_priority'], true) ? 'number' : 'text';
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true)) {
$type = 'datetime-local';
// Convert mysql-ish values to datetime-local if needed.
if ($val !== '' && str_contains($val, ' ')) {
$val = str_replace(' ', 'T', substr($val, 0, 16));
}
}
printf(
'<input type="%s" name="%s" id="%s" value="%s" class="%s" />',
esc_attr($type),
$name,
$id,
esc_attr($val),
$edit ? 'regular-text' : ''
);
}
public function save_discount_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
$fields = $this->get_discount_field_definitions();
foreach ($fields as $key => $def) {
if (in_array($key, ['shop_discount_is_important', 'shop_discount_countdown'], true)) {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
if (in_array($key, ['shop_discount_start', 'shop_discount_end'], true) && is_string($value) && str_contains($value, 'T')) {
$value = str_replace('T', ' ', $value);
if (strlen($value) === 16) {
$value .= ':00';
}
}
update_term_meta($term_id, $key, $value);
}
}
public function render_cat_add_fields(): void {
foreach ($this->get_cat_field_definitions() as $key => $def) {
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
$this->render_cat_field_control($key, $def['default'] ?? '', false);
echo '</div>';
}
}
public function render_cat_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
foreach ($this->get_cat_field_definitions() as $key => $def) {
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
$this->render_cat_field_control($key, $value, true);
echo '</td></tr>';
}
}
/**
* @param mixed $value
*/
private function render_cat_field_control(string $key, $value, bool $edit): void {
$val = (string) $value;
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
if (in_array($key, ['shop_cat_service', 'shop_cat_single_tab', 'shop_cat_content_after'], true)) {
echo '<textarea class="large-text" rows="3" name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">' . esc_textarea($val) . '</textarea>';
return;
}
if ($key === 'shop_cat_shipping_fee') {
$opts = ['' => __('继承', 'aksrt'), 'free' => __('包邮', 'aksrt'), 'fixed' => __('固定运费', 'aksrt'), 'amount' => __('满额包邮', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_after_sale') {
$opts = ['' => __('继承', 'aksrt'), 'refund' => __('仅退款', 'aksrt'), 'refund_return' => __('退货退款', 'aksrt'), 'none' => __('不支持', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_content_layout') {
$opts = ['' => __('继承', 'aksrt'), 'adapt' => __('自适应', 'aksrt'), 'full' => __('通栏', 'aksrt'), 'sidebar' => __('侧栏', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
if ($key === 'shop_cat_list_style') {
$opts = ['' => __('继承', 'aksrt'), 'default' => __('默认', 'aksrt'), 'card-lg' => __('大卡片', 'aksrt'), 'card-sm' => __('小卡片', 'aksrt')];
echo '<select name="' . esc_attr($key) . '" id="' . esc_attr($key) . '">';
foreach ($opts as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($val, $k, false), esc_html($label));
}
echo '</select>';
return;
}
$type = $key === 'shop_cat_limit_buy' ? 'number' : 'text';
printf(
'<input type="%s" class="%s" name="%s" id="%s" value="%s" />',
esc_attr($type),
$edit ? 'regular-text' : '',
esc_attr($key),
esc_attr($key),
esc_attr($val)
);
}
public function save_cat_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
foreach ($this->get_cat_field_definitions() as $key => $def) {
if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
update_term_meta($term_id, $key, $value);
}
}
public function render_tag_add_fields(): void {
foreach ($this->get_tag_field_definitions() as $key => $def) {
echo '<div class="form-field">';
echo '<label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label>';
$this->render_tag_field_control($key, $def['default'] ?? '');
echo '</div>';
}
}
public function render_tag_edit_fields($term): void {
if (!($term instanceof \WP_Term)) {
return;
}
foreach ($this->get_tag_field_definitions() as $key => $def) {
$value = get_term_meta($term->term_id, $key, true);
if ($value === '' || $value === false) {
$value = $def['default'] ?? '';
}
echo '<tr class="form-field"><th scope="row"><label for="' . esc_attr($key) . '">' . esc_html((string) ($def['description'] ?? $key)) . '</label></th><td>';
$this->render_tag_field_control($key, $value);
echo '</td></tr>';
}
}
/**
* @param mixed $value
*/
private function render_tag_field_control(string $key, $value): void {
$val = (string) $value;
if ($key === 'shop_tag_is_important') {
echo '<label><input type="checkbox" name="' . esc_attr($key) . '" value="1" ' . checked($val, '1', false) . '> ' . esc_html__('启用', 'aksrt') . '</label>';
return;
}
$type = $key === 'shop_tag_priority' ? 'number' : 'text';
printf(
'<input type="%s" class="regular-text" name="%s" id="%s" value="%s" />',
esc_attr($type),
esc_attr($key),
esc_attr($key),
esc_attr($val)
);
}
public function save_tag_term_fields(int $term_id): void {
if (!current_user_can('manage_categories')) {
return;
}
foreach ($this->get_tag_field_definitions() as $key => $def) {
if ($key === 'shop_tag_is_important') {
$raw = isset($_POST[$key]) ? '1' : '';
} else {
if (!isset($_POST[$key])) {
continue;
}
$raw = wp_unslash($_POST[$key]);
}
if (isset($def['sanitize']) && is_callable($def['sanitize'])) {
$value = call_user_func($def['sanitize'], $raw);
} else {
$value = sanitize_text_field((string) $raw);
}
update_term_meta($term_id, $key, $value);
}
}
}
-401
View File
@@ -1,401 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
/**
* Shop product list widget — zibll `zib_shop_widget_ui_product_lists` lite.
*
* Supports: include/exclude category, discount, tag; sort order; list style;
* AJAX load-more (via rest widget list endpoint); per-page count.
*/
class WidgetProductList extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_product_list',
__('商品列表', 'aksrt'),
['description' => __('AKSRT 商品列表组件:筛选、排序、AJAX 翻页。', 'aksrt')]
);
}
/**
* Build WP_Query args from instance + page.
*
* @param array $instance
* @param int $paged
* @return array
*/
public static function build_query_args(array $instance, int $paged = 1): array {
$per_page = max(1, min(50, (int) ($instance['per_page'] ?? 8)));
$orderby = sanitize_key($instance['orderby'] ?? 'date');
$order = in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true)
? strtoupper((string) $instance['order'])
: 'DESC';
$cat_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['cat_ids'] ?? '')))));
$tag_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['tag_ids'] ?? '')))));
$discount_ids = array_values(array_filter(array_map('intval', explode(',', (string) ($instance['discount_ids'] ?? '')))));
$exclude_cat = !empty($instance['exclude_cat']);
$query_args = [
'post_type' => 'aksrt_product',
'post_status' => 'publish',
'posts_per_page' => $per_page,
'paged' => max(1, $paged),
'orderby' => $orderby === 'sales' ? 'meta_value_num' : ($orderby === 'views' ? 'meta_value_num' : $orderby),
'order' => $order,
'ignore_sticky_posts' => true,
];
if ($orderby === 'sales') {
$query_args['meta_key'] = '_aksrt_sales';
} elseif ($orderby === 'views') {
$query_args['meta_key'] = 'aksrt_post_views';
}
$tax_query = [];
if (!empty($cat_ids)) {
$tax_query[] = [
'taxonomy' => 'aksrt_product_cat',
'field' => 'term_id',
'terms' => $cat_ids,
'operator' => $exclude_cat ? 'NOT IN' : 'IN',
];
}
if (!empty($tag_ids)) {
$tax_query[] = [
'taxonomy' => 'aksrt_product_tag',
'field' => 'term_id',
'terms' => $tag_ids,
];
}
if (!empty($discount_ids)) {
$tax_query[] = [
'taxonomy' => 'shop_discount',
'field' => 'term_id',
'terms' => $discount_ids,
];
}
if (!empty($tax_query)) {
$query_args['tax_query'] = $tax_query;
}
return $query_args;
}
/**
* Render product cards HTML for a query.
*/
public static function render_items_html(\WP_Query $products, string $style = 'default'): string {
if (!$products->have_posts()) {
return '';
}
$oneline = $style === 'oneline';
$mini = $style === 'mini';
$list = $style === 'list';
ob_start();
while ($products->have_posts()) {
$products->the_post();
$pid = get_the_ID();
$card = class_exists('\AKSRT\Shop\Shop')
? \AKSRT\Shop\Shop::get_instance()->build_product_card_view($pid)
: [];
$price = (float) ($card['price'] ?? 0);
$image = (string) ($card['image'] ?? '');
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
if ($oneline) {
echo '<div class="swiper-slide ajax-item">';
}
if ($mini || $list) {
$row_cls = $list ? 'aksrt-product-widget__item aksrt-product-widget__item--list ajax-item flex ac' : 'aksrt-product-widget__item aksrt-product-widget__item--mini ajax-item flex ac';
$thumb_cls = $list ? 'aksrt-product-widget__thumb list' : 'aksrt-product-widget__thumb mini';
$wh = $list ? 80 : 56;
echo '<div class="' . esc_attr($row_cls) . '">';
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="' . esc_attr($thumb_cls) . '">';
if ($image !== '') {
echo '<img class="aksrt-lazy lazyload fit-cover" data-src="' . esc_url($image) . '" src="' . esc_url($ph) . '" alt="' . esc_attr(get_the_title($pid)) . '" loading="lazy" width="' . (int) $wh . '" height="' . (int) $wh . '">';
}
echo '</a>';
echo '<div class="aksrt-product-widget__info flex1 min-w0">';
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__title' . ($mini ? ' text-ellipsis' : ' text-ellipsis-2') . '">' . esc_html(get_the_title($pid)) . '</a>';
echo '<div class="aksrt-product-widget__meta flex jsb ac">';
echo '<span class="aksrt-product-widget__price c-red">&yen;' . esc_html(number_format_i18n($price, 2)) . '</span>';
if (!empty($card['sales'])) {
echo '<span class="aksrt-product-widget__sales muted-2-color em09">' . esc_html((string) $card['sales']) . esc_html__(' 已售', 'aksrt') . '</span>';
}
echo '</div></div></div>';
} elseif (function_exists('get_template_part') && !empty($card['id'])) {
get_template_part('templates/parts/shop/product', 'card', [
'product' => $card,
'show_sales' => true,
]);
} else {
// Fallback card (lazy thumbs).
echo '<div class="aksrt-product-widget__item ajax-item">';
if ($image !== '') {
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__thumb">';
echo '<img class="aksrt-lazy lazyload" data-src="' . esc_url($image) . '" src="' . esc_url($ph) . '" alt="' . esc_attr(get_the_title($pid)) . '" loading="lazy">';
echo '</a>';
}
echo '<div class="aksrt-product-widget__info">';
echo '<a href="' . esc_url(get_permalink($pid)) . '" class="aksrt-product-widget__title">' . esc_html(get_the_title($pid)) . '</a>';
echo '<div class="aksrt-product-widget__meta">';
echo '<span class="aksrt-product-widget__price">&yen;' . esc_html(number_format_i18n($price, 2)) . '</span>';
if (!empty($card['sales'])) {
echo '<span class="aksrt-product-widget__sales">' . esc_html((string) $card['sales']) . esc_html__(' 已售', 'aksrt') . '</span>';
}
echo '</div></div></div>';
}
if ($oneline) {
echo '</div>';
}
}
wp_reset_postdata();
return (string) ob_get_clean();
}
/**
* Shared payload for REST widget list.
*
* @param array $instance
* @param int $page
* @return array{html:string,paginate_html:string,has_more:bool,page:int,max_pages:int,total:int}
*/
public static function query_payload(array $instance, int $page = 1): array {
$page = max(1, $page);
$args = self::build_query_args($instance, $page);
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$products = new \WP_Query($args);
$html = self::render_items_html($products, $style);
if ($html === '' && $page === 1) {
$html = function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无商品', 'aksrt'))
: '<p class="aksrt-product-widget__empty ajax-item">' . esc_html__('暂无商品', 'aksrt') . '</p>';
}
$max_pages = max(1, (int) $products->max_num_pages);
$total = (int) $products->found_posts;
$has_more = $page < $max_pages;
$paginate_html = '';
if ($paginate === 'ajax' && $has_more) {
$next = add_query_arg(
array_filter([
'page' => $page + 1,
'per_page' => max(1, (int) ($instance['per_page'] ?? 8)),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')),
'order' => sanitize_key((string) ($instance['order'] ?? 'DESC')),
'cat_ids' => (string) ($instance['cat_ids'] ?? ''),
'tag_ids' => (string) ($instance['tag_ids'] ?? ''),
'discount_ids' => (string) ($instance['discount_ids'] ?? ''),
'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0,
'style' => sanitize_key((string) ($instance['style'] ?? 'default')),
'paginate' => 'ajax',
], static function ($v) {
return $v !== '' && $v !== null;
}),
rest_url('aksrt/v1/shop/frontend/widgets/products')
);
if (function_exists('aksrt_get_ajax_next_paginate')) {
$paginate_html = aksrt_get_ajax_next_paginate(
$total,
$page,
max(1, (int) ($instance['per_page'] ?? 8)),
$next
);
} else {
$paginate_html = '<div class="text-center theme-pagination ajax-pag"><div class="next-page ajax-next"><a href="' . esc_url($next) . '">' . esc_html__('加载更多', 'aksrt') . '</a></div></div>';
}
} elseif ($paginate === 'number' && $max_pages > 1) {
$num_base = add_query_arg(
array_filter([
'per_page' => max(1, (int) ($instance['per_page'] ?? 8)),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')),
'order' => sanitize_key((string) ($instance['order'] ?? 'DESC')),
'cat_ids' => (string) ($instance['cat_ids'] ?? ''),
'tag_ids' => (string) ($instance['tag_ids'] ?? ''),
'discount_ids' => (string) ($instance['discount_ids'] ?? ''),
'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0,
'style' => sanitize_key((string) ($instance['style'] ?? 'default')),
'paginate' => 'number',
], static function ($v) {
return $v !== '' && $v !== null;
}),
rest_url('aksrt/v1/shop/frontend/widgets/products')
);
$paginate_html = function_exists('aksrt_get_ajax_number_paginate')
? aksrt_get_ajax_number_paginate($total, $page, max(1, (int) ($instance['per_page'] ?? 8)), $num_base)
: '<div class="aksrt-product-widget__pagination">' . paginate_links([
'base' => add_query_arg('aksrt_product_page', '%#%'),
'format' => '',
'current' => $page,
'total' => $max_pages,
'prev_text' => '&laquo;',
'next_text' => '&raquo;',
]) . '</div>';
} else {
$paginate_html = '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
}
return [
'html' => $html,
'paginate_html' => $paginate_html,
'has_more' => $has_more,
'page' => $page,
'max_pages' => $max_pages,
'total' => $total,
];
}
public function widget($args, $instance): void {
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$page = max(1, (int) ($_GET['aksrt_product_page'] ?? 1)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$cfg = [
'per_page' => max(1, min(50, (int) ($instance['per_page'] ?? 8))),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')),
'order' => in_array(strtoupper((string) ($instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $instance['order']) : 'DESC',
'cat_ids' => (string) ($instance['cat_ids'] ?? ''),
'tag_ids' => (string) ($instance['tag_ids'] ?? ''),
'discount_ids' => (string) ($instance['discount_ids'] ?? ''),
'exclude_cat' => !empty($instance['exclude_cat']) ? 1 : 0,
'style' => $style,
'paginate' => $paginate,
];
$payload = self::query_payload($instance, $page);
$ajax_url = rest_url('aksrt/v1/shop/frontend/widgets/products');
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$style_class = 'aksrt-product-widget--' . $style;
echo '<div class="aksrt-product-widget ' . esc_attr($style_class) . '" data-aksrt-product-widget data-aksrt-product-config="' . esc_attr(wp_json_encode($cfg)) . '">';
if ($style === 'oneline') {
// zibll-style horizontal swiper-scroll (shared slider.js Type A).
echo '<div class="swiper-container swiper-scroll aksrt-product-oneline-swiper">';
echo '<div class="swiper-wrapper product-lists-row">';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo '<div class="swiper-button-prev" aria-label="' . esc_attr__('上一页', 'aksrt') . '"></div>';
echo '<div class="swiper-button-next" aria-label="' . esc_attr__('下一页', 'aksrt') . '"></div>';
echo '</div>';
} else {
// Items sit directly under .ajaxpager so ajax-list inject/append works (zibll product-lists-row).
echo '<div class="ajaxpager aksrt-product-widget__grid product-lists-row" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($ajax_url) . '">';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped — cards escaped upstream
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$defaults = [
'title' => __('商品推荐', 'aksrt'),
'per_page' => 8,
'orderby' => 'date',
'order' => 'DESC',
'cat_ids' => '',
'tag_ids' => '',
'discount_ids' => '',
'exclude_cat' => false,
'style' => 'default',
'paginate' => 'ajax',
];
$instance = wp_parse_args($instance, $defaults);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) $instance['title']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('per_page')); ?>"><?php esc_html_e('每页数量:', 'aksrt'); ?></label>
<input class="tiny-text" type="number" min="1" max="50" id="<?php echo esc_attr($this->get_field_id('per_page')); ?>" name="<?php echo esc_attr($this->get_field_name('per_page')); ?>" value="<?php echo esc_attr((string) $instance['per_page']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('orderby')); ?>"><?php esc_html_e('排序:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('orderby')); ?>" name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="date" <?php selected($instance['orderby'], 'date'); ?>><?php esc_html_e('最新', 'aksrt'); ?></option>
<option value="sales" <?php selected($instance['orderby'], 'sales'); ?>><?php esc_html_e('销量', 'aksrt'); ?></option>
<option value="views" <?php selected($instance['orderby'], 'views'); ?>><?php esc_html_e('热度', 'aksrt'); ?></option>
<option value="title" <?php selected($instance['orderby'], 'title'); ?>><?php esc_html_e('标题', 'aksrt'); ?></option>
<option value="rand" <?php selected($instance['orderby'], 'rand'); ?>><?php esc_html_e('随机', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('order')); ?>"><?php esc_html_e('方向:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('order')); ?>" name="<?php echo esc_attr($this->get_field_name('order')); ?>">
<option value="DESC" <?php selected($instance['order'], 'DESC'); ?>><?php esc_html_e('降序', 'aksrt'); ?></option>
<option value="ASC" <?php selected($instance['order'], 'ASC'); ?>><?php esc_html_e('升序', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>"><?php esc_html_e('分类ID(逗号分隔):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('cat_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('cat_ids')); ?>" value="<?php echo esc_attr((string) $instance['cat_ids']); ?>">
</p>
<p>
<label>
<input type="checkbox" name="<?php echo esc_attr($this->get_field_name('exclude_cat')); ?>" value="1" <?php checked(!empty($instance['exclude_cat'])); ?>>
<?php esc_html_e('排除上述分类', 'aksrt'); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>"><?php esc_html_e('标签ID(逗号分隔):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('tag_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('tag_ids')); ?>" value="<?php echo esc_attr((string) $instance['tag_ids']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>"><?php esc_html_e('优惠活动ID(逗号分隔):', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('discount_ids')); ?>" name="<?php echo esc_attr($this->get_field_name('discount_ids')); ?>" value="<?php echo esc_attr((string) $instance['discount_ids']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('style')); ?>"><?php esc_html_e('列表样式:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('style')); ?>" name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="default" <?php selected($instance['style'], 'default'); ?>><?php esc_html_e('默认', 'aksrt'); ?></option>
<option value="card" <?php selected($instance['style'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="list" <?php selected($instance['style'], 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
<option value="oneline" <?php selected($instance['style'], 'oneline'); ?>><?php esc_html_e('横滑', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('paginate')); ?>"><?php esc_html_e('分页:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('paginate')); ?>" name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="ajax" <?php selected($instance['paginate'], 'ajax'); ?>><?php esc_html_e('AJAX 加载更多', 'aksrt'); ?></option>
<option value="number" <?php selected($instance['paginate'], 'number'); ?>><?php esc_html_e('数字分页', 'aksrt'); ?></option>
<option value="none" <?php selected($instance['paginate'], 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
</select>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'per_page' => max(1, min(50, (int) ($new_instance['per_page'] ?? 8))),
'orderby' => sanitize_key((string) ($new_instance['orderby'] ?? 'date')),
'order' => in_array(strtoupper((string) ($new_instance['order'] ?? '')), ['ASC', 'DESC'], true) ? strtoupper((string) $new_instance['order']) : 'DESC',
'cat_ids' => sanitize_text_field((string) ($new_instance['cat_ids'] ?? '')),
'tag_ids' => sanitize_text_field((string) ($new_instance['tag_ids'] ?? '')),
'discount_ids' => sanitize_text_field((string) ($new_instance['discount_ids'] ?? '')),
'exclude_cat' => !empty($new_instance['exclude_cat']) ? 1 : 0,
'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')),
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
@@ -1,52 +0,0 @@
<?php
/**
* Horizontal swiper product widget — uses shared product list payload.
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
defined('ABSPATH') || exit;
class WidgetProductOneline extends WidgetProductList {
public function __construct() {
\WP_Widget::__construct(
'aksrt_product_oneline',
__('横滑商品', 'aksrt'),
[
'description' => __('AKSRT 单行横滑商品(swiper-scroll,同源 product list)。', 'aksrt'),
'classname' => 'widget_aksrt_product_oneline',
]
);
}
public function widget($args, $instance): void {
$instance['style'] = 'oneline';
// Prefer no paginate for horizontal scroller (zibll loads fixed count).
if (!isset($instance['paginate'])) {
$instance['paginate'] = 'none';
}
parent::widget($args, $instance);
}
public function form($instance): void {
$instance = wp_parse_args($instance, [
'title' => __('横滑商品', 'aksrt'),
'style' => 'oneline',
'paginate' => 'none',
'per_page' => 8,
]);
parent::form($instance);
}
public function update($new_instance, $old_instance): array {
$out = parent::update($new_instance, $old_instance);
$out['style'] = 'oneline';
if (($out['paginate'] ?? '') === '') {
$out['paginate'] = 'none';
}
return $out;
}
}
-179
View File
@@ -1,179 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
/**
* Multi-tab product widget — zibll `zib_shop_widget_ui_tab_product` lite.
* First tab renders immediately via shared payload; other tabs lazy-load via REST.
*/
class WidgetProductTab extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_product_tab',
__('多TAB商品', 'aksrt'),
['description' => __('AKSRT 多 TAB 商品列表组件(每 tab 独立筛选 + AJAX)。', 'aksrt')]
);
}
/**
* @return list<array{label:string,cat_ids:string,discount_ids:string,tag_ids:string,orderby:string}>
*/
private static function parse_tabs(string $tabs_raw): array {
$tabs = [];
foreach (array_filter(explode("\n", $tabs_raw)) as $line) {
$parts = explode('|', trim($line));
if (count($parts) >= 1 && trim($parts[0]) !== '') {
$tabs[] = [
'label' => trim($parts[0]),
'cat_ids' => trim($parts[1] ?? ''),
'discount_ids' => trim($parts[2] ?? ''),
'tag_ids' => trim($parts[3] ?? ''),
'orderby' => trim($parts[4] ?? 'date'),
];
}
}
return $tabs;
}
public function widget($args, $instance): void {
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
$tabs_raw = (string) ($instance['tabs'] ?? '');
$per_page = max(1, min(20, (int) ($instance['per_page'] ?? 8)));
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$tabs = self::parse_tabs($tabs_raw);
if (!$tabs) {
if (current_user_can('manage_options')) {
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo function_exists('aksrt_get_widget_null')
? aksrt_get_widget_null(__('请至少配置一个 TAB', 'aksrt')) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
: '<p class="widget-empty">' . esc_html__('请至少配置一个 TAB', 'aksrt') . '</p>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$rest = rest_url('aksrt/v1/shop/frontend/widgets/products');
echo '<div class="aksrt-product-tab-widget" data-aksrt-product-tab data-per-page="' . esc_attr((string) $per_page) . '" data-style="' . esc_attr($style) . '" data-paginate="' . esc_attr($paginate) . '" data-rest="' . esc_url($rest) . '">';
echo '<div class="aksrt-product-tab__headers index-tab">';
foreach ($tabs as $i => $tab) {
$active = $i === 0 ? ' is-active' : '';
echo '<button type="button" class="aksrt-product-tab__header' . esc_attr($active) . '" data-tab-index="' . esc_attr((string) $i) . '"';
echo ' data-cat-ids="' . esc_attr($tab['cat_ids']) . '"';
echo ' data-discount-ids="' . esc_attr($tab['discount_ids']) . '"';
echo ' data-tag-ids="' . esc_attr($tab['tag_ids']) . '"';
echo ' data-orderby="' . esc_attr(sanitize_key($tab['orderby'] ?: 'date')) . '"';
echo '>' . esc_html($tab['label']) . '</button>';
}
echo '</div>';
foreach ($tabs as $i => $tab) {
$hidden = $i > 0 ? ' style="display:none"' : '';
$loaded = $i === 0 ? '1' : '0';
echo '<div class="aksrt-product-tab__panel" data-tab-index="' . esc_attr((string) $i) . '" data-loaded="' . esc_attr($loaded) . '"' . $hidden . '>';
if ($i === 0) {
$inst = [
'per_page' => $per_page,
'orderby' => sanitize_key($tab['orderby'] ?: 'date'),
'order' => 'DESC',
'cat_ids' => $tab['cat_ids'],
'tag_ids' => $tab['tag_ids'],
'discount_ids' => $tab['discount_ids'],
'style' => $style,
'paginate' => $paginate,
];
$payload = class_exists('\AKSRT\Widgets\WidgetProductList')
? WidgetProductList::query_payload($inst, 1)
: ['html' => '', 'paginate_html' => ''];
echo '<div class="ajaxpager aksrt-product-widget__grid product-lists-row aksrt-product-widget--' . esc_attr($style) . '" data-aksrt-ajaxpager>';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div>';
} else {
echo '<div class="ajaxpager aksrt-product-widget__grid product-lists-row aksrt-product-widget--' . esc_attr($style) . ' is-lazy" data-aksrt-ajaxpager>';
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader>';
echo '<div class="aksrt-product-tab__skeleton">';
for ($si = 0; $si < min(4, $per_page); $si++) {
echo '<div class="placeholder-row product-card-placeholder"><div class="placeholder thumb"></div><div class="placeholder k1"></div><div class="placeholder s1"></div></div>';
}
echo '</div></div>';
echo '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
echo '</div>';
}
echo '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$defaults = [
'title' => __('商品精选', 'aksrt'),
'tabs' => "热门推荐|\n最新上架|",
'per_page' => 8,
'style' => 'default',
'paginate' => 'ajax',
];
$instance = wp_parse_args($instance, $defaults);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" type="text" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) $instance['title']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('tabs')); ?>"><?php esc_html_e('TAB配置(每行: 名称|分类ID|优惠ID|标签ID|排序):', 'aksrt'); ?></label>
<textarea class="widefat" rows="5" id="<?php echo esc_attr($this->get_field_id('tabs')); ?>" name="<?php echo esc_attr($this->get_field_name('tabs')); ?>"><?php echo esc_textarea((string) $instance['tabs']); ?></textarea>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('per_page')); ?>"><?php esc_html_e('每页数量:', 'aksrt'); ?></label>
<input class="tiny-text" type="number" min="1" max="20" id="<?php echo esc_attr($this->get_field_id('per_page')); ?>" name="<?php echo esc_attr($this->get_field_name('per_page')); ?>" value="<?php echo esc_attr((string) $instance['per_page']); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('style')); ?>"><?php esc_html_e('样式:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('style')); ?>" name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="default" <?php selected($instance['style'], 'default'); ?>><?php esc_html_e('默认', 'aksrt'); ?></option>
<option value="card" <?php selected($instance['style'], 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('paginate')); ?>"><?php esc_html_e('分页:', 'aksrt'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('paginate')); ?>" name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="ajax" <?php selected($instance['paginate'], 'ajax'); ?>><?php esc_html_e('AJAX', 'aksrt'); ?></option>
<option value="number" <?php selected($instance['paginate'], 'number'); ?>><?php esc_html_e('数字分页', 'aksrt'); ?></option>
<option value="none" <?php selected($instance['paginate'], 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
</select>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'tabs' => sanitize_textarea_field((string) ($new_instance['tabs'] ?? '')),
'per_page' => max(1, min(20, (int) ($new_instance['per_page'] ?? 8))),
'style' => sanitize_key((string) ($new_instance['style'] ?? 'default')),
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
-292
View File
@@ -1,292 +0,0 @@
<?php
/**
* Template Name: AKSRT - Cart
* Template Post Type: page
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
get_header();
if (!is_user_logged_in()) {
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink());
wp_safe_redirect($login_url);
exit;
}
$cart_view = function_exists('aksrt_get_cart_page_view')
? aksrt_get_cart_page_view(get_current_user_id())
: [
'payment_methods' => [],
'shipping_methods' => [],
'address_book' => [],
'coupon_enabled' => false,
'currency_symbol' => '¥',
'pending_orders' => [],
];
$pending_orders = is_array($cart_view['pending_orders'] ?? null) ? $cart_view['pending_orders'] : [];
$shipping_methods = is_array($cart_view['shipping_methods'] ?? null) ? array_values($cart_view['shipping_methods']) : [];
$payment_methods = is_array($cart_view['payment_methods'] ?? null) ? array_values($cart_view['payment_methods']) : [];
$address_book = is_array($cart_view['address_book'] ?? null) ? $cart_view['address_book'] : [];
?>
<main id="main-content" class="site-main content-shell" tabindex="-1">
<?php if (is_active_sidebar('shop_cart_top_fluid')) : ?>
<div class="aksrt-fluid-widget aksrt-fluid-widget--shop"><?php dynamic_sidebar('shop_cart_top_fluid'); ?></div>
<?php endif; ?>
<?php if (function_exists('aksrt_breadcrumbs')) { aksrt_breadcrumbs(); } ?>
<div
class="aksrt-cart-page container"
data-aksrt-cart-page
data-currency-symbol="<?php echo esc_attr((string) ($cart_view['currency_symbol'] ?? '¥')); ?>"
data-coupon-enabled="<?php echo !empty($cart_view['coupon_enabled']) ? '1' : '0'; ?>"
>
<?php
if (function_exists('aksrt_render_commerce_page_header')) {
aksrt_render_commerce_page_header([
'title' => __('购物车', 'aksrt'),
'subtitle' => __('在下单前确认地址、运费、优惠券和支付方式。', 'aksrt'),
'active' => 'cart',
]);
}
?>
<div class="aksrt-cart-page__header">
<?php if (!empty($pending_orders)) : ?>
<div class="aksrt-cart-page__pending">
<span class="aksrt-cart-page__pending-label"><?php esc_html_e('待支付订单', 'aksrt'); ?></span>
<div class="aksrt-cart-page__pending-list">
<?php foreach ($pending_orders as $pending_order) : ?>
<button
type="button"
class="aksrt-cart-page__pending-chip"
data-aksrt-resume-order="<?php echo esc_attr((string) ($pending_order['order_number'] ?? '')); ?>"
>
<?php echo esc_html((string) ($pending_order['order_number'] ?? '')); ?>
</button>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="aksrt-cart-page__layout">
<section class="aksrt-cart-page__main">
<div class="aksrt-cart-page__panel">
<div class="aksrt-cart-page__panel-head">
<h2><?php esc_html_e('商品清单', 'aksrt'); ?></h2>
<button type="button" class="btn-ghost btn-sm" data-aksrt-clear-cart><?php esc_html_e('清空购物车', 'aksrt'); ?></button>
</div>
<div class="aksrt-cart-page__items" data-aksrt-cart-items>
<div class="aksrt-cart-page__empty">
<i class="fas fa-shopping-cart" aria-hidden="true"></i>
<p><?php esc_html_e('购物车为空,去商品页添加想购买的内容。', 'aksrt'); ?></p>
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-outline btn-sm"><?php esc_html_e('继续浏览', 'aksrt'); ?></a>
</div>
</div>
</div>
<div class="aksrt-cart-page__grid">
<section class="aksrt-cart-page__panel">
<div class="aksrt-cart-page__panel-head">
<h2><?php esc_html_e('收货地址', 'aksrt'); ?></h2>
<button type="button" class="btn-ghost btn-sm" data-aksrt-address-add>
<i class="fas fa-plus" aria-hidden="true"></i> <?php esc_html_e('新增地址', 'aksrt'); ?>
</button>
</div>
<div class="aksrt-cart-page__option-list" data-aksrt-address-list>
<?php if (empty($address_book)) : ?>
<div class="aksrt-cart-page__address-empty" data-aksrt-address-empty>
<i class="fas fa-map-marker-alt" aria-hidden="true"></i>
<p><?php esc_html_e('暂无收货地址,请先新增。', 'aksrt'); ?></p>
</div>
<?php endif; ?>
<?php foreach ($address_book as $index => $address) : ?>
<div class="aksrt-cart-page__option-card aksrt-cart-page__address-card" data-aksrt-address-card="<?php echo esc_attr((string) ($address['id'] ?? 0)); ?>">
<label class="aksrt-cart-page__address-label">
<input
type="radio"
name="aksrt_cart_address"
value="<?php echo esc_attr((string) ($address['id'] ?? 0)); ?>"
<?php checked($index === 0 || !empty($address['is_default'])); ?>
>
<span class="aksrt-cart-page__option-body">
<strong>
<?php echo esc_html((string) ($address['name'] ?? '')); ?>
<?php if (!empty($address['is_default'])) : ?>
<span class="aksrt-cart-page__default-tag"><?php esc_html_e('默认', 'aksrt'); ?></span>
<?php endif; ?>
</strong>
<span><?php echo esc_html((string) ($address['phone'] ?? '')); ?></span>
<span><?php echo esc_html((string) ($address['full_text'] ?? '')); ?></span>
</span>
</label>
<div class="aksrt-cart-page__address-actions">
<button type="button" class="btn-ghost btn-xs" data-aksrt-address-edit="<?php echo esc_attr((string) ($address['id'] ?? 0)); ?>"><?php esc_html_e('编辑', 'aksrt'); ?></button>
<button type="button" class="btn-ghost btn-xs" data-aksrt-address-delete="<?php echo esc_attr((string) ($address['id'] ?? 0)); ?>"><?php esc_html_e('删除', 'aksrt'); ?></button>
<?php if (empty($address['is_default'])) : ?>
<button type="button" class="btn-ghost btn-xs" data-aksrt-address-set-default="<?php echo esc_attr((string) ($address['id'] ?? 0)); ?>"><?php esc_html_e('设为默认', 'aksrt'); ?></button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<!-- Address Editor Modal -->
<div class="aksrt-modal" data-aksrt-address-modal hidden>
<div class="aksrt-modal__overlay"></div>
<div class="aksrt-modal__dialog">
<div class="aksrt-modal__header">
<h3 data-aksrt-address-modal-title><?php esc_html_e('新增地址', 'aksrt'); ?></h3>
<button type="button" class="aksrt-modal__close" data-aksrt-address-modal-close>&times;</button>
</div>
<form class="aksrt-modal__body" data-aksrt-address-form>
<input type="hidden" name="address_id" value="">
<div class="aksrt-cart-page__field">
<label><?php esc_html_e('收货人', 'aksrt'); ?> *</label>
<input type="text" name="name" class="aksrt-cart-page__input" required maxlength="50">
</div>
<div class="aksrt-cart-page__field">
<label><?php esc_html_e('手机号', 'aksrt'); ?> *</label>
<input type="tel" name="phone" class="aksrt-cart-page__input" required maxlength="20">
</div>
<div class="aksrt-cart-page__field-row">
<div class="aksrt-cart-page__field">
<label><?php esc_html_e('省份', 'aksrt'); ?></label>
<input type="text" name="province" class="aksrt-cart-page__input" maxlength="50">
</div>
<div class="aksrt-cart-page__field">
<label><?php esc_html_e('城市', 'aksrt'); ?></label>
<input type="text" name="city" class="aksrt-cart-page__input" maxlength="50">
</div>
<div class="aksrt-cart-page__field">
<label><?php esc_html_e('区/县', 'aksrt'); ?></label>
<input type="text" name="district" class="aksrt-cart-page__input" maxlength="50">
</div>
</div>
<div class="aksrt-cart-page__field">
<label><?php esc_html_e('详细地址', 'aksrt'); ?> *</label>
<input type="text" name="address" class="aksrt-cart-page__input" required maxlength="200">
</div>
<label class="aksrt-cart-page__checkbox">
<input type="checkbox" name="is_default" value="1">
<span><?php esc_html_e('设为默认地址', 'aksrt'); ?></span>
</label>
<div class="aksrt-modal__footer">
<button type="button" class="btn-ghost" data-aksrt-address-modal-close><?php esc_html_e('取消', 'aksrt'); ?></button>
<button type="submit" class="btn-primary"><?php esc_html_e('保存地址', 'aksrt'); ?></button>
</div>
</form>
</div>
</div>
<section class="aksrt-cart-page__panel">
<div class="aksrt-cart-page__panel-head">
<h2><?php esc_html_e('配送与支付', 'aksrt'); ?></h2>
</div>
<div class="aksrt-cart-page__option-columns">
<div>
<h3 class="aksrt-cart-page__mini-title"><?php esc_html_e('配送方式', 'aksrt'); ?></h3>
<div class="aksrt-cart-page__option-list" data-aksrt-shipping-list>
<?php foreach ($shipping_methods as $index => $method) : ?>
<label class="aksrt-cart-page__option-card">
<input
type="radio"
name="aksrt_cart_shipping"
value="<?php echo esc_attr((string) ($method['value'] ?? '')); ?>"
<?php checked($index === 0); ?>
>
<span class="aksrt-cart-page__option-body">
<strong><?php echo esc_html((string) ($method['label'] ?? '')); ?></strong>
<span><?php echo esc_html((string) ($method['description'] ?? '')); ?></span>
</span>
</label>
<?php endforeach; ?>
</div>
</div>
<div>
<h3 class="aksrt-cart-page__mini-title"><?php esc_html_e('支付方式', 'aksrt'); ?></h3>
<div class="aksrt-cart-page__option-list" data-aksrt-payment-list>
<?php foreach ($payment_methods as $index => $method) : ?>
<label class="aksrt-cart-page__option-card">
<input
type="radio"
name="aksrt_cart_payment"
value="<?php echo esc_attr((string) ($method['value'] ?? '')); ?>"
<?php checked($index === 0); ?>
>
<span class="aksrt-cart-page__option-body">
<strong><?php echo esc_html((string) ($method['label'] ?? '')); ?></strong>
<span><?php echo esc_html((string) ($method['description'] ?? '')); ?></span>
</span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</section>
<section class="aksrt-cart-page__panel">
<div class="aksrt-cart-page__panel-head">
<h2><?php esc_html_e('优惠与备注', 'aksrt'); ?></h2>
</div>
<div class="aksrt-cart-page__coupon-box">
<label class="aksrt-cart-page__field">
<span><?php esc_html_e('优惠券', 'aksrt'); ?></span>
<div class="aksrt-cart-page__coupon-row">
<input type="text" class="aksrt-cart-page__input" data-aksrt-coupon-code placeholder="<?php esc_attr_e('输入优惠码后试算', 'aksrt'); ?>">
<button type="button" class="btn-outline btn-sm" data-aksrt-preview-checkout><?php esc_html_e('应用并试算', 'aksrt'); ?></button>
</div>
</label>
<p class="aksrt-cart-page__hint" data-aksrt-preview-message><?php esc_html_e('试算后会展示优惠抵扣、运费和最终应付金额。', 'aksrt'); ?></p>
<label class="aksrt-cart-page__field">
<span><?php esc_html_e('订单备注', 'aksrt'); ?></span>
<textarea class="aksrt-cart-page__textarea" data-aksrt-customer-note rows="4" placeholder="<?php esc_attr_e('例如发票要求、发货备注等。', 'aksrt'); ?>"></textarea>
</label>
</div>
</section>
</div>
</section>
<aside class="aksrt-cart-page__sidebar">
<section class="aksrt-cart-page__panel aksrt-cart-page__summary">
<div class="aksrt-cart-page__panel-head">
<h2><?php esc_html_e('订单摘要', 'aksrt'); ?></h2>
</div>
<dl class="aksrt-cart-page__summary-list">
<div>
<dt><?php esc_html_e('商品小计', 'aksrt'); ?></dt>
<dd data-aksrt-summary-subtotal>¥0.00</dd>
</div>
<div>
<dt><?php esc_html_e('运费', 'aksrt'); ?></dt>
<dd data-aksrt-summary-shipping>¥0.00</dd>
</div>
<div>
<dt><?php esc_html_e('优惠抵扣', 'aksrt'); ?></dt>
<dd data-aksrt-summary-discount>-¥0.00</dd>
</div>
</dl>
<div class="aksrt-cart-page__summary-total">
<span><?php esc_html_e('应付总额', 'aksrt'); ?></span>
<strong data-aksrt-summary-total>¥0.00</strong>
</div>
<button type="button" class="btn-primary btn-lg aksrt-cart-page__submit" data-aksrt-create-order disabled>
<?php esc_html_e('创建订单', 'aksrt'); ?>
</button>
</section>
</aside>
</div>
</div>
</main>
<?php if (is_active_sidebar('shop_cart_bottom_fluid')) : ?>
<div class="aksrt-fluid-widget aksrt-fluid-widget--shop"><?php dynamic_sidebar('shop_cart_bottom_fluid'); ?></div>
<?php endif; ?>
<?php get_footer(); ?>
-218
View File
@@ -1,218 +0,0 @@
<?php
/**
* Template Name: AKSRT - Product Edit
* Template Post Type: page
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
get_header();
if (!is_user_logged_in()) {
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink());
wp_safe_redirect($login_url);
exit;
}
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
if (empty($shop_settings['enabled'])) {
wp_safe_redirect(home_url('/'));
exit;
}
$product_id = (int) ($_GET['product_id'] ?? 0);
$edit_view = function_exists('aksrt_get_product_edit_page_view')
? aksrt_get_product_edit_page_view(get_current_user_id(), $product_id)
: ['allowed' => false, 'is_edit' => false, 'product' => [], 'taxonomy' => ['categories' => [], 'tags' => []], 'shipping_modes' => [], 'status_options' => []];
$product = (array) ($edit_view['product'] ?? []);
?>
<main id="main-content" class="site-main content-shell" tabindex="-1">
<?php if (function_exists('aksrt_breadcrumbs')) { aksrt_breadcrumbs(); } ?>
<div
class="aksrt-product-editor container"
data-aksrt-product-editor
data-product-id="<?php echo esc_attr((string) ($product['id'] ?? 0)); ?>"
data-is-edit="<?php echo !empty($edit_view['is_edit']) ? '1' : '0'; ?>"
>
<?php if (empty($edit_view['allowed'])) : ?>
<div class="aksrt-product-editor__empty">
<i class="fas fa-lock" aria-hidden="true"></i>
<p><?php esc_html_e('你没有权限编辑该商品。', 'aksrt'); ?></p>
</div>
<?php else : ?>
<div class="aksrt-product-editor__header">
<div>
<h1 class="aksrt-product-editor__title">
<?php echo !empty($edit_view['is_edit']) ? esc_html__('编辑商品', 'aksrt') : esc_html__('发布商品', 'aksrt'); ?>
</h1>
<p class="aksrt-product-editor__subtitle"><?php esc_html_e('管理主图、图集、分类标签、发货方式、运费和售后说明。', 'aksrt'); ?></p>
</div>
</div>
<form class="aksrt-product-editor__layout" data-aksrt-product-form>
<section class="aksrt-product-editor__main">
<div class="aksrt-product-editor__panel">
<h2><?php esc_html_e('基础信息', 'aksrt'); ?></h2>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('商品名称', 'aksrt'); ?></span>
<input type="text" name="title" class="aksrt-product-editor__input" value="<?php echo esc_attr((string) ($product['title'] ?? '')); ?>" required>
</label>
<div class="aksrt-product-editor__columns">
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('价格', 'aksrt'); ?></span>
<input type="number" name="price" class="aksrt-product-editor__input" min="0" step="0.01" value="<?php echo esc_attr((string) ($product['price'] ?? 0)); ?>" required>
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('库存', 'aksrt'); ?></span>
<input type="number" name="stock" class="aksrt-product-editor__input" min="0" step="1" value="<?php echo esc_attr((string) ($product['stock'] ?? 0)); ?>" required>
</label>
</div>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('商品简介', 'aksrt'); ?></span>
<textarea name="excerpt" class="aksrt-product-editor__textarea" rows="4"><?php echo esc_textarea((string) ($product['excerpt'] ?? '')); ?></textarea>
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('商品详情', 'aksrt'); ?></span>
<textarea name="content" class="aksrt-product-editor__textarea" rows="10"><?php echo esc_textarea((string) ($product['content'] ?? '')); ?></textarea>
</label>
</div>
<div class="aksrt-product-editor__panel">
<h2><?php esc_html_e('商品素材', 'aksrt'); ?></h2>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('主图 URL', 'aksrt'); ?></span>
<input type="url" name="image" class="aksrt-product-editor__input" value="<?php echo esc_attr((string) ($product['image'] ?? '')); ?>" placeholder="https://">
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('图集 URL(每行一个)', 'aksrt'); ?></span>
<textarea name="gallery" class="aksrt-product-editor__textarea" rows="5"><?php echo esc_textarea(implode("\n", (array) ($product['gallery'] ?? []))); ?></textarea>
</label>
</div>
<div class="aksrt-product-editor__panel">
<h2><?php esc_html_e('规格选项', 'aksrt'); ?></h2>
<p class="aksrt-product-editor__hint"><?php esc_html_e('每行一组:规格名|选项1:加价,选项2:加价。例如:颜色|红:0,蓝:10', 'aksrt'); ?></p>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('规格配置', 'aksrt'); ?></span>
<textarea name="options_text" class="aksrt-product-editor__textarea" rows="6" data-aksrt-options-text placeholder="颜色|红:0,蓝:10&#10;尺码|M:0,L:5"><?php
$opt_lines = [];
foreach ((array) ($product['options'] ?? []) as $group) {
$vals = [];
foreach ((array) ($group['values'] ?? []) as $v) {
$vals[] = (string) ($v['label'] ?? '') . ':' . (string) ($v['price_change'] ?? 0);
}
if (!empty($group['name']) && $vals) {
$opt_lines[] = (string) $group['name'] . '|' . implode(',', $vals);
}
}
echo esc_textarea(implode("\n", $opt_lines));
?></textarea>
</label>
<div class="aksrt-product-editor__columns">
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('起售价', 'aksrt'); ?></span>
<input type="number" name="start_price" class="aksrt-product-editor__input" min="0" step="0.01" value="<?php echo esc_attr((string) ($product['start_price'] ?? 0)); ?>">
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('支付模式', 'aksrt'); ?></span>
<select name="pay_modo" class="aksrt-product-editor__input">
<option value="0" <?php selected((string) ($product['pay_modo'] ?? '0'), '0'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected((string) ($product['pay_modo'] ?? '0'), 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
</label>
</div>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('商品参数(每行 名称:值)', 'aksrt'); ?></span>
<textarea name="product_params" class="aksrt-product-editor__textarea" rows="4"><?php echo esc_textarea((string) ($product['product_params'] ?? '')); ?></textarea>
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('视频地址(每行一个 URL)', 'aksrt'); ?></span>
<textarea name="product_video" class="aksrt-product-editor__textarea" rows="3"><?php echo esc_textarea(implode("\n", (array) ($product['product_video'] ?? []))); ?></textarea>
</label>
</div>
<div class="aksrt-product-editor__panel">
<h2><?php esc_html_e('发货与售后', 'aksrt'); ?></h2>
<div class="aksrt-product-editor__columns">
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('发货方式', 'aksrt'); ?></span>
<select name="shipping_mode" class="aksrt-product-editor__input">
<?php foreach ((array) ($edit_view['shipping_modes'] ?? []) as $mode) : ?>
<option value="<?php echo esc_attr((string) ($mode['value'] ?? '')); ?>" <?php selected((string) ($product['shipping_mode'] ?? ''), (string) ($mode['value'] ?? '')); ?>>
<?php echo esc_html((string) ($mode['label'] ?? '')); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('额外运费', 'aksrt'); ?></span>
<input type="number" name="shipping_fee" class="aksrt-product-editor__input" min="0" step="0.01" value="<?php echo esc_attr((string) ($product['shipping_fee'] ?? 0)); ?>">
</label>
</div>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('售后说明', 'aksrt'); ?></span>
<textarea name="after_sale_policy" class="aksrt-product-editor__textarea" rows="4"><?php echo esc_textarea((string) ($product['after_sale_policy'] ?? '')); ?></textarea>
</label>
<label class="aksrt-product-editor__checkbox">
<input type="checkbox" name="support_refund" value="1" <?php checked(!empty($product['support_refund'])); ?>>
<span><?php esc_html_e('支持退款/售后入口', 'aksrt'); ?></span>
</label>
</div>
</section>
<aside class="aksrt-product-editor__sidebar">
<div class="aksrt-product-editor__panel">
<h2><?php esc_html_e('发布设置', 'aksrt'); ?></h2>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('状态', 'aksrt'); ?></span>
<select name="status" class="aksrt-product-editor__input">
<?php foreach ((array) ($edit_view['status_options'] ?? []) as $status_option) : ?>
<option value="<?php echo esc_attr((string) ($status_option['value'] ?? 'draft')); ?>" <?php selected((string) ($product['status'] ?? 'draft'), (string) ($status_option['value'] ?? 'draft')); ?>>
<?php echo esc_html((string) ($status_option['label'] ?? '')); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('分类(按住 Ctrl 可多选)', 'aksrt'); ?></span>
<select name="categories[]" class="aksrt-product-editor__input" multiple size="6">
<?php foreach ((array) (($edit_view['taxonomy']['categories'] ?? [])) as $category) : ?>
<option value="<?php echo esc_attr((string) ($category['id'] ?? 0)); ?>" <?php echo in_array((int) ($category['id'] ?? 0), (array) ($product['categories'] ?? []), true) ? 'selected' : ''; ?>>
<?php echo esc_html((string) ($category['name'] ?? '')); ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="aksrt-product-editor__field">
<span><?php esc_html_e('标签(英文逗号分隔)', 'aksrt'); ?></span>
<input type="text" name="tags" class="aksrt-product-editor__input" value="<?php echo esc_attr(implode(', ', (array) ($product['tags'] ?? []))); ?>">
</label>
</div>
<div class="aksrt-product-editor__panel">
<h2><?php esc_html_e('动作', 'aksrt'); ?></h2>
<div class="aksrt-product-editor__actions">
<button type="submit" class="btn-primary btn-lg"><?php echo !empty($edit_view['is_edit']) ? esc_html__('保存修改', 'aksrt') : esc_html__('发布商品', 'aksrt'); ?></button>
<a href="<?php echo esc_url(function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')); ?>" class="btn-ghost btn-lg"><?php esc_html_e('返回中心', 'aksrt'); ?></a>
</div>
<p class="aksrt-product-editor__hint" data-aksrt-product-editor-message><?php esc_html_e('保存后会回到用户中心,商品信息供订单页和商品详情页复用。', 'aksrt'); ?></p>
</div>
</aside>
</form>
<?php endif; ?>
</div>
</main>
<?php get_footer(); ?>
-471
View File
@@ -1,471 +0,0 @@
<?php
/**
* Template Name: AKSRT - Product Detail
* Template Post Type: page
*
* Enhanced: options selector, video gallery, params/services modal,
* review tab, favorites, related products, mobile tabbar (§6.10).
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
get_header();
$product_id = (int) ($_GET['product_id'] ?? 0);
if ($product_id <= 0) {
$product_id = (int) get_query_var('product_id', 0);
}
$shop = class_exists('\AKSRT\Shop\Shop') ? \AKSRT\Shop\Shop::get_instance() : null;
$product = function_exists('aksrt_get_product_page_view')
? aksrt_get_product_page_view($product_id)
: ($shop ? $shop->get_product_detail_view($product_id) : []);
$user_id = get_current_user_id();
$is_fav = $shop && $user_id > 0 ? $shop->is_favorite($user_id, $product_id) : false;
$options = $product['options'] ?? [];
$has_options = !empty($options);
$price_range = $product['price_range'] ?? [];
$params = $product['params'] ?? [];
$services = $product['services'] ?? [];
$pay_modo = $product['pay_modo'] ?? '0';
$scores = class_exists('\AKSRT\Shop\ProductReview') ? \AKSRT\Shop\ProductReview::get_instance()->get_product_scores($product_id) : [];
$reviews = class_exists('\AKSRT\Shop\ProductReview') ? \AKSRT\Shop\ProductReview::get_instance()->get_reviews($product_id, ['per_page' => 5]) : ['items' => [], 'total' => 0];
$author_id = (int) get_post_field('post_author', $product_id);
$author = $author_id > 0 ? get_userdata($author_id) : null;
?>
<main id="main-content" class="site-main content-shell content-shell--stream content-shell--product" tabindex="-1">
<?php if (is_active_sidebar('shop_product_top_fluid')) : ?>
<div class="aksrt-fluid-widget aksrt-fluid-widget--shop"><?php dynamic_sidebar('shop_product_top_fluid'); ?></div>
<?php endif; ?>
<?php if (function_exists('aksrt_breadcrumbs')) { aksrt_breadcrumbs(); } ?>
<?php if (!empty($product)) : ?>
<div class="content-wrap container">
<?php $has_shop_sidebar = is_active_sidebar('shop_product_sidebar'); ?>
<div class="content-layout <?php echo $has_shop_sidebar ? 'content-layout--with-sidebar' : 'content-layout--no-sidebar'; ?>">
<div class="content-layout__primary">
<div
class="aksrt-product-page"
data-aksrt-product-page
data-product-id="<?php echo esc_attr((string) $product_id); ?>"
data-options='<?php echo esc_attr(wp_json_encode($options, JSON_UNESCAPED_UNICODE)); ?>'
data-base-price="<?php echo esc_attr((string) (($product['start_price'] ?? 0) > 0 ? $product['start_price'] : ($product['price'] ?? 0))); ?>"
data-base-stock="<?php echo esc_attr((string) ($product['stock'] ?? 0)); ?>"
data-pay-modo="<?php echo esc_attr((string) $pay_modo); ?>"
data-price-range="<?php echo !empty($price_range) && (($price_range['min'] ?? 0) !== ($price_range['max'] ?? 0)) ? '1' : '0'; ?>"
>
<div class="aksrt-product-page__layout">
<!-- Gallery -->
<section class="aksrt-product-page__gallery aksrt-product-page__panel">
<?php if (!empty($product['image'])) : ?>
<div class="aksrt-product-page__primary-image" data-aksrt-main-image>
<img src="<?php echo esc_url((string) $product['image']); ?>" alt="<?php echo esc_attr((string) ($product['title'] ?? '')); ?>" id="product-main-img">
</div>
<?php else : ?>
<div class="aksrt-product-page__placeholder">
<i class="fas fa-image" aria-hidden="true"></i>
</div>
<?php endif; ?>
<?php
$gallery_items = (array) ($product['gallery'] ?? []);
$cover_videos = (array) (get_post_meta($product_id, '_aksrt_cover_videos', true) ?: []);
?>
<?php if (!empty($gallery_items) || !empty($cover_videos)) : ?>
<div class="aksrt-product-page__thumbs">
<?php foreach ($cover_videos as $video_url) : ?>
<button type="button" class="aksrt-product-page__thumb aksrt-product-page__thumb--video" data-aksrt-product-video="<?php echo esc_url($video_url); ?>">
<i class="fas fa-play-circle"></i>
</button>
<?php endforeach; ?>
<?php foreach ($gallery_items as $gallery_image) : ?>
<button type="button" class="aksrt-product-page__thumb" data-aksrt-product-thumb="<?php echo esc_url($gallery_image); ?>">
<img src="<?php echo esc_url($gallery_image); ?>" alt="">
</button>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<!-- Summary -->
<section class="aksrt-product-page__summary aksrt-product-page__panel">
<div class="aksrt-product-page__badges">
<?php if ($pay_modo === 'points') : ?>
<span class="aksrt-product-page__badge aksrt-product-page__badge--points"><?php esc_html_e('积分商品', 'aksrt'); ?></span>
<?php endif; ?>
<?php if (!empty($product['marketing']['headline'])) : ?>
<span class="aksrt-product-page__badge aksrt-product-page__badge--promo"><?php echo esc_html((string) $product['marketing']['headline']); ?></span>
<?php endif; ?>
<?php if (!empty($product['after_sale']['support_refund'])) : ?>
<span class="aksrt-product-page__badge"><?php esc_html_e('支持售后', 'aksrt'); ?></span>
<?php endif; ?>
</div>
<h1 class="aksrt-product-page__title"><?php echo esc_html((string) ($product['title'] ?? '')); ?></h1>
<!-- Price row with range support -->
<div class="aksrt-product-page__price-row">
<?php if ($has_options && !empty($price_range) && $price_range['min'] !== $price_range['max']) : ?>
<strong class="aksrt-product-page__price" data-aksrt-dynamic-price>
<?php echo $pay_modo === 'points' ? '' : '¥'; ?><?php echo esc_html(number_format($price_range['min'], 2)); ?> - <?php echo $pay_modo === 'points' ? '' : '¥'; ?><?php echo esc_html(number_format($price_range['max'], 2)); ?>
<?php if ($pay_modo === 'points') : ?><small><?php esc_html_e('积分', 'aksrt'); ?></small><?php endif; ?>
</strong>
<?php else : ?>
<strong class="aksrt-product-page__price" data-aksrt-dynamic-price>
<?php echo $pay_modo === 'points' ? '' : '¥'; ?><?php echo esc_html(number_format((float) ($product['price'] ?? 0), 2)); ?>
<?php if ($pay_modo === 'points') : ?><small><?php esc_html_e('积分', 'aksrt'); ?></small><?php endif; ?>
</strong>
<?php endif; ?>
<span class="aksrt-product-page__sales"><?php printf(esc_html__('已售 %d', 'aksrt'), (int) ($product['sales'] ?? 0)); ?></span>
<?php if (!empty($scores['average'])) : ?>
<span class="aksrt-product-page__rating"><i class="fas fa-star"></i> <?php echo esc_html((string) $scores['average']); ?></span>
<?php endif; ?>
</div>
<?php if (!empty($product['excerpt'])) : ?>
<p class="aksrt-product-page__excerpt"><?php echo esc_html((string) $product['excerpt']); ?></p>
<?php endif; ?>
<div class="aksrt-product-page__service-strip">
<span><i class="fas fa-shield-alt"></i> <?php esc_html_e('主站统一交易保障', 'aksrt'); ?></span>
<span><i class="fas fa-box-open"></i> <?php esc_html_e('与内容主线共用卡片与购买节奏', 'aksrt'); ?></span>
</div>
<!-- Product options selector -->
<?php if ($has_options) : ?>
<div class="aksrt-product-page__options" data-aksrt-options-selector>
<?php foreach ($options as $group_idx => $group) : ?>
<div class="aksrt-product-page__option-group" data-group-index="<?php echo esc_attr((string) $group_idx); ?>">
<span class="aksrt-product-page__option-label"><?php echo esc_html((string) ($group['name'] ?? '')); ?></span>
<div class="aksrt-product-page__option-values aksrt-product-page__option-values--<?php echo esc_attr($group['view_mode'] ?? 'list'); ?>">
<?php foreach ((array) ($group['values'] ?? []) as $val) : ?>
<button type="button"
class="aksrt-product-page__option-btn"
data-option-group="<?php echo esc_attr((string) $group_idx); ?>"
data-option-label="<?php echo esc_attr((string) ($val['label'] ?? '')); ?>"
data-price-change="<?php echo esc_attr((string) ($val['price_change'] ?? 0)); ?>"
<?php if (!empty($val['image'])) : ?>data-option-image="<?php echo esc_url($val['image']); ?>"<?php endif; ?>
>
<?php if (($group['view_mode'] ?? '') === 'img' && !empty($val['image'])) : ?>
<img src="<?php echo esc_url($val['image']); ?>" alt="<?php echo esc_attr($val['label']); ?>">
<?php endif; ?>
<span><?php echo esc_html((string) ($val['label'] ?? '')); ?></span>
<?php if ((float) ($val['price_change'] ?? 0) !== 0.0) : ?>
<em><?php echo (float) $val['price_change'] > 0 ? '+' : ''; ?><?php echo esc_html(number_format((float) $val['price_change'], 2)); ?></em>
<?php endif; ?>
</button>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
<div class="aksrt-product-page__option-result" data-aksrt-option-result hidden>
<span data-aksrt-selected-labels></span>
<strong data-aksrt-resolved-price></strong>
<span data-aksrt-resolved-stock></span>
</div>
</div>
<?php endif; ?>
<!-- Meta info -->
<dl class="aksrt-product-page__meta">
<div>
<dt><?php esc_html_e('库存', 'aksrt'); ?></dt>
<dd data-aksrt-stock-display><?php
$stock_val = (int) ($product['stock'] ?? 0);
echo $stock_val <= -1 ? esc_html__('充足', 'aksrt') : esc_html((string) $stock_val);
?></dd>
</div>
<div>
<dt><?php esc_html_e('配送', 'aksrt'); ?></dt>
<dd><?php echo esc_html((string) (($product['shipping']['delivery_note'] ?? '') ?: __('标准配送', 'aksrt'))); ?></dd>
</div>
<div>
<dt><?php esc_html_e('运费', 'aksrt'); ?></dt>
<dd><?php
$ship_fee = (float) ($product['shipping']['fee'] ?? 0);
echo $ship_fee > 0 ? '¥' . esc_html(number_format($ship_fee, 2)) : esc_html__('包邮', 'aksrt');
?></dd>
</div>
</dl>
<!-- Params & Services quick links -->
<?php if (!empty($params)) : ?>
<button type="button" class="aksrt-product-page__info-trigger" data-aksrt-show-params>
<i class="fas fa-list-ul"></i> <?php esc_html_e('商品参数', 'aksrt'); ?> <i class="fas fa-chevron-right"></i>
</button>
<?php endif; ?>
<?php if (!empty($services)) : ?>
<button type="button" class="aksrt-product-page__info-trigger" data-aksrt-show-services>
<i class="fas fa-shield-alt"></i> <?php esc_html_e('服务保障', 'aksrt'); ?> <i class="fas fa-chevron-right"></i>
</button>
<?php endif; ?>
<!-- Purchase box -->
<div class="aksrt-product-page__purchase-box">
<label class="aksrt-product-page__field">
<span><?php esc_html_e('购买数量', 'aksrt'); ?></span>
<div class="aksrt-product-page__quantity">
<button type="button" class="aksrt-product-page__qty-btn" data-aksrt-qty-action="dec">-</button>
<input type="number" min="1" max="<?php echo esc_attr((string) max(1, $stock_val > 0 ? $stock_val : 999)); ?>" value="1" class="aksrt-product-page__qty-input" data-aksrt-quantity>
<button type="button" class="aksrt-product-page__qty-btn" data-aksrt-qty-action="inc">+</button>
</div>
</label>
<div class="aksrt-product-page__actions">
<?php if (is_user_logged_in() && !empty($product['purchase_enabled'])) : ?>
<button type="button" class="btn-outline btn-lg" data-aksrt-add-cart><?php esc_html_e('加入购物车', 'aksrt'); ?></button>
<button type="button" class="btn-primary btn-lg" data-aksrt-buy-now>
<?php echo $pay_modo === 'points' ? esc_html__('积分兑换', 'aksrt') : esc_html__('立即下单', 'aksrt'); ?>
</button>
<?php elseif (!is_user_logged_in()) : ?>
<a href="<?php echo esc_url(function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink())); ?>" class="btn-primary btn-lg">
<?php esc_html_e('登录后购买', 'aksrt'); ?>
</a>
<?php else : ?>
<button type="button" class="btn-primary btn-lg" disabled><?php esc_html_e('暂不可购买', 'aksrt'); ?></button>
<?php endif; ?>
</div>
<!-- Auxiliary actions -->
<div class="aksrt-product-page__aux-actions">
<button type="button" class="aksrt-product-page__aux-btn<?php echo $is_fav ? ' is-active' : ''; ?>" data-aksrt-toggle-fav="<?php echo esc_attr((string) $product_id); ?>">
<i class="<?php echo $is_fav ? 'fas' : 'far'; ?> fa-heart"></i>
<?php esc_html_e('收藏', 'aksrt'); ?>
</button>
<button type="button" class="aksrt-product-page__aux-btn" data-aksrt-share>
<i class="fas fa-share-alt"></i> <?php esc_html_e('分享', 'aksrt'); ?>
</button>
</div>
</div>
<!-- Merchant info -->
<?php if ($author) :
$store_url = function_exists('aksrt_get_user_home_url')
? add_query_arg('tab', 'product', aksrt_get_user_home_url($author_id))
: get_author_posts_url($author_id);
?>
<div class="aksrt-product-page__merchant">
<img src="<?php echo esc_url(function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($author_id) : get_avatar_url($author_id)); ?>" alt="" class="aksrt-product-page__merchant-avatar">
<div>
<strong><?php echo esc_html($author->display_name); ?></strong>
<a href="<?php echo esc_url($store_url); ?>" class="aksrt-product-page__merchant-link"><?php esc_html_e('进入店铺', 'aksrt'); ?></a>
</div>
</div>
<?php endif; ?>
</section>
</div>
<!-- Tabs: detail / reviews / custom -->
<div class="aksrt-product-page__tabs">
<nav class="aksrt-product-page__tab-nav">
<button class="aksrt-product-page__tab-btn is-active" data-aksrt-tab="detail"><?php esc_html_e('商品详情', 'aksrt'); ?></button>
<button class="aksrt-product-page__tab-btn" data-aksrt-tab="reviews">
<?php esc_html_e('评价', 'aksrt'); ?>
<?php if (($scores['total'] ?? 0) > 0) : ?>
<em>(<?php echo esc_html((string) $scores['total']); ?>)</em>
<?php endif; ?>
</button>
</nav>
<div class="aksrt-product-page__tab-panel is-active" data-aksrt-tab-panel="detail">
<div class="aksrt-product-page__content">
<?php echo wp_kses_post((string) ($product['content'] ?? '')); ?>
</div>
</div>
<div class="aksrt-product-page__tab-panel" data-aksrt-tab-panel="reviews">
<?php if (!empty($scores['total'])) : ?>
<div class="aksrt-product-page__review-summary shop-comment-header" win-ajax-replace="shop-comment-header">
<div class="comment-score">
<strong class="aksrt-product-page__review-avg"><?php echo esc_html((string) ($scores['average'] ?? '0')); ?></strong>
<span class="aksrt-product-page__review-level"><?php echo esc_html((string) ($scores['level'] ?? '')); ?></span>
</div>
<div class="aksrt-product-page__review-counts comment-filter">
<a href="javascript:;" class="comment-orderby c-theme badg" data-review-filter=""><?php esc_html_e('全部', 'aksrt'); ?></a>
<a href="javascript:;" class="comment-orderby badg" data-review-filter="has_image"><?php printf(esc_html__('有图 %d', 'aksrt'), (int) ($scores['has_image'] ?? 0)); ?></a>
<a href="javascript:;" class="comment-orderby badg" data-review-filter="good"><?php printf(esc_html__('好评 %d', 'aksrt'), (int) ($scores['good'] ?? 0)); ?></a>
<a href="javascript:;" class="comment-orderby badg" data-review-filter="bad"><?php printf(esc_html__('中/差评 %d', 'aksrt'), (int) ($scores['bad'] ?? 0)); ?></a>
</div>
</div>
<?php endif; ?>
<div class="aksrt-product-page__review-list shop-commentlist" data-aksrt-review-list data-product-id="<?php echo esc_attr((string) $product_id); ?>">
<?php foreach (($reviews['items'] ?? []) as $review) : ?>
<div class="aksrt-product-page__review-item comment" data-review-id="<?php echo esc_attr((string) ($review['id'] ?? '')); ?>">
<div class="aksrt-product-page__review-header comment-header">
<img src="<?php echo esc_url((string) ($review['user_avatar'] ?? '')); ?>" alt="" class="aksrt-product-page__review-avatar avatar-img comt-avatar">
<span class="aksrt-product-page__review-name"><?php echo esc_html((string) ($review['user_name'] ?? '')); ?></span>
<span class="aksrt-product-page__review-date"><?php echo esc_html((string) ($review['date'] ?? '')); ?></span>
<span class="aksrt-product-page__review-stars comment-score-badge">
<?php for ($s = 0; $s < 5; $s++) : ?>
<i class="<?php echo $s < (int) round((float) ($review['rating_average'] ?? 0)) ? 'fas' : 'far'; ?> fa-star"></i>
<?php endfor; ?>
</span>
</div>
<div class="aksrt-product-page__review-text comment-content">
<?php
$review_body = (string) ($review['content'] ?? '');
if ($review_body === '') {
echo esc_html__('用户未填写评价内容', 'aksrt');
} elseif (function_exists('aksrt_render_comment_content')) {
echo aksrt_render_comment_content($review_body); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
echo esc_html($review_body);
}
?>
</div>
<?php if (!empty($review['images'])) : ?>
<div class="aksrt-product-page__review-images comment-score-imgs imgbox-container">
<?php foreach ((array) $review['images'] as $rimg) : ?>
<img src="<?php echo esc_url($rimg); ?>" alt="" class="aksrt-product-page__review-img box-img">
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (empty($reviews['items'])) : ?>
<p class="aksrt-product-page__review-empty"><?php esc_html_e('暂无评价', 'aksrt'); ?></p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Related products -->
<?php
$related_items = [];
if ($shop && method_exists($shop, 'build_product_card_view')) {
$related_q = new WP_Query([
'post_type' => 'aksrt_product',
'posts_per_page' => 6,
'post_status' => 'publish',
'post__not_in' => [$product_id],
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'tax_query' => !empty($product['categories']) ? [[
'taxonomy' => 'aksrt_product_cat',
'field' => 'term_id',
'terms' => wp_list_pluck($product['categories'], 'term_id'),
]] : [],
'orderby' => 'rand',
]);
while ($related_q->have_posts()) {
$related_q->the_post();
$card = $shop->build_product_card_view((int) get_the_ID());
if (!empty($card['id'])) {
$related_items[] = $card;
}
}
wp_reset_postdata();
}
?>
<?php if (!empty($related_items)) : ?>
<section class="aksrt-product-page__related">
<h2 class="aksrt-product-page__related-title"><?php esc_html_e('相关推荐', 'aksrt'); ?></h2>
<div class="aksrt-product-archive__grid aksrt-product-archive__grid--default aksrt-product-page__related-grid">
<?php foreach ($related_items as $related_product) :
get_template_part('templates/parts/shop/product', 'card', [
'product' => $related_product,
'show_sales' => ((int) ($related_product['sales'] ?? 0) > 0),
]);
endforeach; ?>
</div>
</section>
<?php endif; ?>
</div>
<!-- Params modal -->
<?php if (!empty($params)) : ?>
<div class="aksrt-modal" data-aksrt-params-modal hidden>
<div class="aksrt-modal__overlay"></div>
<div class="aksrt-modal__dialog">
<div class="aksrt-modal__header">
<h3><?php esc_html_e('商品参数', 'aksrt'); ?></h3>
<button type="button" class="aksrt-modal__close" data-aksrt-modal-close>&times;</button>
</div>
<div class="aksrt-modal__body">
<table class="aksrt-product-page__params-table">
<?php foreach ($params as $param) : ?>
<tr>
<th><?php echo esc_html((string) ($param['name'] ?? '')); ?></th>
<td><?php echo esc_html((string) ($param['value'] ?? '')); ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</div>
<?php endif; ?>
<!-- Services modal -->
<?php if (!empty($services)) : ?>
<div class="aksrt-modal" data-aksrt-services-modal hidden>
<div class="aksrt-modal__overlay"></div>
<div class="aksrt-modal__dialog">
<div class="aksrt-modal__header">
<h3><?php esc_html_e('服务保障', 'aksrt'); ?></h3>
<button type="button" class="aksrt-modal__close" data-aksrt-modal-close>&times;</button>
</div>
<div class="aksrt-modal__body">
<?php foreach ($services as $service) : ?>
<div class="aksrt-product-page__service-item">
<?php if (!empty($service['icon'])) : ?>
<img src="<?php echo esc_url($service['icon']); ?>" alt="" class="aksrt-product-page__service-icon">
<?php else : ?>
<i class="fas fa-check-circle"></i>
<?php endif; ?>
<div>
<strong><?php echo esc_html((string) ($service['name'] ?? '')); ?></strong>
<?php if (!empty($service['desc'])) : ?>
<p><?php echo esc_html((string) $service['desc']); ?></p>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div><!-- .aksrt-product-page -->
</div><!-- .content-layout__primary -->
<?php if (!empty($has_shop_sidebar)) : ?>
<aside class="content-layout__sidebar aksrt-shop-widget-zone">
<?php dynamic_sidebar('shop_product_sidebar'); ?>
</aside>
<?php endif; ?>
</div><!-- .content-layout -->
</div><!-- .content-wrap -->
<?php if (is_active_sidebar('shop_product_bottom_fluid')) : ?>
<div class="aksrt-fluid-widget aksrt-fluid-widget--shop"><?php dynamic_sidebar('shop_product_bottom_fluid'); ?></div>
<?php endif; ?>
<!-- Mobile purchase tabbar -->
<div class="aksrt-product-page__mobile-bar" data-aksrt-mobile-purchase>
<button type="button" class="aksrt-product-page__mobile-btn" data-aksrt-toggle-fav="<?php echo esc_attr((string) $product_id); ?>">
<i class="<?php echo $is_fav ? 'fas' : 'far'; ?> fa-heart"></i>
</button>
<button type="button" class="aksrt-product-page__mobile-btn" data-aksrt-add-cart>
<i class="fas fa-shopping-cart"></i>
</button>
<button type="button" class="aksrt-product-page__mobile-buy btn-primary" data-aksrt-buy-now>
<?php echo $pay_modo === 'points' ? esc_html__('积分兑换', 'aksrt') : esc_html__('立即购买', 'aksrt'); ?>
</button>
</div>
<?php else : ?>
<div class="aksrt-product-page container">
<div class="aksrt-product-page__empty">
<i class="fas fa-box-open" aria-hidden="true"></i>
<p><?php esc_html_e('商品不存在或已下架。', 'aksrt'); ?></p>
</div>
</div>
<?php endif; ?>
</main>
<?php get_footer(); ?>
-59
View File
@@ -1,59 +0,0 @@
<?php
/**
* Shared product card (archive / related / storefront).
*
* Expected args:
* - product: array from \AKSRT\Shop\Shop::build_product_card_view()
* - show_sales: bool (optional)
* - sales_cfg: array (optional; used when show_sales not provided)
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$args = is_array($args ?? null) ? $args : [];
$product = is_array($args['product'] ?? null) ? $args['product'] : [];
if (empty($product) || empty($product['id'])) {
return;
}
$sales = (int) ($product['sales'] ?? 0);
if (array_key_exists('show_sales', $args)) {
$show_sales = !empty($args['show_sales']);
} else {
$sales_cfg = is_array($args['sales_cfg'] ?? null) ? $args['sales_cfg'] : [];
$mode = (string) ($sales_cfg['show'] ?? 'show');
$min_sales = max(0, (int) ($sales_cfg['show_min'] ?? 0));
$show_sales = ($mode === 'show' && $sales > 0) || ($mode === 'threshold' && $sales >= $min_sales);
}
$title = (string) ($product['title'] ?? '');
$permalink = (string) ($product['permalink'] ?? '#');
$image = (string) ($product['image'] ?? '');
$excerpt = (string) ($product['excerpt'] ?? '');
$price = (float) ($product['price'] ?? 0);
?>
<article class="aksrt-product-card product-item posts-item card ajax-item">
<a href="<?php echo esc_url($permalink !== '' ? $permalink : '#'); ?>" class="aksrt-product-card__link">
<?php if ($image !== '') : ?>
<div class="aksrt-product-card__image">
<img src="<?php echo esc_url($image); ?>" alt="<?php echo esc_attr($title); ?>" loading="lazy">
</div>
<?php else : ?>
<div class="aksrt-product-card__image aksrt-product-card__image--placeholder"><i class="fas fa-image" aria-hidden="true"></i></div>
<?php endif; ?>
<div class="aksrt-product-card__body">
<h3 class="aksrt-product-card__title"><?php echo esc_html($title); ?></h3>
<?php if ($excerpt !== '') : ?>
<p class="aksrt-product-card__excerpt"><?php echo esc_html(wp_trim_words($excerpt, 15)); ?></p>
<?php endif; ?>
<div class="aksrt-product-card__footer">
<span class="aksrt-product-card__price">¥<?php echo esc_html(number_format($price, 2)); ?></span>
<?php if ($show_sales) : ?>
<span class="aksrt-product-card__sales"><?php printf(esc_html__('已售 %d', 'aksrt'), $sales); ?></span>
<?php endif; ?>
</div>
</div>
</a>
</article>
-149
View File
@@ -1,149 +0,0 @@
<?php
/**
* Product category hierarchical filter bar (zibll shop-cat-filter parity).
*
* Expects in scope:
* - $filter array from Shop::build_product_cat_filter()
* - $orderby string
* - $order string ASC|DESC
* - $term array|null
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
$filter = is_array($filter ?? null) ? $filter : [];
$orderby = sanitize_key((string) ($orderby ?? ($filter['orderby'] ?? 'date')));
$order = strtoupper(sanitize_key((string) ($order ?? ($filter['order'] ?? 'DESC'))));
if (!in_array($order, ['ASC', 'DESC'], true)) {
$order = 'DESC';
}
$current_id = (int) ($filter['current_id'] ?? 0);
$mode = (string) ($filter['mode'] ?? 'siblings');
$siblings = is_array($filter['siblings'] ?? null) ? $filter['siblings'] : [];
$children = is_array($filter['children'] ?? null) ? $filter['children'] : [];
$child_tabs = is_array($filter['child_tabs'] ?? null) ? $filter['child_tabs'] : [];
$parent_url = (string) ($filter['parent_url'] ?? '');
$parent_id = (int) ($filter['parent_id'] ?? 0);
$term_name = is_array($term ?? null) ? (string) ($term['name'] ?? '') : '';
$order_labels = [
'date' => __('最新', 'aksrt'),
'views' => __('最热', 'aksrt'),
'favorite_count' => __('收藏', 'aksrt'),
'score' => __('评分', 'aksrt'),
'sales' => __('销量', 'aksrt'),
];
$base_term_url = !empty($term['url']) ? (string) $term['url'] : '';
if ($base_term_url === '' && $current_id > 0) {
$link = get_term_link($current_id, 'aksrt_product_cat');
$base_term_url = is_wp_error($link) ? '' : (string) $link;
}
$blog = wp_strip_all_tags(get_bloginfo('name'));
?>
<div class="shop-cat-filter aksrt-shop-cat-filter zib-widget relative" data-aksrt-shop-cat-filter win-ajax-replace="shop-cat-filter">
<?php if (!empty($siblings)) : ?>
<div class="shop-cat-filter-tab list-inline scroll-x mini-scrollbar tab-nav-theme" data-aksrt-cat-tabs>
<?php foreach ($siblings as $sib) :
$sid = (int) ($sib['id'] ?? 0);
$is_active = ($mode === 'with_children' && $sid === $current_id)
|| ($mode === 'parent_siblings' && $sid === $parent_id)
|| ($mode === 'siblings' && $sid === $current_id);
?>
<?php if ($is_active) : ?>
<li class="active"><span class="focus-color"><?php echo esc_html((string) ($sib['name'] ?? '')); ?></span></li>
<?php else : ?>
<li>
<a class="ajax-next"
ajax-replace="true"
route="1"
route-title="<?php echo esc_attr((string) ($sib['name'] ?? '') . ' - ' . __('商城', 'aksrt') . ($blog ? ' · ' . $blog : '')); ?>"
href="<?php echo esc_url((string) ($sib['url'] ?? '#')); ?>"
data-aksrt-shop-term="<?php echo esc_attr((string) $sid); ?>">
<?php echo esc_html((string) ($sib['name'] ?? '')); ?>
</a>
</li>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($mode === 'with_children' && !empty($child_tabs)) : ?>
<div class="shop-cat-filter-child" data-aksrt-cat-children>
<span class="focus-color"><?php esc_html_e('全部', 'aksrt'); ?></span>
<?php foreach ($child_tabs as $child) : ?>
<a class="ajax-next"
ajax-replace="true"
route="1"
route-title="<?php echo esc_attr((string) ($child['name'] ?? '') . ' - ' . __('商城', 'aksrt')); ?>"
href="<?php echo esc_url((string) ($child['url'] ?? '#')); ?>"
data-aksrt-shop-term="<?php echo esc_attr((string) ((int) ($child['id'] ?? 0))); ?>">
<?php echo esc_html((string) ($child['name'] ?? '')); ?>
</a>
<?php endforeach; ?>
</div>
<?php elseif ($mode === 'parent_siblings' && !empty($children)) : ?>
<div class="shop-cat-filter-child" data-aksrt-cat-children>
<?php if ($parent_url !== '') : ?>
<a class="ajax-next" ajax-replace="true" route="1" href="<?php echo esc_url($parent_url); ?>" data-aksrt-shop-term="<?php echo esc_attr((string) $parent_id); ?>">
<?php esc_html_e('全部', 'aksrt'); ?>
</a>
<?php endif; ?>
<?php foreach ($children as $child) :
$cid = (int) ($child['id'] ?? 0);
?>
<?php if ($cid === $current_id) : ?>
<span class="focus-color"><?php echo esc_html((string) ($child['name'] ?? '')); ?></span>
<?php else : ?>
<a class="ajax-next"
ajax-replace="true"
route="1"
route-title="<?php echo esc_attr((string) ($child['name'] ?? '') . ' - ' . __('商城', 'aksrt')); ?>"
href="<?php echo esc_url((string) ($child['url'] ?? '#')); ?>"
data-aksrt-shop-term="<?php echo esc_attr((string) $cid); ?>">
<?php echo esc_html((string) ($child['name'] ?? '')); ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="shop-cat-filter-orderby shop-cat-filter-child" win-ajax-replace="orderby" data-aksrt-shop-orderby-bar>
<span class="opacity5"><?php esc_html_e('排序', 'aksrt'); ?></span>
<?php foreach ($order_labels as $key => $label) :
if ($orderby === $key) : ?>
<span class="focus-color"><?php echo esc_html($label); ?></span>
<?php else :
$href = $base_term_url !== '' ? add_query_arg(['orderby' => $key], $base_term_url) : add_query_arg(['orderby' => $key]);
?>
<a rel="nofollow" ajax-replace="true" class="ajax-next" href="<?php echo esc_url($href); ?>" data-aksrt-shop-orderby="<?php echo esc_attr($key); ?>">
<?php echo esc_html($label); ?>
</a>
<?php endif;
endforeach;
// Price toggle (single 售价 control).
if ($orderby === 'price') {
$next_order = $order === 'DESC' ? 'ASC' : 'DESC';
$icon = $next_order === 'DESC' ? 'up' : 'down';
$href = $base_term_url !== ''
? add_query_arg(['orderby' => 'price', 'order' => $next_order], $base_term_url)
: add_query_arg(['orderby' => 'price', 'order' => $next_order]);
?>
<a rel="nofollow" ajax-replace="true" class="ajax-next focus-color" href="<?php echo esc_url($href); ?>" data-aksrt-shop-orderby="price" data-aksrt-shop-order="<?php echo esc_attr($next_order); ?>">
<?php esc_html_e('售价', 'aksrt'); ?><i class="opacity5 ml3 fa fa-long-arrow-<?php echo esc_attr($icon); ?>"></i>
</a>
<?php } else {
$href = $base_term_url !== ''
? add_query_arg(['orderby' => 'price'], $base_term_url)
: add_query_arg(['orderby' => 'price']);
?>
<a rel="nofollow" ajax-replace="true" class="ajax-next" href="<?php echo esc_url($href); ?>" data-aksrt-shop-orderby="price">
<?php esc_html_e('售价', 'aksrt'); ?>
</a>
<?php } ?>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More