Merge pull request 'Worktree remove shop forum' (#357) from worktree-remove-shop-forum into main
Reviewed-on: CYL/wordpress-theme-aksrt#357
This commit is contained in:
@@ -94,8 +94,7 @@ class ApiClient {
|
||||
|
||||
async request(endpointOrConfig, options = {}) {
|
||||
// Dual-signature support:
|
||||
// request('/forum/overview')
|
||||
// request({ url: '/aksrt/v1/forum/topics', method: 'GET', params: { limit: 20 } })
|
||||
// request('/users/1/restrictions')
|
||||
// request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' })
|
||||
let endpoint, method, headers, params, body, rest
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { renderFaIcon, schemaPage } from './helpers'
|
||||
|
||||
// Forum SPA = config only. Ops moved to native CPT menus (Phase 4).
|
||||
export const forumSection = {
|
||||
key: 'forum',
|
||||
label: '社区&论坛',
|
||||
icon: renderFaIcon('fas fa-comments'),
|
||||
route: '/forum',
|
||||
children: [
|
||||
schemaPage({
|
||||
key: 'forum-settings-basic',
|
||||
name: 'ForumSettingsBasic',
|
||||
label: '基础设置',
|
||||
route: '/forum/settings',
|
||||
title: '论坛基础设置',
|
||||
desc: '论坛总开关、管理员和名称定义。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'basic',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-names',
|
||||
name: 'ForumSettingsNames',
|
||||
label: '名称定义',
|
||||
route: '/forum/settings/names',
|
||||
title: '名称定义',
|
||||
desc: '自定义论坛各实体的显示名称。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'names',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-homepage',
|
||||
name: 'ForumSettingsHomepage',
|
||||
label: '首页设置',
|
||||
route: '/forum/settings/homepage',
|
||||
title: '论坛首页设置',
|
||||
desc: '首页 Tab 引擎、快速发帖入口和移动端交互配置。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'homepage',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-board-page',
|
||||
name: 'ForumSettingsBoardPage',
|
||||
label: '版块页面',
|
||||
route: '/forum/settings/board-page',
|
||||
title: '版块页面设置',
|
||||
desc: '版块页布局、Tab 栏目和移动端交互配置。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'board_page',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-post-page',
|
||||
name: 'ForumSettingsPostPage',
|
||||
label: '帖子页面',
|
||||
route: '/forum/settings/post-page',
|
||||
title: '帖子页面设置',
|
||||
desc: '面包屑、评分、高度限制和封面配置。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'post_page',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-reply',
|
||||
name: 'ForumSettingsReply',
|
||||
label: '回复评论',
|
||||
route: '/forum/settings/reply',
|
||||
title: '回复评论设置',
|
||||
desc: '回复翻页方式、工具栏功能和快捷回复配置。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'reply',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-permissions',
|
||||
name: 'ForumSettingsPermissions',
|
||||
label: '权限配置',
|
||||
route: '/forum/settings/permissions',
|
||||
title: '论坛权限配置',
|
||||
desc: '发帖限制、帖子付费和版块付费关注。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'permissions',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-other',
|
||||
name: 'ForumSettingsOther',
|
||||
label: '其它设置',
|
||||
route: '/forum/settings/other',
|
||||
title: '论坛其它设置',
|
||||
desc: '投票、版主申请说明、标题字数和 URL 别名。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'other',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-upload',
|
||||
name: 'ForumSettingsUpload',
|
||||
label: '上传权限',
|
||||
route: '/forum/settings/upload',
|
||||
title: '上传权限设置',
|
||||
desc: '图片、视频和文件上传大小与格式限制。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'upload',
|
||||
}),
|
||||
schemaPage({
|
||||
key: 'forum-settings-credits',
|
||||
name: 'ForumSettingsCredits',
|
||||
label: '积分设置',
|
||||
route: '/forum/settings/credits',
|
||||
title: '论坛积分设置',
|
||||
desc: '论坛行为积分奖励矩阵和每日上限。',
|
||||
canonicalDomain: 'forum',
|
||||
section: 'credits',
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import { postListSection } from './post-list'
|
||||
import { permissionsSection } from './permissions'
|
||||
import { usersSection } from './users'
|
||||
import { paymentSection } from './payment'
|
||||
import { shopSection } from './shop'
|
||||
import { forumSection } from './forum'
|
||||
import { extensionsSection } from './extensions'
|
||||
import { themeInfoSection } from './theme-info'
|
||||
import { docsSection } from './docs'
|
||||
@@ -18,8 +16,6 @@ const SECTION_DEFAULT_CHILDREN = {
|
||||
permissions: 'permissions-comment',
|
||||
users: 'users-auth',
|
||||
payment: 'payment-paid',
|
||||
shop: 'shop-basic',
|
||||
forum: 'forum-settings-basic',
|
||||
extensions: 'extensions-ip',
|
||||
tools: 'tools-import-export',
|
||||
'theme-info': 'theme-info-dashboard',
|
||||
@@ -47,8 +43,6 @@ export const ADMIN_SECTIONS = [
|
||||
attachSectionDefaultRoute(permissionsSection),
|
||||
attachSectionDefaultRoute(usersSection),
|
||||
attachSectionDefaultRoute(paymentSection),
|
||||
attachSectionDefaultRoute(shopSection),
|
||||
attachSectionDefaultRoute(forumSection),
|
||||
attachSectionDefaultRoute(extensionsSection),
|
||||
attachSectionDefaultRoute(toolsSection),
|
||||
attachSectionDefaultRoute(themeInfoSection),
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -372,7 +372,6 @@ const typeOptions = [
|
||||
{ label: '主题切换', value: 'theme_mode' },
|
||||
{ label: '页面二维码', value: 'qrcode' },
|
||||
{ label: '发布入口', value: 'add' },
|
||||
{ label: '购物车', value: 'shop_cart' },
|
||||
{ label: 'QQ 客服', value: 'service_qq' },
|
||||
{ label: '微信客服', value: 'service_wechat' },
|
||||
{ label: '会员入口', value: 'pay_vip' },
|
||||
@@ -397,7 +396,7 @@ const addPresetOptions = computed(() => {
|
||||
})
|
||||
|
||||
// pay_vip always opens the site VIP modal on the front-end — no link required.
|
||||
const needsLink = computed(() => ['custom', 'add', 'shop_cart'].includes(editingItem.type) && !editingItem.hover)
|
||||
const needsLink = computed(() => ['custom', 'add'].includes(editingItem.type) && !editingItem.hover)
|
||||
// Custom hover HTML only; VIP uses modal, not free-form hover content.
|
||||
const supportsHover = computed(() => editingItem.type === 'custom')
|
||||
const supportsActionMode = computed(() => ['service_qq', 'service_wechat'].includes(editingItem.type))
|
||||
@@ -444,7 +443,6 @@ function applyTypeDefaults(type) {
|
||||
service_wechat: { icon: 'fab fa-weixin', icon_dark: '', icon_light: '', text: '微信', desc: '扫码添加微信', link: '', hover: '', action_mode: 'link', qr_size: 120 },
|
||||
pay_vip: { icon: 'fas fa-gem', icon_dark: '', icon_light: '', text: '会员', desc: '开通/升级会员弹窗', link: '', hover: '', action_mode: 'link', qr_size: 120 },
|
||||
add: { icon: 'fas fa-plus', icon_dark: '', icon_light: '', text: '发布', desc: '发布内容', link: '', hover: '', action_mode: 'link', qr_size: 120 },
|
||||
shop_cart: { icon: 'fas fa-shopping-cart', icon_dark: '', icon_light: '', text: '购物车', desc: '购物车入口', link: '', hover: '', action_mode: 'link', qr_size: 120 },
|
||||
}
|
||||
return map[type] || map.custom
|
||||
}
|
||||
|
||||
@@ -186,8 +186,8 @@
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="['link', 'msg', 'pay_vip', 'shop_cart'].includes(editingItem.type)" label="链接地址">
|
||||
<n-input v-model:value="editingItem.link" :placeholder="editingItem.type === 'msg' ? '留空使用默认消息中心地址' : editingItem.type === 'pay_vip' ? '留空使用默认会员页地址' : editingItem.type === 'shop_cart' ? '留空使用默认购物车地址' : 'https:// 或 /path'" />
|
||||
<n-form-item v-if="['link', 'msg', 'pay_vip'].includes(editingItem.type)" label="链接地址">
|
||||
<n-input v-model:value="editingItem.link" :placeholder="editingItem.type === 'msg' ? '留空使用默认消息中心地址' : editingItem.type === 'pay_vip' ? '留空使用默认会员页地址' : 'https:// 或 /path'" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="editingItem.type === 'link'" label="新窗口打开">
|
||||
@@ -312,7 +312,6 @@ const typeOptions = [
|
||||
{ label: '搜索', value: 'search' },
|
||||
{ label: '消息中心', value: 'msg' },
|
||||
{ label: '开通会员', value: 'pay_vip' },
|
||||
{ label: '购物车', value: 'shop_cart' },
|
||||
]
|
||||
|
||||
const typeLabelMap = {
|
||||
@@ -325,7 +324,6 @@ const typeLabelMap = {
|
||||
search: '搜索',
|
||||
msg: '消息中心',
|
||||
pay_vip: '开通会员',
|
||||
shop_cart: '购物车',
|
||||
}
|
||||
|
||||
const typeDefaults = {
|
||||
@@ -338,7 +336,6 @@ const typeDefaults = {
|
||||
search: { icon: 'fas fa-search', text: '搜索' },
|
||||
msg: { icon: 'fas fa-bell', text: '消息' },
|
||||
pay_vip: { icon: 'fas fa-crown', text: '会员' },
|
||||
shop_cart: { icon: 'fas fa-shopping-cart', text: '购物车' },
|
||||
}
|
||||
|
||||
const enabledPreviewItems = computed(() => form.items.filter((item) => item.enabled))
|
||||
@@ -360,7 +357,6 @@ function getItemSummary(item) {
|
||||
if (item.type === 'auth') return '登录用户显示我的,游客显示登录'
|
||||
if (item.type === 'msg') return '跳转到消息中心页面'
|
||||
if (item.type === 'pay_vip') return item.link || '跳转到会员开通页'
|
||||
if (item.type === 'shop_cart') return item.link || '跳转到购物车页面'
|
||||
return item.link || '自定义链接按钮'
|
||||
}
|
||||
|
||||
@@ -405,7 +401,7 @@ function applyTypeDefaults(type, force = false) {
|
||||
const preset = typeDefaults[type] || typeDefaults.link
|
||||
if (force || !editingItem.icon) editingItem.icon = preset.icon
|
||||
if (force || !editingItem.text) editingItem.text = preset.text
|
||||
if (!['link', 'msg', 'pay_vip', 'shop_cart'].includes(type)) editingItem.link = ''
|
||||
if (!['link', 'msg', 'pay_vip'].includes(type)) editingItem.link = ''
|
||||
if (type !== 'category') editingItem.category_id = null
|
||||
if (type !== 'search') editingItem.search_default_type = 'post'
|
||||
if (type !== 'add') editingItem.add_items = []
|
||||
@@ -483,7 +479,7 @@ function saveItem() {
|
||||
type: editingItem.type,
|
||||
icon: editingItem.icon,
|
||||
text: editingItem.text,
|
||||
link: ['link', 'msg', 'pay_vip', 'shop_cart'].includes(editingItem.type) ? editingItem.link : '',
|
||||
link: ['link', 'msg', 'pay_vip'].includes(editingItem.type) ? editingItem.link : '',
|
||||
badge: (editingItem.badge || '').trim(),
|
||||
badge_mode: editingItem.badge_mode,
|
||||
category_id: editingItem.type === 'category' ? (editingItem.category_id || 0) : 0,
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="版块管理" desc="创建、编辑和维护论坛版块信息,并观察版主申请积压情况。" />
|
||||
|
||||
<n-card size="small" class="aksrt-forum-boards__section">
|
||||
<n-space justify="space-between" align="center">
|
||||
<div class="aksrt-forum-boards__summary">共 {{ boards.length }} 个版块</div>
|
||||
<n-button type="primary" @click="openCreate">新增版块</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card size="small" class="aksrt-forum-boards__section">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="boards"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
:row-key="(row) => row.id"
|
||||
/>
|
||||
</n-card>
|
||||
|
||||
<n-modal :show="modalVisible" preset="card" style="width: 520px" :title="editingId ? '编辑版块' : '新增版块'" @update:show="modalVisible = $event">
|
||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
|
||||
<n-form-item label="版块名称" path="name">
|
||||
<n-input v-model:value="form.name" placeholder="输入版块名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="版块描述" path="description">
|
||||
<n-input v-model:value="form.description" type="textarea" :rows="3" placeholder="描述主题方向和讨论边界" />
|
||||
</n-form-item>
|
||||
<n-form-item label="图标" path="icon">
|
||||
<n-input v-model:value="form.icon" placeholder="例如:fas fa-comments" />
|
||||
</n-form-item>
|
||||
<n-form-item label="排序" path="sort_order">
|
||||
<n-input-number v-model:value="form.sort_order" :min="0" style="width: 100%" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="modalVisible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="handleSave">保存</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const boards = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const editingId = ref(null)
|
||||
const formRef = ref(null)
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
icon: 'fas fa-comments',
|
||||
sort_order: 0,
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: { required: true, message: '请输入版块名称', trigger: 'blur' },
|
||||
}
|
||||
|
||||
const columns = computed(() => [
|
||||
{ title: '排序', key: 'sort_order', width: 70 },
|
||||
{ title: '版块名称', key: 'name', minWidth: 180 },
|
||||
{ title: '描述', key: 'description', ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '治理状态',
|
||||
key: 'governance',
|
||||
width: 180,
|
||||
render: (row) => h(NSpace, { size: 'small' }, () => [
|
||||
h(NTag, { size: 'small', type: 'info' }, () => `${row.post_count || 0} 主题`),
|
||||
h(NTag, { size: 'small', type: 'success' }, () => `${row.moderator_count || 0} 版主`),
|
||||
h(NTag, { size: 'small', type: (row.pending_application_count || 0) > 0 ? 'warning' : 'default' }, () => `${row.pending_application_count || 0} 申请`),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (row) => h(NSpace, { size: 'small' }, () => [
|
||||
h(NButton, { size: 'tiny', quaternary: true, type: 'info', onClick: () => openEdit(row) }, () => '编辑'),
|
||||
h(NButton, { size: 'tiny', quaternary: true, type: 'error', onClick: () => handleDelete(row) }, () => '删除'),
|
||||
]),
|
||||
},
|
||||
])
|
||||
|
||||
function resetForm() {
|
||||
form.name = ''
|
||||
form.description = ''
|
||||
form.icon = 'fas fa-comments'
|
||||
form.sort_order = boards.value.length
|
||||
}
|
||||
|
||||
async function loadBoards() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request('/forum/boards')
|
||||
boards.value = res?.data?.items || []
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载版块列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = null
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editingId.value = row.id
|
||||
form.name = row.name || ''
|
||||
form.description = row.description || ''
|
||||
form.icon = row.icon || 'fas fa-comments'
|
||||
form.sort_order = row.sort_order || 0
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await api.request(`/forum/boards/${editingId.value}`, {
|
||||
method: 'POST',
|
||||
data: { ...form },
|
||||
})
|
||||
message.success('版块已更新')
|
||||
} else {
|
||||
await api.request('/forum/boards', {
|
||||
method: 'POST',
|
||||
data: { ...form },
|
||||
})
|
||||
message.success('版块已创建')
|
||||
}
|
||||
modalVisible.value = false
|
||||
await loadBoards()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '版块保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
dialog.warning({
|
||||
title: '确认删除版块',
|
||||
content: `确定删除“${row.name}”吗?如果仍有主题,请先完成迁移。`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await api.request(`/forum/boards/${row.id}`, { method: 'DELETE' })
|
||||
message.success('版块已删除')
|
||||
await loadBoards()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '版块删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(loadBoards)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-forum-boards__section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-boards__summary {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,223 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="论坛概览" desc="从待审核主题、版主申请和版块状态三个视角观察论坛治理负载。" />
|
||||
|
||||
<div class="aksrt-forum-dashboard__stats">
|
||||
<div v-for="card in statCards" :key="card.key" class="aksrt-forum-dashboard__stat-card">
|
||||
<div class="aksrt-forum-dashboard__stat-icon" :style="{ background: card.color }">
|
||||
<i :class="card.icon"></i>
|
||||
</div>
|
||||
<div class="aksrt-forum-dashboard__stat-info">
|
||||
<div class="aksrt-forum-dashboard__stat-value">{{ stats[card.key] || 0 }}</div>
|
||||
<div class="aksrt-forum-dashboard__stat-label">{{ card.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aksrt-forum-dashboard__grid">
|
||||
<n-card size="small" class="aksrt-forum-dashboard__section">
|
||||
<template #header>
|
||||
<div class="aksrt-forum-dashboard__section-head">
|
||||
<strong>待审核主题</strong>
|
||||
<n-button size="small" quaternary type="primary" @click="goTopics">进入主题治理</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-data-table
|
||||
v-if="pendingTopics.length"
|
||||
:columns="topicColumns"
|
||||
:data="pendingTopics"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
/>
|
||||
<n-empty v-else description="暂无待审核主题" />
|
||||
</n-card>
|
||||
|
||||
<n-card size="small" class="aksrt-forum-dashboard__section">
|
||||
<template #header>
|
||||
<div class="aksrt-forum-dashboard__section-head">
|
||||
<strong>待处理版主申请</strong>
|
||||
<n-button size="small" quaternary type="primary" @click="goApplications">进入申请处理</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-data-table
|
||||
v-if="pendingApplications.length"
|
||||
:columns="applicationColumns"
|
||||
:data="pendingApplications"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
/>
|
||||
<n-empty v-else description="暂无待处理申请" />
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<n-card size="small" class="aksrt-forum-dashboard__section">
|
||||
<template #header>
|
||||
<div class="aksrt-forum-dashboard__section-head">
|
||||
<strong>版块概况</strong>
|
||||
<n-button size="small" quaternary type="primary" @click="goBoards">进入版块管理</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-data-table
|
||||
v-if="boards.length"
|
||||
:columns="boardColumns"
|
||||
:data="boards"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
/>
|
||||
<n-empty v-else description="暂无版块" />
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import { NButton, NTag, useMessage } from 'naive-ui'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const message = useMessage()
|
||||
const stats = ref({})
|
||||
const boards = ref([])
|
||||
const pendingTopics = ref([])
|
||||
const pendingApplications = ref([])
|
||||
|
||||
const statCards = [
|
||||
{ key: 'board_count', label: '版块数', icon: 'fas fa-th-large', color: '#dbeafe' },
|
||||
{ key: 'topic_count', label: '公开主题', icon: 'fas fa-list-alt', color: '#dcfce7' },
|
||||
{ key: 'pending_topic_count', label: '待审核主题', icon: 'fas fa-hourglass-half', color: '#fef3c7' },
|
||||
{ key: 'pending_application_count', label: '待处理申请', icon: 'fas fa-user-shield', color: '#fee2e2' },
|
||||
]
|
||||
|
||||
const topicColumns = [
|
||||
{ title: '标题', key: 'title', minWidth: 260 },
|
||||
{ title: '作者', key: 'author_name', width: 110, render: (row) => row.author?.display_name || '-' },
|
||||
{ title: '版块', key: 'board_name', width: 120, render: (row) => row.boards?.[0]?.name || '-' },
|
||||
{ title: '时间', key: 'date', width: 160 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
render: (row) => h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => goTopics() }, () => '治理'),
|
||||
},
|
||||
]
|
||||
|
||||
const applicationColumns = [
|
||||
{ title: '版块', key: 'board_name', width: 140 },
|
||||
{ title: '申请人', key: 'display_name', width: 140 },
|
||||
{ title: '理由', key: 'reason', ellipsis: { tooltip: true } },
|
||||
{ title: '时间', key: 'created_at', width: 160 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (row) => h(NTag, { size: 'small', type: 'warning' }, () => row.status_label || row.status || '-'),
|
||||
},
|
||||
]
|
||||
|
||||
const boardColumns = [
|
||||
{ title: '版块名称', key: 'name', minWidth: 180 },
|
||||
{ title: '主题数', key: 'post_count', width: 90 },
|
||||
{ title: '版主数', key: 'moderator_count', width: 90 },
|
||||
{ title: '待处理申请', key: 'pending_application_count', width: 120 },
|
||||
]
|
||||
|
||||
async function loadOverview() {
|
||||
try {
|
||||
const res = await api.request('/forum/overview')
|
||||
const data = res?.data || {}
|
||||
stats.value = data.stats || {}
|
||||
boards.value = data.boards || []
|
||||
pendingTopics.value = data.pending_topics || []
|
||||
pendingApplications.value = data.pending_applications || []
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载论坛概览失败')
|
||||
}
|
||||
}
|
||||
|
||||
function goBoards() {
|
||||
window.location.hash = '#/forum/boards'
|
||||
}
|
||||
|
||||
function goTopics() {
|
||||
window.location.hash = '#/forum/topics'
|
||||
}
|
||||
|
||||
function goApplications() {
|
||||
window.location.hash = '#/forum/moderator-applications'
|
||||
}
|
||||
|
||||
onMounted(loadOverview)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-forum-dashboard__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
color: #1d4ed8;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__stat-value {
|
||||
color: #111827;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__stat-label {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-dashboard__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.aksrt-forum-dashboard__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="版主申请处理" desc="处理前台版主申请,完成审核、通过和拒绝反馈闭环。" />
|
||||
|
||||
<n-card size="small" class="aksrt-forum-apps__section">
|
||||
<n-space wrap>
|
||||
<n-input v-model:value="filters.keyword" clearable placeholder="搜索版块 / 用户 / 理由" style="width: 220px" @keyup.enter="loadData" />
|
||||
<n-select v-model:value="filters.board_id" :options="boardOptions" clearable placeholder="版块" style="width: 180px" />
|
||||
<n-select v-model:value="filters.status" :options="statusOptions" clearable placeholder="状态" style="width: 160px" />
|
||||
<n-button type="primary" @click="loadData">查询</n-button>
|
||||
<n-button @click="resetFilters">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card size="small" class="aksrt-forum-apps__section">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="items"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
/>
|
||||
</n-card>
|
||||
|
||||
<n-modal :show="reviewVisible" preset="card" title="处理版主申请" style="width: 520px" @update:show="reviewVisible = $event">
|
||||
<n-space vertical :size="14">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
<div>{{ activeApplication?.display_name || '-' }} / {{ activeApplication?.board_name || '-' }}</div>
|
||||
<div class="aksrt-forum-apps__reason">{{ activeApplication?.reason || '暂无申请理由' }}</div>
|
||||
</n-alert>
|
||||
<n-radio-group v-model:value="reviewForm.status">
|
||||
<n-radio-button value="approved">通过</n-radio-button>
|
||||
<n-radio-button value="rejected">拒绝</n-radio-button>
|
||||
</n-radio-group>
|
||||
<n-input v-model:value="reviewForm.note" type="textarea" :rows="4" placeholder="记录审核结论和补充说明" />
|
||||
</n-space>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="reviewVisible = false">取消</n-button>
|
||||
<n-button type="primary" :loading="reviewing" @click="submitReview">提交处理</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { 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'
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const items = ref([])
|
||||
const boardOptions = ref([{ label: '全部版块', value: null }])
|
||||
const statusOptions = ref([{ label: '全部状态', value: null }])
|
||||
const reviewVisible = ref(false)
|
||||
const reviewing = ref(false)
|
||||
const activeApplication = ref(null)
|
||||
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
board_id: null,
|
||||
status: null,
|
||||
})
|
||||
|
||||
const reviewForm = reactive({
|
||||
status: 'approved',
|
||||
note: '',
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '版块 / 用户',
|
||||
key: 'board_name',
|
||||
minWidth: 220,
|
||||
render: (row) => h('div', { class: 'aksrt-forum-apps__identity' }, [
|
||||
h('div', { class: 'aksrt-forum-apps__board' }, row.board_name || '-'),
|
||||
h('div', { class: 'aksrt-forum-apps__user' }, `${row.display_name || '-'} · ${row.user_email || '-'}`),
|
||||
]),
|
||||
},
|
||||
{ title: '申请理由', key: 'reason', ellipsis: { tooltip: true } },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 110,
|
||||
render: (row) => h(NTag, { size: 'small', type: resolveStatusType(row.status) }, () => row.status_label || row.status || '-'),
|
||||
},
|
||||
{ title: '申请时间', key: 'created_at', width: 160 },
|
||||
{ title: '处理时间', key: 'reviewed_at', width: 160, render: (row) => row.reviewed_at || '-' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
render: (row) => {
|
||||
if (row.status !== 'pending') {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return h('div', { style: 'display:flex;gap:6px;' }, [
|
||||
h(NButton, { size: 'small', quaternary: true, type: 'success', onClick: () => openReview(row, 'approved') }, () => '通过'),
|
||||
h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => openReview(row, 'rejected') }, () => '拒绝'),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function resolveStatusType(status) {
|
||||
if (status === 'approved') return 'success'
|
||||
if (status === 'rejected') return 'error'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request('/forum/moderator/applications', {
|
||||
params: {
|
||||
keyword: filters.keyword || '',
|
||||
board_id: filters.board_id || '',
|
||||
status: filters.status || '',
|
||||
},
|
||||
})
|
||||
|
||||
const data = res?.data || {}
|
||||
items.value = data.items || []
|
||||
boardOptions.value = [{ label: '全部版块', value: null }, ...((data.options?.boards || []).map((item) => ({ label: item.name, value: item.id })))]
|
||||
statusOptions.value = data.options?.status || [{ label: '全部状态', value: null }]
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载版主申请失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.board_id = null
|
||||
filters.status = null
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openReview(row, status) {
|
||||
activeApplication.value = row
|
||||
reviewForm.status = status
|
||||
reviewForm.note = row.review_note || ''
|
||||
reviewVisible.value = true
|
||||
}
|
||||
|
||||
async function submitReview() {
|
||||
if (!activeApplication.value?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
reviewing.value = true
|
||||
try {
|
||||
await api.request(`/forum/moderator/applications/${activeApplication.value.id}/review`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
status: reviewForm.status,
|
||||
note: reviewForm.note,
|
||||
},
|
||||
})
|
||||
message.success('申请状态已更新')
|
||||
reviewVisible.value = false
|
||||
activeApplication.value = null
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '申请处理失败')
|
||||
} finally {
|
||||
reviewing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-forum-apps__section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-apps__identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-apps__board {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-apps__user,
|
||||
.aksrt-forum-apps__reason {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,363 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="版主管理" desc="按版块维护版主名单,支持角色分层和权限档位。" />
|
||||
|
||||
<n-grid cols="1 s:2 m:4" :x-gap="16" :y-gap="16" responsive="screen" style="margin-bottom:16px;">
|
||||
<n-grid-item>
|
||||
<n-statistic label="总版主数" :value="stats.total_moderators" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-statistic label="总版块数" :value="stats.total_boards" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-statistic label="待处理申请" :value="stats.pending_applications">
|
||||
<template #suffix>
|
||||
<n-tag v-if="stats.pending_applications > 0" type="warning" size="small">待处理</n-tag>
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-statistic label="高级版主" :value="stats.senior_moderators" />
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<n-card size="small" class="aksrt-forum-mod__toolbar">
|
||||
<n-space justify="space-between" align="center">
|
||||
<span class="aksrt-forum-mod__summary">已加载 {{ boards.length }} 个版块</span>
|
||||
<n-space>
|
||||
<n-button @click="goApplications">查看版主申请</n-button>
|
||||
<n-button type="primary" @click="showBatchModal = true">批量管理</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card v-for="board in boards" :key="board.board_id" size="small" class="aksrt-forum-mod__section">
|
||||
<template #header>
|
||||
<div class="aksrt-forum-mod__header">
|
||||
<strong>{{ board.board_name }}</strong>
|
||||
<n-space size="small">
|
||||
<n-tag size="small" type="info">{{ board.topic_count }} 主题</n-tag>
|
||||
<n-tag size="small" :type="board.pending_application_count > 0 ? 'warning' : 'default'">
|
||||
{{ board.pending_application_count }} 申请待处理
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 版主列表 -->
|
||||
<div class="aksrt-forum-mod__list">
|
||||
<div v-for="moderator in board.moderators" :key="moderator.id" class="aksrt-forum-mod__item">
|
||||
<n-avatar :src="moderator.avatar_url" :size="36" round />
|
||||
<div class="aksrt-forum-mod__info">
|
||||
<div class="aksrt-forum-mod__name-row">
|
||||
<span class="aksrt-forum-mod__name">{{ moderator.display_name }}</span>
|
||||
<n-tag :type="getRoleTagType(moderator.role)" size="tiny">{{ getRoleLabel(moderator.role) }}</n-tag>
|
||||
</div>
|
||||
<span class="aksrt-forum-mod__email">{{ moderator.user_email }}</span>
|
||||
<div v-if="moderator.permissions?.length" class="aksrt-forum-mod__permissions">
|
||||
<n-tag v-for="perm in moderator.permissions" :key="perm" size="tiny" type="default">{{ getPermissionLabel(perm) }}</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
<n-space size="small">
|
||||
<n-select
|
||||
v-model:value="moderator.role"
|
||||
:options="roleOptions"
|
||||
size="tiny"
|
||||
style="width:100px;"
|
||||
@update:value="handleRoleChange(board.board_id, moderator.id, $event)"
|
||||
/>
|
||||
<n-button size="tiny" quaternary type="error" @click="handleRemove(board.board_id, moderator.id)">移除</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-empty v-if="board.moderators.length === 0" description="暂无版主" size="small" />
|
||||
</div>
|
||||
|
||||
<!-- 添加版主 -->
|
||||
<div class="aksrt-forum-mod__add">
|
||||
<n-input
|
||||
v-model:value="board._newUser"
|
||||
size="small"
|
||||
style="width:140px"
|
||||
placeholder="用户 ID"
|
||||
@keyup.enter="handleAdd(board)"
|
||||
/>
|
||||
<n-select
|
||||
v-model:value="board._newRole"
|
||||
:options="roleOptions"
|
||||
size="small"
|
||||
style="width:100px;"
|
||||
placeholder="角色"
|
||||
/>
|
||||
<n-button size="small" type="primary" :disabled="!board._newUser" @click="handleAdd(board)">添加版主</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<n-empty v-if="boards.length === 0 && !loading" description="暂无论坛版块" />
|
||||
|
||||
<!-- 批量管理弹窗 -->
|
||||
<n-modal v-model:show="showBatchModal" preset="card" title="批量管理版主" style="max-width:560px;" :bordered="false">
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="选择版块">
|
||||
<n-select v-model:value="batchForm.board_id" :options="boardOptions" placeholder="选择版块" />
|
||||
</n-form-item>
|
||||
<n-form-item label="操作">
|
||||
<n-radio-group v-model:value="batchForm.action">
|
||||
<n-space>
|
||||
<n-radio value="add">批量添加</n-radio>
|
||||
<n-radio value="remove">批量移除</n-radio>
|
||||
<n-radio value="update_role">批量修改角色</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="用户 ID 列表">
|
||||
<n-input v-model:value="batchForm.user_ids" type="textarea" :rows="3" placeholder="每行一个用户 ID" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="batchForm.action === 'update_role'" label="目标角色">
|
||||
<n-select v-model:value="batchForm.target_role" :options="roleOptions" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showBatchModal = false">取消</n-button>
|
||||
<n-button type="primary" :loading="batchLoading" @click="handleBatch">执行</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const boards = ref([])
|
||||
const stats = reactive({ total_moderators: 0, total_boards: 0, pending_applications: 0, senior_moderators: 0 })
|
||||
|
||||
// 批量管理
|
||||
const showBatchModal = ref(false)
|
||||
const batchLoading = ref(false)
|
||||
const batchForm = reactive({ board_id: null, action: 'add', user_ids: '', target_role: 'moderator' })
|
||||
|
||||
// 角色选项
|
||||
const roleOptions = [
|
||||
{ label: '超级版主', value: 'super_moderator' },
|
||||
{ label: '高级版主', value: 'senior_moderator' },
|
||||
{ label: '版主', value: 'moderator' },
|
||||
{ label: '实习版主', value: 'trainee' },
|
||||
]
|
||||
|
||||
// 权限选项
|
||||
const permissionLabels = {
|
||||
manage_topics: '管理主题',
|
||||
manage_comments: '管理评论',
|
||||
manage_users: '管理用户',
|
||||
pin_topics: '置顶主题',
|
||||
close_topics: '关闭主题',
|
||||
move_topics: '移动主题',
|
||||
delete_topics: '删除主题',
|
||||
ban_users: '封禁用户',
|
||||
}
|
||||
|
||||
const boardOptions = computed(() => boards.value.map(b => ({ label: b.board_name, value: b.board_id })))
|
||||
|
||||
function getRoleLabel(role) {
|
||||
const opt = roleOptions.find(r => r.value === role)
|
||||
return opt?.label || role || '版主'
|
||||
}
|
||||
|
||||
function getRoleTagType(role) {
|
||||
const map = { super_moderator: 'error', senior_moderator: 'warning', moderator: 'info', trainee: 'default' }
|
||||
return map[role] || 'default'
|
||||
}
|
||||
|
||||
function getPermissionLabel(perm) {
|
||||
return permissionLabels[perm] || perm
|
||||
}
|
||||
|
||||
async function loadModerators() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request('/forum/moderators')
|
||||
boards.value = (res?.data?.boards || []).map(item => ({
|
||||
...item,
|
||||
_newUser: '',
|
||||
_newRole: 'moderator',
|
||||
}))
|
||||
// 计算统计
|
||||
stats.total_boards = boards.value.length
|
||||
stats.total_moderators = boards.value.reduce((sum, b) => sum + (b.moderators?.length || 0), 0)
|
||||
stats.pending_applications = boards.value.reduce((sum, b) => sum + (b.pending_application_count || 0), 0)
|
||||
stats.senior_moderators = boards.value.reduce((sum, b) =>
|
||||
sum + (b.moderators?.filter(m => ['super_moderator', 'senior_moderator'].includes(m.role))?.length || 0), 0)
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载版主列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdd(board) {
|
||||
const userId = parseInt(board._newUser || '', 10)
|
||||
if (!userId || userId <= 0) {
|
||||
message.warning('请输入有效的用户 ID')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.request('/forum/moderators', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
board_id: board.board_id,
|
||||
user_id: userId,
|
||||
role: board._newRole || 'moderator',
|
||||
action: 'add',
|
||||
},
|
||||
})
|
||||
message.success('版主已添加')
|
||||
board._newUser = ''
|
||||
board._newRole = 'moderator'
|
||||
await loadModerators()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '添加版主失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove(boardId, userId) {
|
||||
try {
|
||||
await api.request('/forum/moderators', {
|
||||
method: 'POST',
|
||||
data: { board_id: boardId, user_id: userId, action: 'remove' },
|
||||
})
|
||||
message.success('版主已移除')
|
||||
await loadModerators()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '移除版主失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRoleChange(boardId, userId, newRole) {
|
||||
try {
|
||||
await api.request('/forum/moderators', {
|
||||
method: 'POST',
|
||||
data: { board_id: boardId, user_id: userId, role: newRole, action: 'update_role' },
|
||||
})
|
||||
message.success('角色已更新')
|
||||
await loadModerators()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '角色更新失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatch() {
|
||||
const userIds = batchForm.user_ids.split('\n').map(s => parseInt(s.trim(), 10)).filter(n => n > 0)
|
||||
if (!batchForm.board_id || userIds.length === 0) {
|
||||
message.warning('请选择版块并输入用户 ID')
|
||||
return
|
||||
}
|
||||
|
||||
batchLoading.value = true
|
||||
try {
|
||||
await api.request('/forum/moderators/batch', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
board_id: batchForm.board_id,
|
||||
user_ids: userIds,
|
||||
action: batchForm.action,
|
||||
target_role: batchForm.target_role,
|
||||
},
|
||||
})
|
||||
message.success('批量操作完成')
|
||||
showBatchModal.value = false
|
||||
batchForm.user_ids = ''
|
||||
await loadModerators()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '批量操作失败')
|
||||
} finally {
|
||||
batchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goApplications() {
|
||||
window.location.hash = '#/forum/moderator-applications'
|
||||
}
|
||||
|
||||
onMounted(loadModerators)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-forum-mod__toolbar,
|
||||
.aksrt-forum-mod__section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__summary {
|
||||
color: var(--color-ink-3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--color-line-soft);
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__name {
|
||||
display: block;
|
||||
color: var(--color-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__email {
|
||||
display: block;
|
||||
color: var(--color-ink-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__permissions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-mod__add {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-line-soft);
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="主题治理" desc="集中处理论坛主题的审核、可见性、版块移动和治理状态。" />
|
||||
|
||||
<n-card size="small" class="aksrt-forum-topics__section">
|
||||
<n-space wrap>
|
||||
<n-input v-model:value="filters.keyword" clearable placeholder="搜索标题 / 作者 / 版块" style="width: 220px" @keyup.enter="loadData(1)" />
|
||||
<n-select v-model:value="filters.board_id" :options="boardOptions" clearable placeholder="版块" style="width: 180px" />
|
||||
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="帖子状态" style="width: 160px" />
|
||||
<n-select v-model:value="filters.review_status" :options="reviewOptions" clearable placeholder="审核状态" style="width: 160px" />
|
||||
<n-select v-model:value="filters.visibility" :options="visibilityOptions" clearable placeholder="可见性" style="width: 160px" />
|
||||
<n-button type="primary" @click="loadData(1)">查询</n-button>
|
||||
<n-button @click="resetFilters">重置</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card size="small" class="aksrt-forum-topics__section">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="items"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
:pagination="pagination"
|
||||
@update:page="handlePageChange"
|
||||
/>
|
||||
</n-card>
|
||||
|
||||
<TopicModerationDrawer
|
||||
v-model:show="detailVisible"
|
||||
:topic-id="detailTopicId"
|
||||
@updated="loadData(page)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { 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 TopicModerationDrawer from './components/TopicModerationDrawer.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const items = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const detailTopicId = ref(null)
|
||||
|
||||
const boardOptions = ref([])
|
||||
const statusOptions = ref([{ label: '全部', value: 'all' }])
|
||||
const reviewOptions = ref([])
|
||||
const visibilityOptions = ref([])
|
||||
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
board_id: null,
|
||||
status: 'all',
|
||||
review_status: null,
|
||||
visibility: null,
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '主题',
|
||||
key: 'title',
|
||||
minWidth: 280,
|
||||
render: (row) => h('div', { class: 'aksrt-forum-topics__title-cell' }, [
|
||||
h('div', { class: 'aksrt-forum-topics__title' }, row.title || '-'),
|
||||
h('div', { class: 'aksrt-forum-topics__subtitle' }, [
|
||||
row.boards?.[0]?.name || '未分配版块',
|
||||
' · ',
|
||||
row.author?.display_name || '未知作者',
|
||||
]),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '可见性',
|
||||
key: 'visibility',
|
||||
width: 110,
|
||||
render: (row) => h(NTag, { size: 'small', type: row.visibility?.key === 'public' ? 'default' : 'warning' }, () => row.visibility?.label || '-'),
|
||||
},
|
||||
{
|
||||
title: '审核',
|
||||
key: 'review_status',
|
||||
width: 110,
|
||||
render: (row) => h(NTag, { size: 'small', type: resolveReviewType(row.review_status?.key) }, () => row.review_status?.label || '-'),
|
||||
},
|
||||
{ title: '回复', key: 'comment_count', width: 80 },
|
||||
{ title: '浏览', key: 'views', width: 80 },
|
||||
{ title: '发布时间', key: 'date', width: 160 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 110,
|
||||
render: (row) => h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => openDetail(row) }, () => '治理'),
|
||||
},
|
||||
]
|
||||
|
||||
function resolveReviewType(status) {
|
||||
if (status === 'approved') return 'success'
|
||||
if (status === 'rejected') return 'error'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
async function loadData(nextPage = 1) {
|
||||
loading.value = true
|
||||
page.value = nextPage
|
||||
pagination.page = nextPage
|
||||
|
||||
try {
|
||||
const res = await api.request('/forum/topics', {
|
||||
params: {
|
||||
page: nextPage,
|
||||
per_page: pagination.pageSize,
|
||||
keyword: filters.keyword || '',
|
||||
board_id: filters.board_id || '',
|
||||
status: filters.status || 'all',
|
||||
review_status: filters.review_status || '',
|
||||
visibility: filters.visibility || '',
|
||||
},
|
||||
})
|
||||
|
||||
const data = res?.data || {}
|
||||
items.value = data.items || []
|
||||
pagination.itemCount = data.total || 0
|
||||
boardOptions.value = [{ label: '全部版块', value: null }, ...((data.options?.boards || []).map((item) => ({ label: item.name, value: item.id })))]
|
||||
statusOptions.value = data.options?.status || [{ label: '全部', value: 'all' }]
|
||||
reviewOptions.value = [{ label: '全部审核', value: null }, ...(data.options?.review || [])]
|
||||
visibilityOptions.value = [{ label: '全部可见性', value: null }, ...(data.options?.visibility || [])]
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载主题列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(nextPage) {
|
||||
loadData(nextPage)
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.board_id = null
|
||||
filters.status = 'all'
|
||||
filters.review_status = null
|
||||
filters.visibility = null
|
||||
loadData(1)
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
detailTopicId.value = row.id
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => loadData(1))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-forum-topics__section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topics__title-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topics__title {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-topics__subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,284 +0,0 @@
|
||||
<template>
|
||||
<n-drawer :show="show" :width="620" @update:show="$emit('update:show', $event)">
|
||||
<n-drawer-content title="主题治理" closable>
|
||||
<n-spin :show="loading">
|
||||
<template v-if="detail.topic">
|
||||
<n-descriptions bordered :column="1" size="small" label-placement="left">
|
||||
<n-descriptions-item label="主题标题">{{ detail.topic.title || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="所属版块">{{ primaryBoardName }}</n-descriptions-item>
|
||||
<n-descriptions-item label="作者">{{ detail.topic.author?.display_name || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="可见性">{{ detail.topic.visibility?.label || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="审核状态">{{ detail.topic.review_status?.label || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="主题状态">{{ detail.topic.status || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="置顶">{{ stickySummary }}</n-descriptions-item>
|
||||
<n-descriptions-item label="精华">{{ detail.topic.featured ? '是' : '否' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="帖子类型">{{ bbsTypeLabel }}</n-descriptions-item>
|
||||
<n-descriptions-item v-if="detail.topic.bbs_type === 'question'" label="提问状态">{{ questionStatusLabel }}</n-descriptions-item>
|
||||
<n-descriptions-item label="收藏">{{ detail.topic.favorite_count ?? 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="评分">{{ detail.topic.score ?? 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="专题">{{ detail.topic.special_topic || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="投票">{{ voteSummary }}</n-descriptions-item>
|
||||
<n-descriptions-item label="治理备注">{{ detail.topic.moderation_note || '-' }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div class="aksrt-topic-drawer__excerpt">
|
||||
<div class="aksrt-topic-drawer__section-title">内容摘要</div>
|
||||
<p>{{ detail.topic.excerpt || '暂无摘要' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="aksrt-topic-drawer__actions">
|
||||
<div class="aksrt-topic-drawer__section-title">执行治理动作</div>
|
||||
<n-space vertical :size="12">
|
||||
<n-select v-model:value="form.action" :options="actionOptions" />
|
||||
|
||||
<n-select
|
||||
v-if="form.action === 'review'"
|
||||
v-model:value="form.review_status"
|
||||
:options="detail.options?.review || []"
|
||||
placeholder="选择审核状态"
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-if="form.action === 'move_board'"
|
||||
v-model:value="form.target_board_id"
|
||||
:options="boardOptions"
|
||||
placeholder="选择目标版块"
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-if="form.action === 'visibility'"
|
||||
v-model:value="form.visibility"
|
||||
:options="detail.options?.visibility || []"
|
||||
placeholder="选择可见性"
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-if="form.action === 'sticky'"
|
||||
v-model:value="form.sticky_value"
|
||||
:options="stickyLevelOptions"
|
||||
placeholder="选择置顶级别"
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-if="form.action === 'question_status'"
|
||||
v-model:value="form.question_status_value"
|
||||
:options="questionStatusOptions"
|
||||
placeholder="选择提问状态"
|
||||
/>
|
||||
|
||||
<n-select
|
||||
v-if="booleanActions.includes(form.action) && form.action !== 'sticky'"
|
||||
v-model:value="form.value"
|
||||
:options="booleanOptions"
|
||||
placeholder="选择状态"
|
||||
/>
|
||||
|
||||
<n-input
|
||||
v-model:value="form.note"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="记录本次处理原因,便于后续审计"
|
||||
/>
|
||||
|
||||
<n-space>
|
||||
<n-button type="primary" :loading="submitting" @click="handleSubmit">执行</n-button>
|
||||
<n-button @click="loadDetail">刷新详情</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-empty v-else description="暂无主题详情" />
|
||||
</n-spin>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
topicId: { type: [Number, String], default: null },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show', 'updated'])
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const detail = ref({ topic: null, options: { boards: [], visibility: [], review: [] } })
|
||||
// sticky 现在是 0~3 分级选择,不再是布尔动作(§7.3 置顶分级)。
|
||||
const booleanActions = ['featured', 'locked', 'close', 'hide']
|
||||
|
||||
const form = reactive({
|
||||
action: 'review',
|
||||
review_status: 'pending',
|
||||
target_board_id: null,
|
||||
visibility: 'public',
|
||||
value: '1',
|
||||
sticky_value: 0,
|
||||
question_status_value: 0,
|
||||
note: '',
|
||||
})
|
||||
|
||||
const actionOptions = [
|
||||
{ label: '审核状态', value: 'review' },
|
||||
{ label: '移动版块', value: 'move_board' },
|
||||
{ label: '修改可见性', value: 'visibility' },
|
||||
{ label: '置顶分级', value: 'sticky' },
|
||||
{ label: '精华切换', value: 'featured' },
|
||||
{ label: '锁定切换', value: 'locked' },
|
||||
{ label: '关闭帖子', value: 'close' },
|
||||
{ label: '隐藏帖子', value: 'hide' },
|
||||
{ label: '提问状态', value: 'question_status' },
|
||||
]
|
||||
|
||||
const booleanOptions = [
|
||||
{ label: '启用', value: '1' },
|
||||
{ label: '关闭', value: '0' },
|
||||
]
|
||||
|
||||
// 置顶分级选项(对标 zibll topping 0/1/2/3)。
|
||||
const stickyLevelOptions = [
|
||||
{ label: '不置顶', value: 0 },
|
||||
{ label: '版块置顶', value: 1 },
|
||||
{ label: '超级置顶', value: 2 },
|
||||
{ label: '全局置顶', value: 3 },
|
||||
]
|
||||
|
||||
const questionStatusOptions = [
|
||||
{ label: '未解决', value: 0 },
|
||||
{ label: '已解决', value: 1 },
|
||||
]
|
||||
|
||||
const primaryBoardName = computed(() => detail.value?.topic?.boards?.[0]?.name || '-')
|
||||
const boardOptions = computed(() => (detail.value?.options?.boards || []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})))
|
||||
// 置顶摘要:优先用后端返回的 sticky_label,否则按 sticky_level 映射。
|
||||
const stickySummary = computed(() => {
|
||||
const topic = detail.value?.topic
|
||||
if (!topic) return '-'
|
||||
const label = topic.sticky_label
|
||||
if (label) return label
|
||||
const level = topic.sticky_level ?? 0
|
||||
const found = stickyLevelOptions.find((opt) => opt.value === level)
|
||||
return found ? found.label : '不置顶'
|
||||
})
|
||||
const voteSummary = computed(() => {
|
||||
const vote = detail.value?.topic?.vote
|
||||
if (!vote?.enabled) {
|
||||
return '未开启'
|
||||
}
|
||||
|
||||
return `${vote.total_votes || 0} 票 / ${vote.multiple ? '多选' : '单选'}`
|
||||
})
|
||||
const bbsTypeLabel = computed(() => {
|
||||
const t = detail.value?.topic?.bbs_type
|
||||
return t === 'question' ? '提问' : '标准帖子'
|
||||
})
|
||||
const questionStatusLabel = computed(() => {
|
||||
const s = detail.value?.topic?.question_status
|
||||
return s === 1 ? '已解决' : '未解决'
|
||||
})
|
||||
|
||||
watch(() => props.show, async (visible) => {
|
||||
if (visible && props.topicId) {
|
||||
await loadDetail()
|
||||
} else if (!visible) {
|
||||
detail.value = { topic: null, options: { boards: [], visibility: [], review: [] } }
|
||||
resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
form.action = 'review'
|
||||
form.review_status = detail.value?.topic?.review_status?.key || 'pending'
|
||||
form.target_board_id = detail.value?.topic?.boards?.[0]?.id || null
|
||||
form.visibility = detail.value?.topic?.visibility?.key || 'public'
|
||||
form.value = '1'
|
||||
form.sticky_value = detail.value?.topic?.sticky_level ?? 0
|
||||
form.question_status_value = detail.value?.topic?.question_status ?? 0
|
||||
form.note = ''
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.topicId) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request(`/forum/topics/${props.topicId}`)
|
||||
detail.value = res?.data || { topic: null, options: { boards: [], visibility: [], review: [] } }
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载主题详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!props.topicId) {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
action: form.action,
|
||||
note: form.note,
|
||||
}
|
||||
|
||||
if (form.action === 'review') {
|
||||
payload.review_status = form.review_status
|
||||
} else if (form.action === 'move_board') {
|
||||
payload.target_board_id = form.target_board_id
|
||||
} else if (form.action === 'visibility') {
|
||||
payload.visibility = form.visibility
|
||||
} else if (form.action === 'sticky') {
|
||||
// 置顶分级:发送 0~3 整数。
|
||||
payload.value = form.sticky_value
|
||||
} else if (form.action === 'question_status') {
|
||||
payload.value = form.question_status_value
|
||||
} else {
|
||||
payload.value = form.value === '1'
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await api.request(`/forum/topics/${props.topicId}/moderate`, {
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
message.success('治理动作已执行')
|
||||
await loadDetail()
|
||||
emit('updated')
|
||||
} catch (error) {
|
||||
message.error(error?.message || '治理动作执行失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-topic-drawer__excerpt,
|
||||
.aksrt-topic-drawer__actions {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.aksrt-topic-drawer__section-title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.aksrt-topic-drawer__excerpt p {
|
||||
margin: 0;
|
||||
color: #4b5563;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
@@ -64,11 +64,11 @@ onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [gwRes, ovRes] = await Promise.all([
|
||||
api.request('/shop/gateways').catch(() => null),
|
||||
api.request('/payment/overview').catch(() => null),
|
||||
])
|
||||
if (gwRes?.data?.gateways) {
|
||||
gateways.value = gwRes.data.gateways
|
||||
if (gwRes?.data?.gateway?.gateways) {
|
||||
const gwList = Object.entries(gwRes.data.gateway.gateways).map(([id, enabled]) => ({ id, name: id, configured: !!enabled }))
|
||||
gateways.value = gwList
|
||||
}
|
||||
if (ovRes?.data?.gateway) {
|
||||
overview.value = ovRes.data.gateway
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -91,7 +91,6 @@ const guides = [
|
||||
{ key: 'meilisearch', title: 'Meilisearch', desc: '配置搜索索引与同步策略', url: '#/global/search' },
|
||||
{ key: 'i18n', title: '多语言翻译', desc: '检查语言切换与术语配置', url: '#/global/i18n' },
|
||||
{ key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' },
|
||||
{ key: 'forum', title: '论坛体系', desc: '检查版块、话题与版主管理', url: '#/forum' },
|
||||
{ key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/security' },
|
||||
{ key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' },
|
||||
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与邀请奖励', url: '#/users/signin' },
|
||||
|
||||
@@ -130,7 +130,6 @@ export default {
|
||||
{ key: 'system_notice', desc: '系统公告通知' },
|
||||
{ key: 'order_paid', desc: '订单支付通知' },
|
||||
{ key: 'credits_change', desc: '积分变动通知' },
|
||||
{ key: 'forum_reply', desc: '论坛回复通知' },
|
||||
];
|
||||
|
||||
const allowedTypes = ref([]);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Forum ops Vue pages removed (Phase 4). Config uses SchemaPage.
|
||||
export const forumPageLoaders = {}
|
||||
@@ -2,8 +2,6 @@ import { corePageLoaders } from './core'
|
||||
import { usersPageLoaders } from './users'
|
||||
import { paymentPageLoaders } from './payment'
|
||||
import { toolsPageLoaders } from './tools'
|
||||
import { shopPageLoaders } from './shop'
|
||||
import { forumPageLoaders } from './forum'
|
||||
import { extensionsPageLoaders } from './extensions'
|
||||
|
||||
export const componentLoaders = {
|
||||
@@ -11,7 +9,5 @@ export const componentLoaders = {
|
||||
...usersPageLoaders,
|
||||
...paymentPageLoaders,
|
||||
...toolsPageLoaders,
|
||||
...shopPageLoaders,
|
||||
...forumPageLoaders,
|
||||
...extensionsPageLoaders,
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Shop ops Vue pages removed (Phase 4). Config uses SchemaPage.
|
||||
export const shopPageLoaders = {}
|
||||
@@ -73,21 +73,9 @@ 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: '/shop/products', title: '商品管理', nativeAdminPath: 'edit.php?post_type=aksrt_product' },
|
||||
{ path: '/shop/orders', title: '订单管理', nativeAdminPath: 'admin.php?page=aksrt_orders' },
|
||||
{ path: '/shop/after-sale', title: '售后管理', nativeAdminPath: 'admin.php?page=aksrt_after_sale' },
|
||||
{ path: '/shop/shipping', title: '物流管理', nativeAdminPath: 'admin.php?page=aksrt_shipping' },
|
||||
{ path: '/shop/coupons', title: '优惠券', nativeAdminPath: 'admin.php?page=aksrt_coupons' },
|
||||
{ path: '/shop/withdrawals', title: '提现管理', nativeAdminPath: 'admin.php?page=aksrt_withdraw' },
|
||||
{ path: '/shop/dashboard', title: '商城概览', nativeAdminPath: 'admin.php?page=aksrt_pay' },
|
||||
{ path: '/forum/boards', title: '板块管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' },
|
||||
{ path: '/forum/topics', title: '主题治理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' },
|
||||
{ path: '/forum/moderators', title: '版主管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' },
|
||||
{ path: '/forum/moderator-applications', title: '版主申请', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' },
|
||||
{ path: '/forum/dashboard', title: '论坛概览', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' },
|
||||
{ path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' },
|
||||
{ path: '/payment/revenue', title: '创作分成', nativeAdminPath: 'admin.php?page=aksrt_income' },
|
||||
{ path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' },
|
||||
|
||||
@@ -34,8 +34,6 @@ import { usersSchemas } from './sections/users/index.js'
|
||||
import { paymentSchemas } from './sections/payment/index.js'
|
||||
import { extensionsSchemas } from './sections/extensions/index.js'
|
||||
import { toolsSchemas } from './sections/tools/index.js'
|
||||
import { shopSchemas } from './sections/shop/index.js'
|
||||
import { forumSchemas } from './sections/forum/index.js'
|
||||
|
||||
export function normalizeSchemaDomain(domain = '') {
|
||||
const value = String(domain).trim()
|
||||
@@ -55,8 +53,6 @@ export const CANONICAL_SCHEMA_DOMAINS = [
|
||||
'payment',
|
||||
'extensions',
|
||||
'tools',
|
||||
'shop',
|
||||
'forum',
|
||||
]
|
||||
|
||||
/** All schemas organized by domain */
|
||||
@@ -69,8 +65,6 @@ export const ALL_SCHEMAS = Object.freeze({
|
||||
payment: paymentSchemas,
|
||||
extensions: extensionsSchemas,
|
||||
tools: toolsSchemas,
|
||||
shop: shopSchemas,
|
||||
forum: forumSchemas,
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,6 @@ export const auditSchema = {
|
||||
fields: [
|
||||
{ key: 'content_audit_text_enabled', label: '文本审核', type: 'switch', default: true, dependsOn: { key: 'content_audit_enabled', value: true } },
|
||||
{ key: 'content_audit_image_enabled', label: '图片审核', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } },
|
||||
{ key: 'audit_bbs_term', label: '审核论坛分类', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
export const basicSchema = {
|
||||
key: 'forum.basic',
|
||||
title: '基础设置',
|
||||
icon: 'fas fa-power-off',
|
||||
description: '论坛总开关与管理员配置。',
|
||||
defaults: {
|
||||
bbs_s: true,
|
||||
bbs_admin_users: [],
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'switch',
|
||||
title: '功能开关',
|
||||
icon: 'fas fa-toggle-on',
|
||||
fields: [
|
||||
{ key: 'bbs_s', label: '启用论坛', type: 'switch', default: true, help: '关闭后论坛页面将返回 404' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'admin',
|
||||
title: '论坛管理',
|
||||
icon: 'fas fa-user-shield',
|
||||
fields: [
|
||||
{ key: 'bbs_admin_users', label: '论坛管理员', type: 'user_select', multiple: true, default: [], help: '拥有跨版块全部治理权限,绕过 ban' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export const boardPageSchema = {
|
||||
key: 'forum.board_page',
|
||||
title: '版块页面',
|
||||
icon: 'fas fa-th-large',
|
||||
description: '版块页布局、Tab 栏目和移动端交互配置。',
|
||||
defaults: {
|
||||
bbs_plate_top_info_s: true,
|
||||
bbs_plate_tab: [
|
||||
{ label: '全部', sort: 'date' },
|
||||
{ label: '推荐', sort: 'featured' },
|
||||
{ label: '最新回复', sort: 'last_reply' },
|
||||
{ label: '热门', sort: 'hot' },
|
||||
{ label: '精华', sort: 'essence' },
|
||||
],
|
||||
bbs_plate_tab_active_index: 0,
|
||||
bbs_plate_tab_swiper: true,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'layout',
|
||||
title: '版块页布局',
|
||||
icon: 'fas fa-layer-group',
|
||||
fields: [
|
||||
{ key: 'bbs_plate_top_info_s', label: '版块信息卡', type: 'switch', default: true, help: '版块页顶部显示版块详情卡片' },
|
||||
{ key: 'bbs_plate_tab', label: '版块栏目', type: 'group', default: [{ label: '全部', sort: 'date' }, { label: '推荐', sort: 'featured' }, { label: '最新回复', sort: 'last_reply' }, { label: '热门', sort: 'hot' }, { label: '精华', sort: 'essence' }] },
|
||||
{ key: 'bbs_plate_tab_active_index', label: '默认选中 Tab', type: 'number', default: 0, min: 0 },
|
||||
{ key: 'bbs_plate_tab_swiper', label: '移动端滑动切换', type: 'switch', default: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export const creditsSchema = {
|
||||
key: 'forum.credits',
|
||||
title: '积分设置',
|
||||
icon: 'fas fa-coins',
|
||||
description: '论坛行为积分奖励矩阵和每日积分上限。',
|
||||
defaults: {
|
||||
bbs_credits_post_new: 5,
|
||||
bbs_credits_plate_new: 10,
|
||||
bbs_credits_comment_new: 2,
|
||||
bbs_credits_comment_like: 1,
|
||||
bbs_credits_score_extra: 3,
|
||||
bbs_credits_essence: 10,
|
||||
bbs_credits_posts_hot: 5,
|
||||
bbs_hot_recommend_threshold: 500,
|
||||
bbs_credits_adopted: 20,
|
||||
bbs_credits_daily_max: 50,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'earn',
|
||||
title: '积分奖励',
|
||||
icon: 'fas fa-gift',
|
||||
fields: [
|
||||
{ key: 'bbs_credits_post_new', label: '发帖奖励', type: 'number', default: 5, min: 0 },
|
||||
{ key: 'bbs_credits_plate_new', label: '建版奖励', type: 'number', default: 10, min: 0 },
|
||||
{ key: 'bbs_credits_comment_new', label: '回复奖励', type: 'number', default: 2, min: 0 },
|
||||
{ key: 'bbs_credits_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0 },
|
||||
{ key: 'bbs_credits_score_extra', label: '被加分', type: 'number', default: 3, min: 0 },
|
||||
{ key: 'bbs_credits_essence', label: '被设精华', type: 'number', default: 10, min: 0 },
|
||||
{ key: 'bbs_credits_posts_hot', label: '成为热帖', type: 'number', default: 5, min: 0 },
|
||||
{ key: 'bbs_hot_recommend_threshold', label: '热帖推荐分阈值', type: 'number', default: 500, min: 1, help: '推荐分达到该值触发热帖奖励与勋章累计' },
|
||||
{ key: 'bbs_credits_adopted', label: '被采纳', type: 'number', default: 20, min: 0 },
|
||||
{ key: 'bbs_credits_daily_max', label: '每日积分上限', type: 'number', default: 50, min: 0, help: '0 = 不限' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
export const homepageSchema = {
|
||||
key: 'forum.homepage',
|
||||
title: '首页设置',
|
||||
icon: 'fas fa-home',
|
||||
description: '论坛首页 Tab 引擎、快速发帖入口和移动端交互配置。',
|
||||
defaults: {
|
||||
bbs_home_tab: [
|
||||
{ type: 'follow', label: '关注' },
|
||||
{ type: 'synthesis', label: '综合' },
|
||||
{ type: 'plate', label: '版块' },
|
||||
],
|
||||
bbs_home_tab_active_index: 1,
|
||||
bbs_home_tab_quick_posts_s: true,
|
||||
bbs_home_tab_swiper: true,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'tabs',
|
||||
title: '首页 Tab',
|
||||
icon: 'fas fa-columns',
|
||||
fields: [
|
||||
{ key: 'bbs_home_tab', label: '首页栏目', type: 'group', default: [{ type: 'follow', label: '关注' }, { type: 'synthesis', label: '综合' }, { type: 'plate', label: '版块' }], help: '可拖拽排序,支持 follow / synthesis / plate / tabs 类型' },
|
||||
{ key: 'bbs_home_tab_active_index', label: '默认选中 Tab', type: 'number', default: 1, min: 0 },
|
||||
{ key: 'bbs_home_tab_quick_posts_s', label: '快速发帖', type: 'switch', default: true, help: '首页顶部显示快速发帖入口' },
|
||||
{ key: 'bbs_home_tab_swiper', label: '移动端滑动切换', type: 'switch', default: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Forum domain schemas.
|
||||
*
|
||||
* 12 sections matching PHP schema_forum():
|
||||
* basic / names / homepage / board_page / post_page / reply /
|
||||
* permissions / other / upload / credits
|
||||
*/
|
||||
|
||||
import { basicSchema } from './basic.js'
|
||||
import { namesSchema } from './names.js'
|
||||
import { homepageSchema } from './homepage.js'
|
||||
import { boardPageSchema } from './board-page.js'
|
||||
import { postPageSchema } from './post-page.js'
|
||||
import { replySchema } from './reply.js'
|
||||
import { permissionsSchema } from './permissions.js'
|
||||
import { otherSchema } from './other.js'
|
||||
import { uploadSchema } from './upload.js'
|
||||
import { creditsSchema } from './credits.js'
|
||||
|
||||
export const forumSchemas = {
|
||||
basic: basicSchema,
|
||||
names: namesSchema,
|
||||
homepage: homepageSchema,
|
||||
board_page: boardPageSchema,
|
||||
post_page: postPageSchema,
|
||||
reply: replySchema,
|
||||
permissions: permissionsSchema,
|
||||
other: otherSchema,
|
||||
upload: uploadSchema,
|
||||
credits: creditsSchema,
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
export const namesSchema = {
|
||||
key: 'forum.names',
|
||||
title: '名称定义',
|
||||
icon: 'fas fa-language',
|
||||
description: '自定义论坛各实体的显示名称(12 词条)。',
|
||||
defaults: {
|
||||
bbs_forum_display_name: '论坛',
|
||||
bbs_plate_display_name: '版块',
|
||||
bbs_posts_display_name: '帖子',
|
||||
bbs_topic_display_name: '话题',
|
||||
bbs_tag_display_name: '标签',
|
||||
bbs_comment_display_name: '回复',
|
||||
bbs_question_badge_name: '提问',
|
||||
bbs_question_ok_badge_name: '已解决',
|
||||
bbs_cat_moderator_name: '分区版主',
|
||||
bbs_plate_author_name: '超级版主',
|
||||
bbs_plate_moderator_name: '版主',
|
||||
bbs_plate_follow_name: '关注',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'display_names',
|
||||
title: '名词自定义',
|
||||
icon: 'fas fa-tags',
|
||||
fields: [
|
||||
{ key: 'bbs_forum_display_name', label: '论坛', type: 'input', default: '论坛' },
|
||||
{ key: 'bbs_plate_display_name', label: '版块', type: 'input', default: '版块' },
|
||||
{ key: 'bbs_posts_display_name', label: '帖子', type: 'input', default: '帖子' },
|
||||
{ key: 'bbs_topic_display_name', label: '话题', type: 'input', default: '话题' },
|
||||
{ key: 'bbs_tag_display_name', label: '标签', type: 'input', default: '标签' },
|
||||
{ key: 'bbs_comment_display_name', label: '回复', type: 'input', default: '回复' },
|
||||
{ key: 'bbs_question_badge_name', label: '提问徽章', type: 'input', default: '提问' },
|
||||
{ key: 'bbs_question_ok_badge_name', label: '已解决徽章', type: 'input', default: '已解决' },
|
||||
{ key: 'bbs_cat_moderator_name', label: '分区版主', type: 'input', default: '分区版主' },
|
||||
{ key: 'bbs_plate_author_name', label: '超级版主', type: 'input', default: '超级版主' },
|
||||
{ key: 'bbs_plate_moderator_name', label: '版主', type: 'input', default: '版主' },
|
||||
{ key: 'bbs_plate_follow_name', label: '关注', type: 'input', default: '关注' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
export const otherSchema = {
|
||||
key: 'forum.other',
|
||||
title: '其它设置',
|
||||
icon: 'fas fa-cog',
|
||||
description: '投票、版主申请说明、标题字数限制和 URL 别名配置。',
|
||||
defaults: {
|
||||
bbs_vote_number_type: 'percent',
|
||||
bbs_vote_max: 8,
|
||||
bbs_apply_moderator_desc: '欢迎有热情的用户申请成为版主,协助管理社区秩序。',
|
||||
bbs_term_thumb: '',
|
||||
bbs_post_lists_hide_pay_s: false,
|
||||
bbs_post_title_strlen_limit: 30,
|
||||
bbs_t_placeholder: '请输入标题',
|
||||
bbs_c_placeholder: '请输入内容',
|
||||
bbs_rewrite_suffix_html_s: true,
|
||||
bbs_plate_rewrite_slug: 'board',
|
||||
bbs_posts_rewrite_slug: 'topic',
|
||||
bbs_posts_edit_rewrite_slug: 'topic-edit',
|
||||
bbs_posts_show_in_rest_s: false,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'vote',
|
||||
title: '投票',
|
||||
icon: 'fas fa-poll',
|
||||
fields: [
|
||||
{ key: 'bbs_vote_number_type', label: '投票显示', type: 'select', default: 'percent', options: [{ label: '百分比', value: 'percent' }, { label: '票数', value: 'number' }] },
|
||||
{ key: 'bbs_vote_max', label: '选项上限', type: 'number', default: 8, min: 2, max: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'moderator_apply',
|
||||
title: '版主申请',
|
||||
icon: 'fas fa-user-plus',
|
||||
fields: [
|
||||
{ key: 'bbs_apply_moderator_desc', label: '申请说明', type: 'textarea', default: '欢迎有热情的用户申请成为版主,协助管理社区秩序。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'misc',
|
||||
title: '杂项',
|
||||
icon: 'fas fa-sliders-h',
|
||||
fields: [
|
||||
{ key: 'bbs_term_thumb', label: '标签默认缩略图', type: 'media', default: '' },
|
||||
{ key: 'bbs_post_lists_hide_pay_s', label: '列表隐藏付费标记', type: 'switch', default: false },
|
||||
{ key: 'bbs_post_title_strlen_limit', label: '标题字数限制', type: 'number', default: 30, min: 5, max: 100, help: '最小 5,最大 100' },
|
||||
{ key: 'bbs_t_placeholder', label: '标题占位符', type: 'input', default: '请输入标题' },
|
||||
{ key: 'bbs_c_placeholder', label: '内容占位符', type: 'input', default: '请输入内容' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
title: 'URL / 别名',
|
||||
icon: 'fas fa-link',
|
||||
fields: [
|
||||
{ key: 'bbs_rewrite_suffix_html_s', label: '.html 后缀', type: 'switch', default: true },
|
||||
{ key: 'bbs_plate_rewrite_slug', label: '版块别名', type: 'input', default: 'board' },
|
||||
{ key: 'bbs_posts_rewrite_slug', label: '帖子别名', type: 'input', default: 'topic' },
|
||||
{ key: 'bbs_posts_edit_rewrite_slug', label: '编辑别名', type: 'input', default: 'topic-edit' },
|
||||
{ key: 'bbs_posts_show_in_rest_s', label: '古腾堡编辑器', type: 'switch', default: false, help: '开启后帖子可使用 Gutenberg' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
export const permissionsSchema = {
|
||||
key: 'forum.permissions',
|
||||
title: '权限配置',
|
||||
icon: 'fas fa-lock',
|
||||
description: '发帖限制、按钮显示、帖子付费和版块付费关注配置。',
|
||||
defaults: {
|
||||
bbs_posts_add_limit_opt_max: 10,
|
||||
bbs_plate_add_limit_opt_max: 3,
|
||||
bbs_show_new_plate: true,
|
||||
bbs_show_new_topic: true,
|
||||
bbs_show_apply_moderator: true,
|
||||
bbs_post_pay_hide_type_s: true,
|
||||
bbs_post_pay_vip_price_s: true,
|
||||
bbs_post_pay_vip_1_discount: 80,
|
||||
bbs_post_pay_vip_2_discount: 50,
|
||||
bbs_post_pay_price_limit: 1000,
|
||||
bbs_plate_follow_pay_s: false,
|
||||
bbs_plate_follow_pay_price: 10,
|
||||
bbs_plate_follow_pay_type: 'money',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'posting',
|
||||
title: '发帖限制',
|
||||
icon: 'fas fa-ban',
|
||||
fields: [
|
||||
{ key: 'bbs_posts_add_limit_opt_max', label: '每日发帖上限', type: 'number', default: 10, min: 0, help: '0 = 不限' },
|
||||
{ key: 'bbs_plate_add_limit_opt_max', label: '建版数量上限', type: 'number', default: 3, min: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'display_btns',
|
||||
title: '按钮显示',
|
||||
icon: 'fas fa-eye',
|
||||
fields: [
|
||||
{ key: 'bbs_show_new_plate', label: '始终显示建版按钮', type: 'switch', default: true },
|
||||
{ key: 'bbs_show_new_topic', label: '始终显示发帖按钮', type: 'switch', default: true },
|
||||
{ key: 'bbs_show_apply_moderator', label: '始终显示申请版主', type: 'switch', default: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'paid_content',
|
||||
title: '帖子付费',
|
||||
icon: 'fas fa-money-bill-wave',
|
||||
fields: [
|
||||
{ key: 'bbs_post_pay_hide_type_s', label: '启用付费内容', type: 'switch', default: true },
|
||||
{ key: 'bbs_post_pay_vip_price_s', label: 'VIP 特价', type: 'switch', default: true, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } },
|
||||
{ key: 'bbs_post_pay_vip_1_discount', label: '一级 VIP 折扣 (%)', type: 'number', default: 80, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } },
|
||||
{ key: 'bbs_post_pay_vip_2_discount', label: '二级 VIP 折扣 (%)', type: 'number', default: 50, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } },
|
||||
{ key: 'bbs_post_pay_price_limit', label: '最高定价', type: 'number', default: 1000, min: 1, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'plate_follow',
|
||||
title: '版块付费关注',
|
||||
icon: 'fas fa-heart',
|
||||
fields: [
|
||||
{ key: 'bbs_plate_follow_pay_s', label: '启用付费关注', type: 'switch', default: false },
|
||||
{ key: 'bbs_plate_follow_pay_price', label: '关注价格', type: 'number', default: 10, min: 0, dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } },
|
||||
{ key: 'bbs_plate_follow_pay_type', label: '支付方式', type: 'select', default: 'money', options: [{ label: '现金', value: 'money' }, { label: '积分', value: 'points' }], dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
export const postPageSchema = {
|
||||
key: 'forum.post_page',
|
||||
title: '帖子页面',
|
||||
icon: 'fas fa-file-alt',
|
||||
description: '帖子页面包屑、评分、高度限制、封面比例和视频静音配置。',
|
||||
defaults: {
|
||||
bbs_breadcrumbs_s: true,
|
||||
bbs_breadcrumbs_home: true,
|
||||
bbs_breadcrumbs_bbs_home: true,
|
||||
bbs_breadcrumbs_bbs_home_name: '论坛',
|
||||
bbs_breadcrumbs_plate_cat: true,
|
||||
bbs_score_extra_max: 5,
|
||||
bbs_score_deduct_max: 3,
|
||||
bbs_posts_maxheight_s: false,
|
||||
bbs_posts_maxheight: 800,
|
||||
bbs_posts_cover_ratio: '16:9',
|
||||
bbs_posts_cover_video_mute: true,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'breadcrumbs',
|
||||
title: '面包屑',
|
||||
icon: 'fas fa-angle-double-right',
|
||||
fields: [
|
||||
{ key: 'bbs_breadcrumbs_s', label: '显示面包屑', type: 'switch', default: true },
|
||||
{ key: 'bbs_breadcrumbs_home', label: '显示首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
|
||||
{ key: 'bbs_breadcrumbs_bbs_home', label: '显示论坛首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
|
||||
{ key: 'bbs_breadcrumbs_bbs_home_name', label: '论坛首页名称', type: 'input', default: '论坛', dependsOn: { key: 'bbs_breadcrumbs_bbs_home', value: true } },
|
||||
{ key: 'bbs_breadcrumbs_plate_cat', label: '显示分区', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'score',
|
||||
title: '评分设置',
|
||||
icon: 'fas fa-star',
|
||||
fields: [
|
||||
{ key: 'bbs_score_extra_max', label: '加分上限', type: 'number', default: 5, min: 1, max: 50 },
|
||||
{ key: 'bbs_score_deduct_max', label: '扣分上限', type: 'number', default: 3, min: 1, max: 50 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'display',
|
||||
title: '内容显示',
|
||||
icon: 'fas fa-expand-arrows-alt',
|
||||
fields: [
|
||||
{ key: 'bbs_posts_maxheight_s', label: '内容高度限制', type: 'switch', default: false },
|
||||
{ key: 'bbs_posts_maxheight', label: '最大高度 (px)', type: 'number', default: 800, min: 300, max: 3000, dependsOn: { key: 'bbs_posts_maxheight_s', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'cover',
|
||||
title: '封面设置',
|
||||
icon: 'fas fa-image',
|
||||
fields: [
|
||||
{ key: 'bbs_posts_cover_ratio', label: '封面比例', type: 'select', default: '16:9', options: [{ label: '16:9', value: '16:9' }, { label: '4:3', value: '4:3' }, { label: '1:1', value: '1:1' }, { label: '3:4', value: '3:4' }] },
|
||||
{ key: 'bbs_posts_cover_video_mute', label: '视频封面静音', type: 'switch', default: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
export const replySchema = {
|
||||
key: 'forum.reply',
|
||||
title: '回复评论',
|
||||
icon: 'fas fa-reply-all',
|
||||
description: '回复翻页、工具栏功能、表情、代码和快捷回复配置。',
|
||||
defaults: {
|
||||
bbs_reply_paginate_type: 'ajax',
|
||||
bbs_reply_paginate_ias_s: true,
|
||||
bbs_reply_paginate_ias_max: 3,
|
||||
bbs_comment_smilie: true,
|
||||
bbs_comment_code: true,
|
||||
bbs_comment_img: true,
|
||||
bbs_comment_upload_img: true,
|
||||
bbs_comment_quick_s: true,
|
||||
bbs_comment_quick_often: '不错,学习了\n感谢分享\n期待更新',
|
||||
bbs_comment_placeholder: '写下你的回复…',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'paginate',
|
||||
title: '翻页',
|
||||
icon: 'fas fa-chevron-circle-right',
|
||||
fields: [
|
||||
{ key: 'bbs_reply_paginate_type', label: '翻页方式', type: 'select', default: 'ajax', options: [{ label: 'AJAX 加载', value: 'ajax' }, { label: '传统翻页', value: 'page' }] },
|
||||
{ key: 'bbs_reply_paginate_ias_s', label: '无限滚动', type: 'switch', default: true },
|
||||
{ key: 'bbs_reply_paginate_ias_max', label: '自动加载页数', type: 'number', default: 3, min: 0, max: 10, dependsOn: { key: 'bbs_reply_paginate_ias_s', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'toolbar',
|
||||
title: '回复工具栏',
|
||||
icon: 'fas fa-toolbox',
|
||||
fields: [
|
||||
{ key: 'bbs_comment_smilie', label: '表情', type: 'switch', default: true },
|
||||
{ key: 'bbs_comment_code', label: '代码', type: 'switch', default: true },
|
||||
{ key: 'bbs_comment_img', label: '插图', type: 'switch', default: true },
|
||||
{ key: 'bbs_comment_upload_img', label: '上传图片', type: 'switch', default: true },
|
||||
{ key: 'bbs_comment_quick_s', label: '快捷回复', type: 'switch', default: true },
|
||||
{ key: 'bbs_comment_quick_often', label: '预设短语', type: 'textarea', default: '不错,学习了\n感谢分享\n期待更新', help: '每行一条', dependsOn: { key: 'bbs_comment_quick_s', value: true } },
|
||||
{ key: 'bbs_comment_placeholder', label: '回复占位符', type: 'input', default: '写下你的回复…' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
export const uploadSchema = {
|
||||
key: 'forum.upload',
|
||||
title: '上传权限',
|
||||
icon: 'fas fa-cloud-upload-alt',
|
||||
description: '论坛帖子图片、视频和文件的上传大小与格式限制。',
|
||||
defaults: {
|
||||
bbs_upload_img_size: 2048,
|
||||
bbs_image_upload_multiple: 9,
|
||||
bbs_upload_video_size: 20480,
|
||||
bbs_upload_file_size: 10240,
|
||||
bbs_upload_file_mimes: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'limits',
|
||||
title: '上传限制',
|
||||
icon: 'fas fa-weight-hanging',
|
||||
fields: [
|
||||
{ key: 'bbs_upload_img_size', label: '图片大小 (KB)', type: 'number', default: 2048, min: 100 },
|
||||
{ key: 'bbs_image_upload_multiple', label: '批量上传数', type: 'number', default: 9, min: 1, max: 20 },
|
||||
{ key: 'bbs_upload_video_size', label: '视频大小 (KB)', type: 'number', default: 20480, min: 1024 },
|
||||
{ key: 'bbs_upload_file_size', label: '文件大小 (KB)', type: 'number', default: 10240, min: 100 },
|
||||
{ key: 'bbs_upload_file_mimes', label: '允许格式', type: 'input', default: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', help: '逗号分隔' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -25,8 +25,6 @@ export const searchSchema = {
|
||||
search_type_user: false,
|
||||
search_type_category: true,
|
||||
search_type_tag: true,
|
||||
search_type_product: false,
|
||||
search_type_topic: false,
|
||||
search_default_type: 'post',
|
||||
search_ms_enabled: false,
|
||||
search_ms_api_url: '',
|
||||
@@ -63,8 +61,6 @@ export const searchSchema = {
|
||||
{ key: 'search_type_user', label: '用户', type: 'switch', default: false },
|
||||
{ key: 'search_type_category', label: '分类', type: 'switch', default: true },
|
||||
{ key: 'search_type_tag', label: '标签', type: 'switch', default: true },
|
||||
{ key: 'search_type_product', label: '商品', type: 'switch', default: false },
|
||||
{ key: 'search_type_topic', label: '帖子', type: 'switch', default: false },
|
||||
{
|
||||
key: 'search_default_type',
|
||||
label: '默认类型',
|
||||
@@ -75,8 +71,6 @@ export const searchSchema = {
|
||||
{ label: '用户', value: 'user' },
|
||||
{ label: '分类', value: 'category' },
|
||||
{ label: '标签', value: 'tag' },
|
||||
{ label: '商品', value: 'product' },
|
||||
{ label: '帖子', value: 'topic' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -136,15 +136,9 @@ export const pageDisplaySchemas = {
|
||||
], dependsOn: { key: 'header_newposts_enabled', value: true } },
|
||||
{ key: 'header_newposts_types', label: '发布类型', type: 'checkbox', default: ['post'], options: [
|
||||
{ label: '文章', value: 'post' },
|
||||
{ label: '帖子', value: 'topic' },
|
||||
{ label: '商品', value: 'product' },
|
||||
{ label: '版块', value: 'plate' },
|
||||
], dependsOn: { key: 'header_newposts_enabled', value: true }, help: '未开启的模块不会显示' },
|
||||
{ key: 'header_user_newposts_btn', label: '用户卡发布类型', type: 'select', default: 'post', options: [
|
||||
{ label: '文章', value: 'post' },
|
||||
{ label: '帖子', value: 'topic' },
|
||||
{ label: '商品', value: 'product' },
|
||||
{ label: '版块', value: 'plate' },
|
||||
] },
|
||||
{ key: 'header_user_newposts_text', label: '用户卡发布文案', type: 'input', default: '发布内容' },
|
||||
{ key: 'header_nav_pay_vip', label: '导航栏开通会员按钮', type: 'switch', default: true, help: '顶部导航显示开通会员' },
|
||||
@@ -349,7 +343,7 @@ export const pageDisplaySchemas = {
|
||||
title: 'Tab 配置',
|
||||
icon: 'fas fa-columns',
|
||||
fields: [
|
||||
{ key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits,product,plate,forum' },
|
||||
{ key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits' },
|
||||
{ key: 'user_home_tabs_order', label: 'Tab 显示顺序', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔,控制栏目顺序' },
|
||||
{ key: 'user_home_tabs_pc_hide', label: 'PC 端隐藏', type: 'input', default: '', help: '逗号分隔要在 PC 端隐藏的栏目' },
|
||||
{ key: 'user_home_tabs_mobile_hide', label: '移动端隐藏', type: 'input', default: '', help: '逗号分隔要在移动端隐藏的栏目' },
|
||||
@@ -669,9 +663,6 @@ export const pageDisplaySchemas = {
|
||||
{ label: '用户页', value: 'author' },
|
||||
{ label: '文章页', value: 'single' },
|
||||
{ label: '搜索页', value: 'search' },
|
||||
{ label: '[论坛]首页', value: 'forum_home' },
|
||||
{ label: '[论坛]板块页面', value: 'forum_plate' },
|
||||
{ label: '[论坛]帖子页面', value: 'forum_post' },
|
||||
{ label: '其它页面', value: 'page' },
|
||||
], help: '选择启用顶部多功能组件的页面类型,一个都不选则为关闭此功能' },
|
||||
{ key: 'header_components_device_visibility', label: '设备显示规则', type: 'radio', default: '', options: [
|
||||
@@ -717,13 +708,10 @@ export const pageDisplaySchemas = {
|
||||
{ key: 'header_components_search_popular_limit', label: '关键词最多显示', type: 'number', default: 6, min: 2, max: 50, dependsOn: { key: 'header_components_search_show_keywords', value: true } },
|
||||
{ key: 'header_components_search_show_input_cat', label: '分类搜索', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示分类搜索选择' },
|
||||
{ key: 'header_components_search_in_cat', label: '默认搜索的分类', type: 'input', default: '', dependsOn: { key: 'header_components_search_show_input_cat', value: true }, help: '分类 ID,可留空' },
|
||||
{ key: 'header_components_search_show_type', label: '显示类型选择', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示切换搜索类型的按钮(文章、用户、版块、帖子、商品)' },
|
||||
{ key: 'header_components_search_show_type', label: '显示类型选择', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示切换搜索类型的按钮(文章、用户)' },
|
||||
{ key: 'header_components_search_in_type', label: '默认搜索类型', type: 'radio', default: 'post', dependsOn: { key: 'header_components_search_show_type', value: true }, options: [
|
||||
{ label: '文章', value: 'post' },
|
||||
{ label: '用户', value: 'user' },
|
||||
{ label: '商品', value: 'product' },
|
||||
{ label: '帖子', value: 'forum' },
|
||||
{ label: '版块', value: 'plate' },
|
||||
]},
|
||||
{
|
||||
key: 'header_components_search_before_html',
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -7,16 +7,10 @@ export const MEDAL_GET_TYPES = [
|
||||
{ label: '注册天数', value: 'registration_time' },
|
||||
{ label: '累计签到天数', value: 'checkin_all_day' },
|
||||
{ label: '发布文章数', value: 'new_post' },
|
||||
{ label: '论坛发帖数', value: 'bbs_post' },
|
||||
{ label: '评论数', value: 'comment' },
|
||||
{ label: '粉丝数', value: 'followed' },
|
||||
{ label: '文章获赞', value: 'like' },
|
||||
{ label: '评论获赞', value: 'comment_like' },
|
||||
{ label: '回复被采纳', value: 'bbs_adopt' },
|
||||
{ label: '神评次数', value: 'bbs_comment_hot' },
|
||||
{ label: '热帖次数', value: 'bbs_post_hot' },
|
||||
{ label: '精华帖数', value: 'bbs_essence' },
|
||||
{ label: '帖子评分累计', value: 'bbs_score' },
|
||||
{ label: '内容阅读量', value: 'views' },
|
||||
{ label: '创作分成累计(元)', value: 'pay_income' },
|
||||
{ label: '推广佣金累计(元)', value: 'pay_rebate' },
|
||||
|
||||
@@ -19,13 +19,6 @@ export const creditsSchema = {
|
||||
points_free_comment_new: 1,
|
||||
points_free_comment_like: 1,
|
||||
points_free_followed: 1,
|
||||
points_free_bbs_posts_new: 5,
|
||||
points_free_bbs_reply: 2,
|
||||
points_free_bbs_essence: 10,
|
||||
points_free_bbs_adopt: 20,
|
||||
points_free_bbs_posts_hot: 5,
|
||||
points_free_bbs_comment_hot: 5,
|
||||
points_free_bbs_score_extra: 3,
|
||||
points_earn_invite: 10,
|
||||
points_spend_comment: 0,
|
||||
points_spend_download: 0,
|
||||
@@ -74,13 +67,6 @@ export const creditsSchema = {
|
||||
{ key: 'points_free_comment_new', label: '发表评论', type: 'number', default: 1, min: 0, max: 100 },
|
||||
{ key: 'points_free_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0, max: 100 },
|
||||
{ key: 'points_free_followed', label: '被关注', type: 'number', default: 1, min: 0, max: 100 },
|
||||
{ key: 'points_free_bbs_posts_new', label: '论坛发帖', type: 'number', default: 5, min: 0, max: 1000 },
|
||||
{ key: 'points_free_bbs_reply', label: '论坛回复', type: 'number', default: 2, min: 0, max: 1000 },
|
||||
{ key: 'points_free_bbs_essence', label: '帖子加精', type: 'number', default: 10, min: 0, max: 1000 },
|
||||
{ key: 'points_free_bbs_adopt', label: '回复被采纳', type: 'number', default: 20, min: 0, max: 1000 },
|
||||
{ key: 'points_free_bbs_posts_hot', label: '热帖奖励', type: 'number', default: 5, min: 0, max: 1000 },
|
||||
{ key: 'points_free_bbs_comment_hot', label: '神评奖励', type: 'number', default: 5, min: 0, max: 1000 },
|
||||
{ key: 'points_free_bbs_score_extra', label: '帖子加分', type: 'number', default: 3, min: 0, max: 1000 },
|
||||
{ key: 'points_earn_invite', label: '邀请注册奖励', type: 'number', default: 10, min: 0, max: 1000, help: '邀请不计入免费日上限' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -11,14 +11,6 @@ export const INTEGRAL_CATALOG = [
|
||||
{ key: 'post_favorite', label: '文章被收藏', default: 2, group: '文章' },
|
||||
{ key: 'comment_new', label: '发表评论', default: 2, group: '文章' },
|
||||
{ key: 'comment_like', label: '评论获赞', default: 1, group: '文章' },
|
||||
{ key: 'bbs_posts_new', label: '发布帖子', default: 3, group: '论坛' },
|
||||
{ key: 'bbs_score_extra', label: '帖子被加分', default: 1, group: '论坛' },
|
||||
{ key: 'bbs_essence', label: '帖子评为精华', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_posts_hot', label: '帖子成为热门', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_plate_new', label: '创建版块', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_plate_hot', label: '版块成为热门', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_adopt', label: '回答被采纳', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_comment_hot', label: '评论成为神评', default: 2, group: '论坛' },
|
||||
]
|
||||
|
||||
export function defaultLevelOpt(max = 10) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
@import url("forum/board.css");
|
||||
@import url("forum/topic.css");
|
||||
@import url("forum/compose.css");
|
||||
@import url("forum/extras.css");
|
||||
|
||||
|
||||
/* Forum home shell: sidebar compose CTA */
|
||||
.aksrt-forum-sidebar-card--compose {
|
||||
padding: 12px;
|
||||
}
|
||||
.aksrt-forum-sidebar-card--compose .aksrt-forum-quick-post__input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--body-bg-color, #f5f6f7);
|
||||
color: var(--muted-color, #777);
|
||||
text-decoration: none;
|
||||
}
|
||||
.aksrt-forum-sidebar-card--compose .aksrt-forum-quick-post__input:hover {
|
||||
color: var(--focus-color, #f04494);
|
||||
}
|
||||
.aksrt-forum-sidebar-card--compose .aksrt-forum-header__action {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.aksrt-forum-header__stats {
|
||||
display: none; /* stats demoted on home shell; counts remain in eyebrow meta */
|
||||
}
|
||||
|
||||
|
||||
/* Forum pages share the single/archive content stream shell */
|
||||
.content-shell--forum .content-wrap {
|
||||
padding-top: 8px;
|
||||
}
|
||||
.content-shell--forum .content-layout__primary > .aksrt-forum-board,
|
||||
.content-shell--forum .content-layout__primary > .aksrt-forum-topic,
|
||||
.content-shell--forum .content-layout__primary > .aksrt-forum-tag,
|
||||
.content-shell--forum .content-layout__primary > .aksrt-forum-plate-cat {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.aksrt-forum-topic__cover {
|
||||
margin: 0 0 16px;
|
||||
border-radius: var(--main-radius, 8px);
|
||||
overflow: hidden;
|
||||
background: var(--body-bg-color, #f5f6f7);
|
||||
}
|
||||
.aksrt-forum-topic__cover--image {
|
||||
aspect-ratio: var(--cover-ratio, 16/9);
|
||||
}
|
||||
.aksrt-forum-topic__cover img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.aksrt-forum-topic__cover--slide {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 100%;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
.aksrt-forum-topic__cover--slide img {
|
||||
scroll-snap-align: start;
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
.aksrt-forum-topic__paywall {
|
||||
margin: 12px 0 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -1,755 +0,0 @@
|
||||
/* Forum home-shell refinement */
|
||||
.aksrt-forum-board__hero {
|
||||
margin-bottom: 20px;
|
||||
padding: 24px;
|
||||
border-radius: var(--radius-2xl, 16px);
|
||||
background: linear-gradient(180deg, rgba(240,68,148,0.05), rgba(255,255,255,0.96));
|
||||
border: none;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__summary-item {
|
||||
min-width: 120px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: 1px solid var(--color-line-soft, rgba(0,0,0,0.06));
|
||||
}
|
||||
|
||||
.aksrt-forum-board__summary-label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__layout {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__governance--inline {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__governance-card--sidebar {
|
||||
position: sticky;
|
||||
top: calc(var(--nav-height, 64px) + 16px);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__moderators--stacked {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.aksrt-forum-board {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 22px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--radius-2xl, 16px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__desc {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__meta i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__compose-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__compose-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__manage-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
background: var(--surface-1, #fff);
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__manage-btn--danger {
|
||||
border-color: var(--color-error, #f5222d);
|
||||
color: var(--color-error, #f5222d);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__manage-btn--danger:hover {
|
||||
background: var(--color-error, #f5222d);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__governance {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__governance-card {
|
||||
padding: 18px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--main-radius, var(--radius, 8px));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__section-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__moderators {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__moderator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-1, #f8f9fa);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__moderator img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__application-form textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__application-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__apply-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__application-note {
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__application-reason {
|
||||
margin: 10px 0;
|
||||
padding: 10px 12px;
|
||||
border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
|
||||
background: var(--surface-2, #f8fafc);
|
||||
color: var(--color-ink-3, #64748b);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__governance-card .aksrt-forum-board__apply-btn {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Sticky section */
|
||||
.aksrt-forum-board__sticky {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface-1, #fff);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__sticky h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__sticky-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
text-decoration: none;
|
||||
color: var(--color-ink);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__sticky-item:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__sticky-meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #c0c4cc);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__sticky-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Topic list items */
|
||||
.aksrt-forum-board__topics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
background: var(--surface-1, #fff);
|
||||
border-bottom: 1px solid var(--color-line-soft);
|
||||
transition: background 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item:hover {
|
||||
background: var(--surface-4, #f8f9fa);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item.is-sticky {
|
||||
background: var(--surface-2, #f0f5ff);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item.is-featured {
|
||||
border-left: 3px solid var(--color-success, #52c41a);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__title a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__title a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge--sticky {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 置顶分级视觉区分(§7.3):超级置顶加深色,全局置顶用红色凸显。 */
|
||||
.aksrt-forum-topic-item__badge--sticky-super {
|
||||
background: var(--focus-color, #fd53a1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge--sticky-global {
|
||||
background: #e53e3e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge--featured {
|
||||
background: var(--color-success, #52c41a);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge--muted {
|
||||
background: var(--surface-2, #f5f7fa);
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__lock {
|
||||
color: var(--color-warning, #fa8c16);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__stats i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
background: #fff;
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__action--link {
|
||||
background: var(--surface-1, #f8f9fa);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.aksrt-forum-board__pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.aksrt-forum-board__empty {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__empty i {
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__empty a {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
|
||||
/* Board sort tabs */
|
||||
.aksrt-forum-board__tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 0 0 16px;
|
||||
padding: 8px;
|
||||
border-radius: var(--radius-xl, 12px);
|
||||
background: var(--surface-1, #fff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
color: var(--color-ink-3, #5c6370);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: background .15s ease, color .15s ease;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__tab:hover {
|
||||
background: rgba(240, 68, 148, 0.08);
|
||||
color: var(--color-primary, #f04494);
|
||||
}
|
||||
|
||||
.aksrt-forum-board__tab.is-active {
|
||||
background: var(--color-primary, #f04494);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-board__governance--footer {
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: var(--radius-xl, 12px);
|
||||
background: var(--surface-1, #fff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__title-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__title a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__badge--sticky { background: rgba(255, 149, 0, 0.12); color: #c77700; }
|
||||
.aksrt-forum-topic-row__badge--featured { background: rgba(240, 68, 148, 0.12); color: #f04494; }
|
||||
.aksrt-forum-topic-row__badge--question { background: rgba(47, 128, 237, 0.12); color: #2f80ed; }
|
||||
.aksrt-forum-topic-row__badge--question.is-solved { background: rgba(39, 174, 96, 0.12); color: #27ae60; }
|
||||
|
||||
.aksrt-forum-topic-row__cover {
|
||||
position: relative;
|
||||
width: 84px;
|
||||
height: 60px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-2, #f5f6f8);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__cover .fa-play-circle,
|
||||
.aksrt-forum-topic-row__cover-count {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
bottom: 6px;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,.35);
|
||||
}
|
||||
|
||||
/* detail list card (zibll forum-posts detail) */
|
||||
.aksrt-forum-topic-row.detail,
|
||||
.aksrt-forum-posts.detail {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 14px;
|
||||
background: var(--surface-1, #fff);
|
||||
box-shadow: var(--shadow-sm, 0 1px 4px rgba(0,0,0,.06));
|
||||
}
|
||||
.aksrt-forum-topic-row__author-card { gap: 10px; }
|
||||
.aksrt-forum-topic-row__author-name { font-weight: 700; font-size: 14px; }
|
||||
.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover,
|
||||
.aksrt-forum-posts.detail .forum-lists-cover {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 280px;
|
||||
margin: 0 0 12px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.aksrt-forum-topic-row.detail .aksrt-forum-topic-row__cover img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 280px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.aksrt-forum-topic-row .excerpt {
|
||||
margin-top: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: var(--color-ink-2, #4b5563);
|
||||
}
|
||||
.aksrt-forum-topic-row .lists-imgs {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.aksrt-forum-topic-row .lists-imgs.count-1 { grid-template-columns: 1fr; max-width: 280px; }
|
||||
.aksrt-forum-topic-row .lists-imgs.count-2 { grid-template-columns: 1fr 1fr; }
|
||||
.aksrt-forum-topic-row .lists-imgs.count-3,
|
||||
.aksrt-forum-topic-row .lists-imgs.count-4 { grid-template-columns: 1fr 1fr; }
|
||||
.aksrt-forum-topic-row .lists-imgs__item {
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1;
|
||||
background: var(--surface-2, #f3f4f6);
|
||||
}
|
||||
.aksrt-forum-topic-row .lists-imgs__item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.aksrt-forum-topic-row__vote { display: grid; gap: 6px; }
|
||||
.aksrt-forum-topic-row__vote-opt {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(80px, 2fr) 42px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.aksrt-forum-topic-row__vote-bar {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-4, #eef0f3);
|
||||
overflow: hidden;
|
||||
}
|
||||
.aksrt-forum-topic-row__vote-bar i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--color-primary, #f04494);
|
||||
}
|
||||
.aksrt-forum-topic-row__footer .but-plate,
|
||||
.aksrt-forum-topic-row__board.but {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 46%;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-4, #f3f4f6);
|
||||
color: var(--color-ink-2, #5c6370);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.aksrt-forum-topic-row__actions {
|
||||
display: inline-flex;
|
||||
gap: 12px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 13px;
|
||||
}
|
||||
.aksrt-forum-topic-row__actions a { color: inherit; text-decoration: none; }
|
||||
.aksrt-forum-topic-row.mini { display: flex; gap: 12px; align-items: flex-start; }
|
||||
|
||||
.aksrt-forum-board-card__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-xl, 12px);
|
||||
background: var(--surface-1, #fff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__cover {
|
||||
display: block;
|
||||
height: 96px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__cover--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(240,68,148,.08), rgba(47,128,237,.08));
|
||||
color: var(--color-primary, #f04494);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__desc,
|
||||
.aksrt-forum-board-card__meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum__empty,
|
||||
.aksrt-forum-board__empty,
|
||||
.aksrt-forum-tag__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 48px 20px;
|
||||
text-align: center;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum__empty a,
|
||||
.aksrt-forum-board__empty a {
|
||||
color: var(--color-primary, #f04494);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -1,714 +0,0 @@
|
||||
/* ============================================================
|
||||
* Compose page
|
||||
* ============================================================ */
|
||||
|
||||
.aksrt-forum-compose {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 48px;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__header h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__notice {
|
||||
margin: 8px 0 0;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__back {
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__back:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* §7.4 — Two-column layout: main + sidebar */
|
||||
.aksrt-forum-compose__layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 311px;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* §7.4 — Sidebar panels */
|
||||
.aksrt-forum-compose__panel {
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--main-radius, var(--radius, 8px));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__panel-title {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
border-bottom: 1px solid var(--color-line, #eceff4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__panel-title i {
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__panel-body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__readonly {
|
||||
margin: 0;
|
||||
color: var(--color-ink-2, #555);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__status-line {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__form {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__main {
|
||||
padding: 24px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--radius-2xl, 16px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__field label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__field .required {
|
||||
color: var(--color-error, #f5222d);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__field select,
|
||||
.aksrt-forum-compose__field input[type="text"],
|
||||
.aksrt-forum-compose__field input[type="number"],
|
||||
.aksrt-forum-compose__field input[type="url"] {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__field textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* §7.4 — wp_editor wrapper */
|
||||
.aksrt-forum-compose__editor-wrap {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__editor-wrap .wp-editor-wrap {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__editor-wrap .wp-editor-container {
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* §7.4 — Character counter */
|
||||
.aksrt-forum-compose__char-counter {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__char-counter.is-near {
|
||||
color: var(--color-warning, #d48806);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__char-counter.is-over {
|
||||
color: var(--color-error, #cf1322);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* §7.4 — Draft notice */
|
||||
.aksrt-forum-compose__draft-notice {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 16px;
|
||||
background: var(--color-info-bg, #e6f7ff);
|
||||
border: 1px solid var(--color-info-border, #91d5ff);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-info, #0050b3);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-toggle {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-list {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface-1, #fff);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-title {
|
||||
flex: 1;
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-title:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-meta {
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-delete {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft-delete:hover {
|
||||
color: var(--color-error, #cf1322);
|
||||
}
|
||||
|
||||
/* §7.4 — Remote search select */
|
||||
.aksrt-remote-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.aksrt-remote-select__input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aksrt-remote-select__dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.aksrt-remote-select__option {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-remote-select__option:hover {
|
||||
background: var(--color-primary-bg, #e6f7ff);
|
||||
}
|
||||
|
||||
/* §7.4 — Tag tokenizer */
|
||||
.aksrt-tag-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-4, #fff);
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.aksrt-tag-input__tokens {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.aksrt-tag-input__token {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: var(--color-primary-bg, #e6f7ff);
|
||||
color: var(--color-primary, #1890ff);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-tag-input__token-remove {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-primary, #1890ff);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.aksrt-tag-input__token-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.aksrt-tag-input__input {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
border: none !important;
|
||||
padding: 2px 4px !important;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.aksrt-tag-input__dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.aksrt-tag-input__suggestion {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-tag-input__suggestion:hover {
|
||||
background: var(--color-primary-bg, #e6f7ff);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__vote-fields[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__vote {
|
||||
margin-bottom: 20px;
|
||||
padding: 18px;
|
||||
border: none;
|
||||
border-radius: var(--radius-2xl, 16px);
|
||||
background: var(--surface-1, #f8f9fa);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__vote-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__vote-head p {
|
||||
margin: 0;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__feedback,
|
||||
.aksrt-forum-board__application-feedback,
|
||||
.aksrt-forum-topic__vote-feedback,
|
||||
.aksrt-forum-topic__moderation-feedback {
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__feedback.is-success,
|
||||
.aksrt-forum-board__application-feedback.is-success,
|
||||
.aksrt-forum-topic__vote-feedback.is-success,
|
||||
.aksrt-forum-topic__moderation-feedback.is-success {
|
||||
color: var(--color-success, #389e0d);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__feedback.is-error,
|
||||
.aksrt-forum-board__application-feedback.is-error,
|
||||
.aksrt-forum-topic__vote-feedback.is-error,
|
||||
.aksrt-forum-topic__moderation-feedback.is-error {
|
||||
color: var(--color-error, #cf1322);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__feedback.is-loading,
|
||||
.aksrt-forum-board__application-feedback.is-loading,
|
||||
.aksrt-forum-topic__vote-feedback.is-loading,
|
||||
.aksrt-forum-topic__moderation-feedback.is-loading {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__submit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__submit:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 24px;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-1, #fff);
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__draft:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-softer, color-mix(in srgb, var(--color-primary) 6%, transparent));
|
||||
}
|
||||
|
||||
/* ── topic cover (§7.3) ── */
|
||||
|
||||
.aksrt-forum-topic__cover {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-2, #f5f7fa);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-slides {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-slides img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-player {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-poster {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-play-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-size: 48px;
|
||||
border-radius: 50%;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-play-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
/* ── topic list cover thumbnails (§7.3) ── */
|
||||
|
||||
.aksrt-forum-topic-item__cover {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--surface-2, #f5f7fa);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__cover-video,
|
||||
.aksrt-forum-topic-item__cover-slide {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__cover-video img,
|
||||
.aksrt-forum-topic-item__cover-slide img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__cover-video i {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__cover-count {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* ── compose cover upload (§7.3) ── */
|
||||
|
||||
.aksrt-forum-compose__cover-upload {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__cover-preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__cover-preview img {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__cover-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 18px;
|
||||
border: 1px dashed var(--color-line, #d9d9d9);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-4, #fff);
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__cover-btn:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__field input[type="url"] {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.aksrt-forum-board__header,
|
||||
.aksrt-forum-board__actions,
|
||||
.aksrt-forum-topic__vote-head,
|
||||
.aksrt-forum-topic__moderation-head,
|
||||
.aksrt-forum-topic__vote-actions,
|
||||
.aksrt-forum-topic__moderation-actions,
|
||||
.aksrt-forum-compose__header,
|
||||
.aksrt-forum-compose__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__actions {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__cover {
|
||||
width: 80px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__cover-slides {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__moderation-grid,
|
||||
.aksrt-forum-compose__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* §7.4 — Stack sidebar below main on mobile */
|
||||
.aksrt-forum-compose__layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__sidebar {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
@import url("extras/base.css");
|
||||
@import url("extras/reply-header-tabs-filters.css");
|
||||
@import url("extras/load-more.css");
|
||||
@import url("extras/additional-role-badges.css");
|
||||
@import url("extras/like-button.css");
|
||||
@import url("extras/reply-action-buttons.css");
|
||||
@import url("extras/quick-post-bar.css");
|
||||
@@ -1,52 +0,0 @@
|
||||
/* --- additional role badges --- */
|
||||
|
||||
.aksrt-forum-reply__role-badge--author {
|
||||
background: #fff7e6;
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__role-badge--super {
|
||||
background: #f6ffed;
|
||||
color: #389e0d;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__role-badge--catmod {
|
||||
background: #fff0f6;
|
||||
color: #c41d7f;
|
||||
}
|
||||
|
||||
/* --- hot comment (神评) badge --- */
|
||||
|
||||
.aksrt-forum-reply__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__badge--hot {
|
||||
background: #fff2e8;
|
||||
color: #d4380d;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__badge--pinned {
|
||||
background: #e6f7ff;
|
||||
color: #096dd9;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply.is-hot {
|
||||
border-left: 3px solid #fa541c;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply.is-pinned {
|
||||
border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
|
||||
}
|
||||
|
||||
.aksrt-forum-reply.is-pinned.is-hot {
|
||||
border-left: 3px solid #fa541c;
|
||||
}
|
||||
|
||||
@@ -1,621 +0,0 @@
|
||||
/* ============================================================
|
||||
* Forum tag archive page
|
||||
* ============================================================ */
|
||||
|
||||
.aksrt-forum-tag {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__header {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
background: var(--surface-1, #fff);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--color-ink);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__title i {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__desc {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__count {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
background: var(--surface-1, #fff);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card:hover {
|
||||
background: var(--surface-1, #f8f9fa);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card__title a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card__title a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card__excerpt {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card__meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-card__meta i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__empty {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-tag__empty i {
|
||||
font-size: 40px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Forum widgets
|
||||
* ============================================================ */
|
||||
|
||||
.aksrt-forum-widget__list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--color-line-soft, #f0f1f3);
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__rank {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface-2, #f4f5f7);
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__item:nth-child(1) .aksrt-forum-widget__rank {
|
||||
background: var(--color-error, #ef4444);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__item:nth-child(2) .aksrt-forum-widget__rank {
|
||||
background: var(--color-warning, #f59e0b);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__item:nth-child(3) .aksrt-forum-widget__rank {
|
||||
background: var(--color-info, #3b82f6);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__link {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__link:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__views,
|
||||
.aksrt-forum-widget__author {
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__boards {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__board {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--color-line-soft, #f0f1f3);
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__board:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__board a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__board a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__board a i {
|
||||
margin-right: 6px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
background: var(--surface-2, #f4f5f7);
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-widget__empty {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── bbs_type / question badges (§7.3) ── */
|
||||
|
||||
.aksrt-forum-topic__badge--question {
|
||||
background: #e6f7ff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--solved {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge--question {
|
||||
background: #e6f7ff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge--solved {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
/* ── favorite button (§7.3) ── */
|
||||
|
||||
.js-aksrt-topic-favorite {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color .15s;
|
||||
}
|
||||
|
||||
.js-aksrt-topic-favorite[data-favorited="1"] {
|
||||
color: var(--color-warning, #faad14);
|
||||
}
|
||||
|
||||
.js-aksrt-topic-favorite:disabled {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── score actions (§7.3) ── */
|
||||
|
||||
.aksrt-forum-topic__score-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 16px 0;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: 1px solid var(--border-2, #e8e8e8);
|
||||
border-radius: var(--radius-m, 8px);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-total {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink, #333);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-total i {
|
||||
color: var(--color-warning, #faad14);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--border-2, #e0e0e0);
|
||||
border-radius: var(--radius-s, 4px);
|
||||
background: var(--surface-1, #fff);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all .15s;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-btn:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-btn--up:hover:not(:disabled) {
|
||||
color: var(--color-success, #52c41a);
|
||||
border-color: var(--color-success, #52c41a);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-btn--down:hover:not(:disabled) {
|
||||
color: var(--color-error, #f5222d);
|
||||
border-color: var(--color-error, #f5222d);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-btn:disabled {
|
||||
opacity: .4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-badge {
|
||||
color: var(--color-warning, #faad14);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-feedback {
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-4, #999);
|
||||
}
|
||||
|
||||
/* ── vote enhancements (§7.3) ── */
|
||||
|
||||
.aksrt-forum-topic__vote--pk .aksrt-forum-topic__vote-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote--expired {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-type-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-expired-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
background: var(--color-error, #f5222d);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-expiry-info {
|
||||
color: var(--color-ink-4, #999);
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* ── topic_cat chip (§7.3) ── */
|
||||
|
||||
.aksrt-forum-topic__chip--topic-cat {
|
||||
background: var(--color-primary-bg, #e6f7ff);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── report modal (§7.5) ── */
|
||||
|
||||
.aksrt-forum-topic__header-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__dialog {
|
||||
position: relative;
|
||||
max-width: 520px;
|
||||
margin: 8vh auto 0;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
background: var(--surface-1, #fff);
|
||||
box-shadow: 0 18px 60px rgba(15, 23, 42, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__head h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__close {
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: var(--color-ink-3, #64748b);
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__target {
|
||||
margin: 0 0 14px;
|
||||
color: var(--color-ink-3, #64748b);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-report-modal__feedback {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.is-report-modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__report-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-ink-4, #94a3b8);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__report-btn:hover {
|
||||
color: var(--color-error, #ef4444);
|
||||
}
|
||||
|
||||
/* ── moderation enhancements (§7.5) ── */
|
||||
|
||||
.aksrt-forum-topic__moderation-note {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: var(--surface-2, #f8fafc);
|
||||
color: var(--color-ink-3, #64748b);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__moderation-note strong {
|
||||
color: var(--color-ink, #334155);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__field small {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: var(--color-ink-4, #94a3b8);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── paywall / credits unlock (§7.3) ── */
|
||||
|
||||
.aksrt-forum-topic__pay-price {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
padding: 6px 16px;
|
||||
background: #fff7e6;
|
||||
color: #d46b08;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__unlock-feedback {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__visibility-extra select[multiple] {
|
||||
min-height: 90px;
|
||||
}
|
||||
|
||||
.aksrt-forum-compose__visibility-extra input[type="number"] {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* ── adopted answer (§7.3) ── */
|
||||
|
||||
.aksrt-forum-reply.is-adopted {
|
||||
border: 2px solid var(--color-success, #52c41a);
|
||||
border-radius: var(--radius-m, 8px);
|
||||
padding: 12px;
|
||||
background: #f6ffed;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__adopted-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: var(--color-success, #52c41a);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__adopt-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--color-success, #52c41a);
|
||||
border-radius: var(--radius-s, 4px);
|
||||
background: transparent;
|
||||
color: var(--color-success, #52c41a);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__adopt-btn:hover:not(:disabled) {
|
||||
background: var(--color-success, #52c41a);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__adopt-btn:disabled,
|
||||
.aksrt-forum-reply__adopt-btn.is-adopted {
|
||||
opacity: .6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── moderator role badge (§7.5) ── */
|
||||
|
||||
.aksrt-forum-reply__role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__role-badge--admin {
|
||||
background: #fff1f0;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__role-badge--mod {
|
||||
background: #e6f7ff;
|
||||
color: #096dd9;
|
||||
}
|
||||
|
||||
/* ── §7.6 forum comment engine ── */
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/* --- like button --- */
|
||||
|
||||
.aksrt-forum-reply__like-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--c-text-3, #999);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: color .2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__like-btn:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__like-btn.is-active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__like-count {
|
||||
font-size: 12px;
|
||||
min-width: 10px;
|
||||
}
|
||||
|
||||
/* --- nested reply (楼中楼) --- */
|
||||
|
||||
.aksrt-forum-reply--nested {
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__children {
|
||||
margin-top: 8px;
|
||||
padding: 8px 0 0 12px;
|
||||
border-left: 2px solid var(--c-border, #f0f0f0);
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__children .aksrt-forum-reply {
|
||||
padding: 8px 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid var(--c-border-light, #f5f5f5);
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__children .aksrt-forum-reply:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__avatar--sm img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__reply-to {
|
||||
font-size: 12px;
|
||||
color: var(--c-text-3, #999);
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/* --- load more --- */
|
||||
|
||||
.aksrt-forum-replies__load-more {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__more-btn {
|
||||
padding: 8px 24px;
|
||||
border: 1px solid var(--c-border, #d9d9d9);
|
||||
border-radius: var(--radius, 6px);
|
||||
background: none;
|
||||
color: var(--c-text-2, #666);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__more-btn:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/* Quick post bar */
|
||||
|
||||
.aksrt-forum-quick-post {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-quick-post__input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--c-border, #e8e8e8);
|
||||
border-radius: var(--radius, 6px);
|
||||
background: var(--c-fill-1, #fafafa);
|
||||
color: var(--c-text-3, #999);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
transition: border-color .2s, box-shadow .2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-quick-post__input:hover {
|
||||
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-forum-quick-post__input img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/* --- reply action buttons --- */
|
||||
|
||||
.aksrt-forum-reply__reply-btn,
|
||||
.aksrt-forum-reply__top-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--c-text-3, #999);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: color .2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__reply-btn:hover,
|
||||
.aksrt-forum-reply__top-btn:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── §7.3 Hidden content block [aksrt_hide] ── */
|
||||
|
||||
.aksrt-forum-hide {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-hide--locked {
|
||||
padding: 16px;
|
||||
border: 1px dashed var(--c-border, #d9d9d9);
|
||||
border-radius: var(--radius, 6px);
|
||||
background: var(--c-fill-1, #fafafa);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.aksrt-forum-hide__message {
|
||||
font-size: 13px;
|
||||
color: var(--c-text-3, #999);
|
||||
}
|
||||
|
||||
.aksrt-forum-hide__message i {
|
||||
margin-right: 4px;
|
||||
color: var(--c-warning, #faad14);
|
||||
}
|
||||
|
||||
.aksrt-forum-hide--revealed {
|
||||
padding: 12px 16px;
|
||||
border-left: 3px solid var(--c-success, #52c41a);
|
||||
background: var(--c-fill-1, #f6ffed);
|
||||
border-radius: 0 var(--radius, 6px) var(--radius, 6px) 0;
|
||||
}
|
||||
|
||||
/* ── §7.10 Forum home tab engine ── */
|
||||
|
||||
.aksrt-forum-home-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 0 12px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--c-border, #e8e8e8);
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.aksrt-forum-home-tabs__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--c-border, #d9d9d9);
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
background: none;
|
||||
color: var(--c-text-3, #999);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-home-tabs__btn:hover {
|
||||
color: var(--c-text-1, #333);
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
}
|
||||
|
||||
.aksrt-forum-home-tabs__btn.is-active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
}
|
||||
|
||||
.aksrt-forum-home-tabs__badge {
|
||||
display: inline-block;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.aksrt-forum-home__load-more {
|
||||
text-align: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/* --- reply header tabs & filters --- */
|
||||
|
||||
.aksrt-forum-replies__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--c-border, #e8e8e8);
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__tab {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--c-border, #d9d9d9);
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
background: none;
|
||||
color: var(--c-text-3, #999);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__tab:hover {
|
||||
color: var(--c-text-1, #333);
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__tab.is-active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--c-text-3, #999);
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__filter input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__empty {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: var(--c-text-3, #bbb);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__empty i {
|
||||
font-size: 24px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
/* ============================================================
|
||||
* Topic detail page
|
||||
* ============================================================ */
|
||||
|
||||
.aksrt-forum-topic {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
gap: 24px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__shell {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar-card {
|
||||
padding: 18px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--main-radius, var(--radius, 8px));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar-card h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar-link span {
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar-author img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sidebar-author strong,
|
||||
.aksrt-forum-topic__board-link {
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__board-link-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
.aksrt-forum-topic__breadcrumb {
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__breadcrumb a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__sep {
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--color-ink);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--sticky {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--sticky-super {
|
||||
background: var(--focus-color, #fd53a1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--sticky-global {
|
||||
background: #e53e3e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--featured {
|
||||
background: var(--color-success, #52c41a);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--locked {
|
||||
background: #fff7e6;
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--visibility {
|
||||
background: var(--surface-2, #f5f7fa);
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__author img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__quick-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__quick-action,
|
||||
.aksrt-forum-topic__primary-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 16px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-1, #fff);
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__primary-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__taxonomy {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-2, #f5f7fa);
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__chip--special {
|
||||
background: #fff1f0;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__content {
|
||||
padding: 22px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--radius-2xl, 16px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.8;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__content img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote,
|
||||
.aksrt-forum-topic__moderation {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--radius-2xl, 16px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-head,
|
||||
.aksrt-forum-topic__moderation-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-head h2,
|
||||
.aksrt-forum-topic__moderation-head h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-head span,
|
||||
.aksrt-forum-topic__moderation-head p {
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-options {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-option {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: var(--surface-1, #f8f9fa);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-option.is-selected {
|
||||
outline: none;
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 12%, transparent));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-count {
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__vote-actions,
|
||||
.aksrt-forum-topic__moderation-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__moderation-grid,
|
||||
.aksrt-forum-compose__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__field select,
|
||||
.aksrt-forum-topic__field textarea,
|
||||
.aksrt-forum-compose__field input[type="text"],
|
||||
.aksrt-forum-compose__field textarea,
|
||||
.aksrt-forum-compose__field select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__field label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__field select,
|
||||
.aksrt-forum-topic__field textarea {
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__denied {
|
||||
padding: 56px 24px;
|
||||
text-align: center;
|
||||
background: var(--surface-1, #fff);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__denied i {
|
||||
font-size: 42px;
|
||||
margin-bottom: 14px;
|
||||
color: var(--color-ink-4, #8a909c);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__replies {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__replies-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__reply-form {
|
||||
padding: 20px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: none;
|
||||
border-radius: var(--radius-2xl, 16px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__reply-form h2,
|
||||
.aksrt-forum-topic__reply-form h3 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__reply-field textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-line, #d9d9d9);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__reply-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__reply-btn:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__locked-notice,
|
||||
.aksrt-forum-topic__login-notice {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--color-ink-3, #8a909c);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.aksrt-forum-topic {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__login-notice a {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/* Board cards */
|
||||
.aksrt-forum-boards__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: 1px solid var(--color-line, #e8e8e8);
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: var(--color-primary-soft, #e6f4ff);
|
||||
color: var(--color-primary);
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__desc {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-ink-3, #999);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__meta {
|
||||
font-size: 11px;
|
||||
color: var(--color-ink-3, #999);
|
||||
}
|
||||
|
||||
.aksrt-forum-board-card__meta i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/* Topic rows */
|
||||
.aksrt-forum-topics__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--surface-1, #fff);
|
||||
border: 1px solid var(--color-line, #e8e8e8);
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row:hover {
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__avatar img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__title a {
|
||||
color: var(--color-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__title a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 11px;
|
||||
color: var(--color-ink-3, #999);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-row__meta i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, """) + '">' +
|
||||
(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();
|
||||
})();
|
||||
@@ -367,8 +367,6 @@
|
||||
}
|
||||
|
||||
function payApiBase(apiKind) {
|
||||
if (apiKind === 'topic') return '/pay/topic/';
|
||||
if (apiKind === 'plate_follow') return '/pay/plate-follow/';
|
||||
return '/pay/content/';
|
||||
}
|
||||
|
||||
@@ -448,34 +446,15 @@
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
// Content free: reload. Topic/plate free: auto free-order POST (VIP free / free-after-coupon).
|
||||
if (preview.is_free && preview.pay_type !== 'topic' && preview.pay_type !== 'plate_follow') {
|
||||
// Content free: reload.
|
||||
if (preview.is_free) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const isPoints = preview.pay_modo === 'points';
|
||||
const priceType = (preview._price_type || preview.price_type || '');
|
||||
const apiKind = preview._api_kind
|
||||
|| (preview.pay_type === 'topic' ? 'topic' : (preview.pay_type === 'plate_follow' ? 'plate_follow' : 'content'));
|
||||
|
||||
// Auto-submit free topic/plate orders so user doesn't click through empty cash UI.
|
||||
if (preview.is_free && (apiKind === 'topic' || apiKind === 'plate_follow') && !isPoints) {
|
||||
open(
|
||||
'<h3 class="aksrt-dl-modal__title">' + tt('confirm_buy', '确认购买') + '</h3>' +
|
||||
'<p class="aksrt-dl-modal__msg">' + tt('processing', '处理中…') + '</p>',
|
||||
returnFocusEl
|
||||
);
|
||||
const freeBtn = document.createElement('button');
|
||||
freeBtn.type = 'button';
|
||||
freeBtn.style.display = 'none';
|
||||
freeBtn.setAttribute('data-price-type', priceType || '');
|
||||
document.body.appendChild(freeBtn);
|
||||
const freeCode = String(preview.coupon_code || preview._coupon_code || '');
|
||||
submitPurchase(preview.post_id, 'cash', 'balance', freeBtn, returnFocusEl, freeCode, apiKind);
|
||||
try { freeBtn.remove(); } catch (e0) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
const apiKind = preview._api_kind || 'content';
|
||||
|
||||
let amountLabel = escapeHtml(preview.amount_label || '');
|
||||
let baseLabel = escapeHtml(preview.base_label || '');
|
||||
@@ -697,22 +676,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const topicBuyBtn = e.target.closest('[data-aksrt-pay-topic]');
|
||||
if (topicBuyBtn) {
|
||||
e.preventDefault();
|
||||
const topicId = parseInt(topicBuyBtn.getAttribute('data-aksrt-pay-topic') || '0', 10);
|
||||
if (topicId) openCashier(topicId, topicBuyBtn, '', '', 'topic');
|
||||
return;
|
||||
}
|
||||
|
||||
const plateFollowBuyBtn = e.target.closest('[data-aksrt-pay-plate-follow]');
|
||||
if (plateFollowBuyBtn) {
|
||||
e.preventDefault();
|
||||
const plateId = parseInt(plateFollowBuyBtn.getAttribute('data-aksrt-pay-plate-follow') || '0', 10);
|
||||
if (plateId) openCashier(plateId, plateFollowBuyBtn, '', '', 'plate_follow');
|
||||
return;
|
||||
}
|
||||
|
||||
const buyBtn = e.target.closest('[data-action="aksrt-need-purchase"], [data-aksrt-pay-content]');
|
||||
if (buyBtn) {
|
||||
e.preventDefault();
|
||||
@@ -743,7 +706,7 @@
|
||||
requestDownload(btn);
|
||||
});
|
||||
|
||||
// Bridge for forum-governance plate money cashier.
|
||||
// Public cashier bridge (content pay).
|
||||
window.aksrtOpenCashier = function (postId, returnFocusEl, priceType, couponCode, apiKind) {
|
||||
openCashier(postId, returnFocusEl || null, priceType || '', couponCode || '', apiKind || 'content');
|
||||
};
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
closestPager(link);
|
||||
if (!container) {
|
||||
// Fall back: parent stream list.
|
||||
container = link.closest(".aksrt-product-grid, .aksrt-forum-topics__list, [data-aksrt-ajax-list]");
|
||||
container = link.closest("[data-aksrt-ajax-list]");
|
||||
}
|
||||
if (!container) return Promise.resolve();
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* AKSRT Forum Edit — 帖子编辑页交互
|
||||
*
|
||||
* 处理论坛帖子编辑页的表单增强和标签输入。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var form = document.querySelector('.aksrt-forum-edit__form');
|
||||
if (!form) return;
|
||||
|
||||
// 标签输入增强:逗号分隔转标签 chips
|
||||
var tagInput = form.querySelector('#topic_tags');
|
||||
if (tagInput) {
|
||||
tagInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交确认
|
||||
form.addEventListener('submit', function (e) {
|
||||
var title = form.querySelector('#topic_title');
|
||||
var content = form.querySelector('#topic_content');
|
||||
|
||||
if (title && title.value.trim() === '') {
|
||||
e.preventDefault();
|
||||
alert('标题不能为空');
|
||||
title.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// TinyMCE 检查
|
||||
if (typeof tinyMCE !== 'undefined' && tinyMCE.get('topic_content')) {
|
||||
var editorContent = tinyMCE.get('topic_content').getContent().trim();
|
||||
if (editorContent === '') {
|
||||
e.preventDefault();
|
||||
alert('内容不能为空');
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
+1
-226
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
})();
|
||||
@@ -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 = "网络错误,请稍后重试。";
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -156,73 +156,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Forum multi-tab topics.
|
||||
function initForumTabTopics() {
|
||||
document.querySelectorAll('.aksrt-ftab[data-aksrt-ftab]').forEach(function (root) {
|
||||
if (root.dataset.aksrtFtabBound === '1') return;
|
||||
root.dataset.aksrtFtabBound = '1';
|
||||
var rest = root.getAttribute('data-rest') || '';
|
||||
var count = root.getAttribute('data-count') || '6';
|
||||
var style = root.getAttribute('data-style') || 'mini';
|
||||
var plate = root.getAttribute('data-plate') || '0';
|
||||
var btns = [].slice.call(root.querySelectorAll('.aksrt-ftab__btn'));
|
||||
var panels = [].slice.call(root.querySelectorAll('.aksrt-ftab__panel'));
|
||||
|
||||
function load(panel, orderby) {
|
||||
if (!panel || panel.getAttribute('data-loaded') === '1' || !rest) return;
|
||||
var qs = new URLSearchParams({
|
||||
page: '1',
|
||||
count: count,
|
||||
orderby: orderby || 'date',
|
||||
style: style,
|
||||
plate: plate,
|
||||
period: 'all',
|
||||
paginate: 'none',
|
||||
});
|
||||
var url = rest + (rest.indexOf('?') > -1 ? '&' : '?') + qs.toString();
|
||||
panel.classList.add('is-ajax-loading');
|
||||
fetch(url, { credentials: 'same-origin', headers: restHeaders() })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (j) {
|
||||
var d = (j && j.data) || j || {};
|
||||
var html = d.html || '';
|
||||
panel.innerHTML = html || '<div class="ajax-item aksrt-ftab__empty muted-2-color">暂无内容</div>';
|
||||
panel.setAttribute('data-loaded', '1');
|
||||
panel.classList.remove('is-ajax-loading');
|
||||
})
|
||||
.catch(function () {
|
||||
panel.classList.remove('is-ajax-loading');
|
||||
});
|
||||
}
|
||||
|
||||
function go(i) {
|
||||
btns.forEach(function (b, idx) {
|
||||
var on = idx === i;
|
||||
b.classList.toggle('is-active', on);
|
||||
b.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||
});
|
||||
panels.forEach(function (p, idx) {
|
||||
var on = idx === i;
|
||||
p.classList.toggle('is-active', on);
|
||||
if (on) {
|
||||
p.removeAttribute('hidden');
|
||||
} else {
|
||||
p.setAttribute('hidden', '');
|
||||
}
|
||||
});
|
||||
var btn = btns[i];
|
||||
var panel = panels[i];
|
||||
if (btn && panel) load(panel, btn.getAttribute('data-orderby') || 'date');
|
||||
}
|
||||
|
||||
btns.forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
go(parseInt(b.getAttribute('data-index') || '0', 10));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Product multi-tab.
|
||||
function initProductTabs() {
|
||||
document.querySelectorAll('[data-aksrt-product-tab]').forEach(function (root) {
|
||||
@@ -383,86 +316,6 @@
|
||||
}
|
||||
|
||||
|
||||
function initModeratorApplyModal() {
|
||||
document.addEventListener('click', function (e) {
|
||||
var openBtn = e.target.closest('[data-aksrt-mod-apply-open]');
|
||||
if (openBtn) {
|
||||
e.preventDefault();
|
||||
var id = openBtn.getAttribute('data-aksrt-mod-apply-open');
|
||||
var modal = id ? document.getElementById(id) : null;
|
||||
if (modal) {
|
||||
modal.hidden = false;
|
||||
document.body.classList.add('aksrt-mod-apply-open');
|
||||
}
|
||||
return;
|
||||
}
|
||||
var closeBtn = e.target.closest('[data-aksrt-mod-apply-close]');
|
||||
if (closeBtn) {
|
||||
var box = closeBtn.closest('.aksrt-mod-apply-modal');
|
||||
if (box) {
|
||||
box.hidden = true;
|
||||
document.body.classList.remove('aksrt-mod-apply-open');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.js-aksrt-moderator-apply-form').forEach(function (form) {
|
||||
if (form.dataset.aksrtModApplyBound === '1') return;
|
||||
form.dataset.aksrtModApplyBound = '1';
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var boardId = parseInt(form.getAttribute('data-board-id') || '0', 10);
|
||||
var reasonField = form.querySelector('[name="reason"]');
|
||||
var reason = reasonField ? reasonField.value.trim() : '';
|
||||
var feedback = form.querySelector('.js-aksrt-action-feedback');
|
||||
var submitButton = form.querySelector('button[type="submit"]');
|
||||
if (!boardId || !reason) {
|
||||
if (feedback) {
|
||||
feedback.textContent = '请填写完整的申请理由。';
|
||||
feedback.className = 'js-aksrt-action-feedback mb10 is-error';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (submitButton) submitButton.disabled = true;
|
||||
if (feedback) {
|
||||
feedback.textContent = '正在提交申请…';
|
||||
feedback.className = 'js-aksrt-action-feedback mb10 is-loading';
|
||||
}
|
||||
var siteUrl = (window.aksrtData && aksrtData.siteUrl) ? String(aksrtData.siteUrl).replace(/\/+$/, '') : '';
|
||||
var url = siteUrl + '/wp-json/aksrt/v1/forum/moderator/apply';
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-WP-Nonce': (window.aksrtData && aksrtData.restNonce) || '',
|
||||
},
|
||||
body: JSON.stringify({ board_id: boardId, reason: reason }),
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); })
|
||||
.then(function (res) {
|
||||
if (!res.ok || (res.j && res.j.success === false)) {
|
||||
throw new Error((res.j && res.j.message) || '申请提交失败');
|
||||
}
|
||||
var msg = (res.j && (res.j.message || (res.j.data && res.j.data.message))) || '申请已提交';
|
||||
if (feedback) {
|
||||
feedback.textContent = msg;
|
||||
feedback.className = 'js-aksrt-action-feedback mb10 is-success';
|
||||
}
|
||||
if (reasonField) reasonField.disabled = true;
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (feedback) {
|
||||
feedback.textContent = (err && err.message) || '申请提交失败。';
|
||||
feedback.className = 'js-aksrt-action-feedback mb10 is-error';
|
||||
}
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initFloatingQRCodes() {
|
||||
var wrap = document.querySelector('.float-right');
|
||||
if (!wrap) return;
|
||||
@@ -508,11 +361,9 @@
|
||||
function init() {
|
||||
initWidgetTabs();
|
||||
initCategoryTabs();
|
||||
initForumTabTopics();
|
||||
initProductTabs();
|
||||
initMainPostsLazy();
|
||||
initFeatureIntro();
|
||||
initModeratorApplyModal();
|
||||
initFloatingQRCodes();
|
||||
}
|
||||
|
||||
|
||||
-10
@@ -396,12 +396,6 @@ if ($floating_enabled) :
|
||||
<i class="fas fa-sun float-btn__icon-sun" aria-hidden="true"></i>
|
||||
<i class="fas fa-moon float-btn__icon-moon" aria-hidden="true"></i>
|
||||
</a>
|
||||
<?php elseif ($item_type === 'shop_cart') :
|
||||
$cart_html = function_exists('aksrt_get_float_shop_cart_html') ? aksrt_get_float_shop_cart_html($item) : '';
|
||||
if ($cart_html !== '') {
|
||||
echo $cart_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
?>
|
||||
<?php elseif ($item_type === 'pay_vip') :
|
||||
$vip_html = function_exists('aksrt_get_float_pay_vip_html') ? aksrt_get_float_pay_vip_html($item) : '';
|
||||
if ($vip_html !== '') {
|
||||
@@ -490,10 +484,6 @@ if ($floating_enabled) :
|
||||
<?php endforeach;
|
||||
// After loop: inject missing shop cart once. Never auto-inject pay_vip
|
||||
// (admin may already add one; previous in-loop inject created duplicates).
|
||||
$float_types = array_column($floating_items, 'type');
|
||||
if (!in_array('shop_cart', $float_types, true) && function_exists('aksrt_get_float_shop_cart_html')) {
|
||||
echo aksrt_get_float_shop_cart_html(['icon' => 'fas fa-shopping-cart']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
+2
-66
@@ -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) {
|
||||
@@ -211,12 +211,6 @@ if (class_exists('\AKSRT\Content\Download')) {
|
||||
if (class_exists('\AKSRT\Content\Pay')) {
|
||||
\AKSRT\Content\Pay::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Forum\TopicPay')) {
|
||||
\AKSRT\Forum\TopicPay::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Forum\PlateFollowPay')) {
|
||||
\AKSRT\Forum\PlateFollowPay::get_instance()->init();
|
||||
}
|
||||
if (class_exists('\AKSRT\Content\VipService')) {
|
||||
\AKSRT\Content\VipService::get_instance()->init();
|
||||
}
|
||||
@@ -324,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();
|
||||
@@ -355,10 +345,7 @@ if (class_exists('\AKSRT\Payment\Gateway')) {
|
||||
do_action('aksrt_register_payment_gateways', $gw);
|
||||
}
|
||||
|
||||
if (class_exists('\AKSRT\Forum\Forum')) {
|
||||
\AKSRT\Forum\Forum::get_instance()->init();
|
||||
}
|
||||
if (is_admin() && class_exists('\AKSRT\Core\ResourceMetaBox')) {
|
||||
if (class_exists('\AKSRT\Core\ResourceMetaBox')) {
|
||||
\AKSRT\Core\ResourceMetaBox::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -367,51 +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();
|
||||
}
|
||||
|
||||
// 论坛版块版主 meta box
|
||||
if (is_admin() && class_exists('\AKSRT\Forum\PlateMetaBox')) {
|
||||
\AKSRT\Forum\PlateMetaBox::get_instance()->init();
|
||||
}
|
||||
// 论坛帖子原生列表治理列/筛选/批量
|
||||
if (is_admin() && class_exists('\AKSRT\Forum\TopicAdminList')) {
|
||||
\AKSRT\Forum\TopicAdminList::get_instance()->init();
|
||||
}
|
||||
// 论坛帖子治理 meta box
|
||||
if (is_admin() && class_exists('\AKSRT\Forum\TopicMetaBox')) {
|
||||
\AKSRT\Forum\TopicMetaBox::get_instance()->init();
|
||||
}
|
||||
// 论坛版块列表列
|
||||
if (is_admin() && class_exists('\AKSRT\Forum\PlateAdminList')) {
|
||||
\AKSRT\Forum\PlateAdminList::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();
|
||||
@@ -464,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();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-archive.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-archive-filters.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-comments.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-forum.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-forum-widgets.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-slider.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-vip.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-auth-ui.php';
|
||||
|
||||
@@ -83,339 +83,4 @@ function aksrt_register_topics_taxonomy() {
|
||||
}
|
||||
add_action('init', 'aksrt_register_topics_taxonomy');
|
||||
|
||||
// ============================================================
|
||||
// 注册自定义文章类型 - Forum Topic & 分类法 - Forum Board
|
||||
// ============================================================
|
||||
|
||||
function aksrt_register_forum_cpt() {
|
||||
// Respect bbs_s switch — skip CPT registration when forum is disabled.
|
||||
// Admin context always registers so the backend settings page works.
|
||||
if (!is_admin() && class_exists('\AKSRT\Settings\Settings')) {
|
||||
$forum = \AKSRT\Settings\Settings::get_forum_settings();
|
||||
if (empty($forum['enabled'])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Read custom slugs / names from settings when available
|
||||
$plate_slug = 'board';
|
||||
$topic_slug = 'topic';
|
||||
$forum_enabled = true;
|
||||
$forum_settings = [];
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$forum_settings = \AKSRT\Settings\Settings::get_forum_settings();
|
||||
$plate_slug = !empty($forum_settings['other']['plate_slug']) ? $forum_settings['other']['plate_slug'] : 'board';
|
||||
$topic_slug = !empty($forum_settings['other']['posts_slug']) ? $forum_settings['other']['posts_slug'] : 'topic';
|
||||
$forum_enabled = !empty($forum_settings['enabled']);
|
||||
}
|
||||
// Admin always registers types; menu follows bbs_s (zibll: show when forum on).
|
||||
$forum_show_menu = is_admin() ? $forum_enabled : false;
|
||||
$names = !empty($forum_settings['names']) ? $forum_settings['names'] : [];
|
||||
$forum_label = sanitize_text_field((string) ($names['forum'] ?? __('论坛', 'aksrt')));
|
||||
$posts_label = sanitize_text_field((string) ($names['posts'] ?? __('帖子', 'aksrt')));
|
||||
$topic_label = sanitize_text_field((string) ($names['topic'] ?? __('话题', 'aksrt')));
|
||||
$tag_label = sanitize_text_field((string) ($names['tag'] ?? __('标签', 'aksrt')));
|
||||
// Legacy board taxonomy: keep rewrite/bridge, hide from admin (plate CPT is canonical).
|
||||
register_taxonomy('aksrt_forum_board', 'aksrt_forum_topic', [
|
||||
'labels' => [
|
||||
'name' => __('论坛版块', 'aksrt'),
|
||||
'singular_name' => __('版块', 'aksrt'),
|
||||
'search_items' => __('搜索版块', 'aksrt'),
|
||||
'all_items' => __('所有版块', 'aksrt'),
|
||||
'edit_item' => __('编辑版块', 'aksrt'),
|
||||
'add_new_item' => __('添加版块', 'aksrt'),
|
||||
],
|
||||
'hierarchical' => true,
|
||||
'public' => true,
|
||||
'show_ui' => false,
|
||||
'show_in_menu' => false,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => false,
|
||||
'rewrite' => ['slug' => $plate_slug],
|
||||
]);
|
||||
|
||||
// Forum Tag taxonomy
|
||||
register_taxonomy('aksrt_forum_tag', 'aksrt_forum_topic', [
|
||||
'labels' => [
|
||||
'name' => sprintf(/* translators: %s: forum name */ __('%s标签', 'aksrt'), $forum_label),
|
||||
'singular_name' => $tag_label,
|
||||
'search_items' => sprintf(/* translators: %s: tag name */ __('搜索%s', 'aksrt'), $tag_label),
|
||||
'all_items' => sprintf(/* translators: %s: tag name */ __('所有%s', 'aksrt'), $tag_label),
|
||||
'edit_item' => sprintf(/* translators: %s: tag name */ __('编辑%s', 'aksrt'), $tag_label),
|
||||
'add_new_item' => sprintf(/* translators: %s: tag name */ __('添加%s', 'aksrt'), $tag_label),
|
||||
'menu_name' => $tag_label,
|
||||
],
|
||||
'hierarchical' => false,
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => $forum_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => ['slug' => 'forum-tag'],
|
||||
]);
|
||||
|
||||
// Forum Topic taxonomy (话题,单选,区别于标签)
|
||||
register_taxonomy('aksrt_forum_topic_cat', 'aksrt_forum_topic', [
|
||||
'labels' => [
|
||||
'name' => sprintf(/* translators: %s: forum name */ __('%s话题', 'aksrt'), $forum_label),
|
||||
'singular_name' => $topic_label,
|
||||
'search_items' => sprintf(/* translators: %s: topic name */ __('搜索%s', 'aksrt'), $topic_label),
|
||||
'all_items' => sprintf(/* translators: %s: topic name */ __('所有%s', 'aksrt'), $topic_label),
|
||||
'edit_item' => sprintf(/* translators: %s: topic name */ __('编辑%s', 'aksrt'), $topic_label),
|
||||
'add_new_item' => sprintf(/* translators: %s: topic name */ __('添加%s', 'aksrt'), $topic_label),
|
||||
'menu_name' => $topic_label,
|
||||
],
|
||||
'hierarchical' => false,
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => $forum_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => ['slug' => 'forum-topic'],
|
||||
]);
|
||||
|
||||
// Forum Topic CPT — native sidebar (zibll: forum_post @ pos 40)
|
||||
register_post_type('aksrt_forum_topic', [
|
||||
'labels' => [
|
||||
'name' => $forum_label . $posts_label,
|
||||
'singular_name' => $posts_label,
|
||||
'add_new' => sprintf(/* translators: %s: posts name */ __('添加%s', 'aksrt'), $posts_label),
|
||||
'add_new_item' => sprintf(/* translators: %s: posts name */ __('发布新%s', 'aksrt'), $posts_label),
|
||||
'edit_item' => sprintf(/* translators: %s: posts name */ __('编辑%s', 'aksrt'), $posts_label),
|
||||
'view_item' => sprintf(/* translators: %s: posts name */ __('查看%s', 'aksrt'), $posts_label),
|
||||
'all_items' => sprintf(/* translators: %s: posts name */ __('所有%s', 'aksrt'), $posts_label),
|
||||
'menu_name' => $forum_label . $posts_label,
|
||||
],
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => $forum_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'has_archive' => true,
|
||||
'menu_position' => 40,
|
||||
'menu_icon' => 'dashicons-buddicons-topics',
|
||||
'supports' => ['title', 'editor', 'author', 'comments', 'thumbnail', 'excerpt'],
|
||||
'capability_type' => 'post',
|
||||
'rewrite' => ['slug' => $topic_slug],
|
||||
]);
|
||||
}
|
||||
add_action('init', 'aksrt_register_forum_cpt');
|
||||
|
||||
// ============================================================
|
||||
// 注册板块 CPT (aksrt_forum_plate) + 板块分区 taxonomy (aksrt_plate_cat)
|
||||
// §7.0 plate CPT 脚手架:板块从 taxonomy 迁移为独立 CPT,
|
||||
// 支持作者(超级版主)、正文/摘要/封面、层级父子、独立 meta 扩展位。
|
||||
// ============================================================
|
||||
|
||||
function aksrt_register_forum_plate_cpt() {
|
||||
if (!is_admin() && class_exists('\AKSRT\Settings\Settings')) {
|
||||
$forum = \AKSRT\Settings\Settings::get_forum_settings();
|
||||
if (empty($forum['enabled'])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$plate_slug = 'board';
|
||||
$forum_enabled = true;
|
||||
$forum = [];
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$forum = \AKSRT\Settings\Settings::get_forum_settings();
|
||||
$plate_slug = !empty($forum['other']['plate_slug']) ? $forum['other']['plate_slug'] : 'board';
|
||||
$forum_enabled = !empty($forum['enabled']);
|
||||
}
|
||||
$forum_show_menu = is_admin() ? $forum_enabled : false;
|
||||
$names = !empty($forum['names']) ? $forum['names'] : [];
|
||||
$forum_label = sanitize_text_field((string) ($names['forum'] ?? __('论坛', 'aksrt')));
|
||||
$plate_label = sanitize_text_field((string) ($names['plate'] ?? __('版块', 'aksrt')));
|
||||
|
||||
// Plate category taxonomy (分区)
|
||||
register_taxonomy('aksrt_plate_cat', 'aksrt_forum_plate', [
|
||||
'labels' => [
|
||||
'name' => sprintf(/* translators: %s: plate name */ __('%s分区', 'aksrt'), $plate_label),
|
||||
'singular_name' => __('分区', 'aksrt'),
|
||||
'search_items' => __('搜索分区', 'aksrt'),
|
||||
'all_items' => __('所有分区', 'aksrt'),
|
||||
'edit_item' => __('编辑分区', 'aksrt'),
|
||||
'add_new_item' => __('添加分区', 'aksrt'),
|
||||
'menu_name' => __('分区', 'aksrt'),
|
||||
],
|
||||
'hierarchical' => true,
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => $forum_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => ['slug' => 'plate-cat'],
|
||||
]);
|
||||
|
||||
// Plate CPT (板块) — native sidebar (zibll: plate @ pos 40)
|
||||
register_post_type('aksrt_forum_plate', [
|
||||
'labels' => [
|
||||
'name' => $forum_label . $plate_label,
|
||||
'singular_name' => $plate_label,
|
||||
'add_new' => sprintf(/* translators: %s: plate name */ __('添加%s', 'aksrt'), $plate_label),
|
||||
'add_new_item' => sprintf(/* translators: %s: plate name */ __('创建%s', 'aksrt'), $plate_label),
|
||||
'edit_item' => sprintf(/* translators: %s: plate name */ __('编辑%s', 'aksrt'), $plate_label),
|
||||
'view_item' => sprintf(/* translators: %s: plate name */ __('查看%s', 'aksrt'), $plate_label),
|
||||
'search_items' => sprintf(/* translators: %s: plate name */ __('搜索%s', 'aksrt'), $plate_label),
|
||||
'not_found' => sprintf(/* translators: %s: plate name */ __('未找到%s', 'aksrt'), $plate_label),
|
||||
'not_found_in_trash' => sprintf(/* translators: %s: plate name */ __('回收站中未找到%s', 'aksrt'), $plate_label),
|
||||
'all_items' => sprintf(/* translators: %s: plate name */ __('所有%s', 'aksrt'), $plate_label),
|
||||
'menu_name' => $forum_label . $plate_label,
|
||||
],
|
||||
'public' => true,
|
||||
'hierarchical' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => $forum_show_menu,
|
||||
'show_in_rest' => true,
|
||||
'has_archive' => false,
|
||||
'menu_position' => 40,
|
||||
'menu_icon' => 'dashicons-buddicons-forums',
|
||||
'supports' => ['title', 'editor', 'author', 'excerpt', 'thumbnail', 'page-attributes'],
|
||||
'capability_type' => 'post',
|
||||
'rewrite' => ['slug' => $plate_slug, 'with_front' => false],
|
||||
]);
|
||||
|
||||
// Keep aksrt_forum_topic assignable to plates via taxonomy bridge
|
||||
register_taxonomy_for_object_type('aksrt_forum_board', 'aksrt_forum_topic');
|
||||
}
|
||||
add_action('init', 'aksrt_register_forum_plate_cpt');
|
||||
|
||||
// ============================================================
|
||||
// §7.0 forum_post CPT 对齐:注册帖子 meta 扩展位地基
|
||||
// 为 aksrt_forum_topic 注册全部 meta key,提供类型安全 + REST 暴露。
|
||||
// ============================================================
|
||||
|
||||
function aksrt_register_forum_topic_meta() {
|
||||
$post_type = 'aksrt_forum_topic';
|
||||
|
||||
$meta_fields = [
|
||||
// 基础治理 meta (已有,补注册)
|
||||
'_aksrt_visibility' => ['type' => 'string', 'default' => 'public'],
|
||||
'_aksrt_review_status' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_sticky' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_featured' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_locked' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_highlight' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_views' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_moderation_note' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_moderated_by' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_moderated_at' => ['type' => 'string', 'default' => ''],
|
||||
|
||||
// 帖子类型 + 问答状态 (Wave3)
|
||||
'_aksrt_bbs_type' => ['type' => 'string', 'default' => 'standard'],
|
||||
'_aksrt_question_status' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_adopted_comment' => ['type' => 'integer', 'default' => 0],
|
||||
|
||||
// 投票 (已有,补注册)
|
||||
'_aksrt_vote_enabled' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_vote_title' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_vote_multiple' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_vote_type' => ['type' => 'string', 'default' => 'standard'],
|
||||
'_aksrt_vote_expiry_days' => ['type' => 'integer', 'default' => 0],
|
||||
|
||||
// 评分 (Wave3)
|
||||
'_aksrt_score' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_recommend_score' => ['type' => 'number', 'default' => 0],
|
||||
'_aksrt_hot_rewarded' => ['type' => 'integer', 'default' => 0],
|
||||
|
||||
// 收藏 (Wave3)
|
||||
'_aksrt_favorite_count' => ['type' => 'integer', 'default' => 0],
|
||||
|
||||
// 封面扩展位(论坛帖子封面;前台 forum-topic 渲染)
|
||||
'_aksrt_cover_type' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_cover_image' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_featured_slide' => ['type' => 'array', 'default' => []],
|
||||
'_aksrt_featured_video' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_cover_ratio' => ['type' => 'string', 'default' => '16:9'],
|
||||
|
||||
// 付费内容扩展位(论坛帖子付费门槛;前台 forum-topic 渲染)
|
||||
'_aksrt_allow_view' => ['type' => 'string', 'default' => 'public'],
|
||||
'_aksrt_pay_price' => ['type' => 'number', 'default' => 0],
|
||||
'_aksrt_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy single VIP
|
||||
'_aksrt_pay_vip1_price' => ['type' => 'number', 'default' => 0],
|
||||
'_aksrt_pay_vip2_price' => ['type' => 'number', 'default' => 0],
|
||||
'_aksrt_pay_credits' => ['type' => 'integer', 'default' => 0], // legacy alias of credits_cost
|
||||
'_aksrt_credits_cost' => ['type' => 'integer', 'default' => 0], // runtime unlock price
|
||||
'_aksrt_credits_vip1' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_credits_vip2' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_pay_hide_part' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_virtual_sales' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_topic_coupon_s' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_visibility_roles' => ['type' => 'array', 'default' => []],
|
||||
|
||||
// 状态工作流扩展位 (Wave4)
|
||||
'_aksrt_closed' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_hidden' => ['type' => 'integer', 'default' => 0],
|
||||
|
||||
// 板块 CPT 关联 (§7.0 新增)
|
||||
'_aksrt_plate_id' => ['type' => 'integer', 'default' => 0],
|
||||
];
|
||||
|
||||
foreach ($meta_fields as $key => $args) {
|
||||
$schema = [
|
||||
'type' => $args['type'],
|
||||
'description' => $key,
|
||||
'single' => true,
|
||||
'default' => $args['default'],
|
||||
'show_in_rest' => false,
|
||||
];
|
||||
|
||||
if ($args['type'] === 'array') {
|
||||
$schema['show_in_rest'] = false;
|
||||
$schema['type'] = 'array';
|
||||
}
|
||||
|
||||
register_post_meta($post_type, $key, $schema);
|
||||
}
|
||||
|
||||
// Plate CPT meta registration
|
||||
$plate_meta = [
|
||||
'_aksrt_sort_order' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_icon' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_moderators' => ['type' => 'array', 'default' => []],
|
||||
'_aksrt_moderator_roles' => ['type' => 'array', 'default' => []],
|
||||
'_aksrt_posts_count' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_reply_count' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_today_reply_count' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_plate_views' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_follow_count' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_last_reply' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_last_post' => ['type' => 'string', 'default' => ''],
|
||||
'_aksrt_allow_view' => ['type' => 'string', 'default' => 'public'],
|
||||
'_aksrt_add_limit' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_follow_pay' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_follow_pay_modo' => ['type' => 'string', 'default' => 'money'],
|
||||
'_aksrt_follow_pay_price' => ['type' => 'number', 'default' => 0],
|
||||
'_aksrt_follow_pay_points' => ['type' => 'integer', 'default' => 0],
|
||||
'_aksrt_follow_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy read fallback → vip1
|
||||
'_aksrt_follow_pay_vip1_price' => ['type' => 'number', 'default' => 0],
|
||||
'_aksrt_follow_pay_vip2_price' => ['type' => 'number', 'default' => 0],
|
||||
'_aksrt_follow_pay_coupon_s' => ['type' => 'integer', 'default' => 0],
|
||||
];
|
||||
|
||||
foreach ($plate_meta as $key => $args) {
|
||||
register_post_meta('aksrt_forum_plate', $key, [
|
||||
'type' => $args['type'] === 'array' ? 'array' : $args['type'],
|
||||
'description' => $key,
|
||||
'single' => true,
|
||||
'default' => $args['default'],
|
||||
'show_in_rest' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
add_action('init', 'aksrt_register_forum_topic_meta');
|
||||
|
||||
// Include forum topics in frontend search results (§7.10).
|
||||
add_action('pre_get_posts', function ($query) {
|
||||
if (!is_admin() && $query->is_main_query() && $query->is_search()) {
|
||||
$post_types = $query->get('post_type');
|
||||
if (empty($post_types) || $post_types === 'post') {
|
||||
$query->set('post_type', ['post', 'page', 'aksrt_forum_topic']);
|
||||
} elseif (is_array($post_types) && !in_array('aksrt_forum_topic', $post_types, true)) {
|
||||
$post_types[] = 'aksrt_forum_topic';
|
||||
$query->set('post_type', $post_types);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,6 @@ if (!function_exists('aksrt_credits_label')) {
|
||||
'download' => __('下载消费', 'aksrt'),
|
||||
'admin' => __('管理员调整', 'aksrt'),
|
||||
'refund' => __('退款', 'aksrt'),
|
||||
'forum_topic' => __('论坛发帖', 'aksrt'),
|
||||
'forum_reply' => __('论坛回复', 'aksrt'),
|
||||
'free_sign_up' => __('注册奖励', 'aksrt'),
|
||||
'free_sign_in' => __('每日签到', 'aksrt'),
|
||||
'free_login' => __('每日登录', 'aksrt'),
|
||||
@@ -58,13 +56,6 @@ if (!function_exists('aksrt_credits_label')) {
|
||||
'free_post_favorite'=> __('文章被收藏', 'aksrt'),
|
||||
'free_comment_new'=> __('发表评论', 'aksrt'),
|
||||
'free_comment_like'=> __('评论被赞', 'aksrt'),
|
||||
'free_bbs_posts_new'=> __('论坛发帖', 'aksrt'),
|
||||
'free_bbs_reply'=> __('论坛回复', 'aksrt'),
|
||||
'free_bbs_essence'=> __('帖子加精', 'aksrt'),
|
||||
'free_bbs_adopt'=> __('回复被采纳', 'aksrt'),
|
||||
'free_bbs_posts_hot'=> __('热帖奖励', 'aksrt'),
|
||||
'free_bbs_comment_hot'=> __('神评奖励', 'aksrt'),
|
||||
'free_bbs_score_extra'=> __('帖子加分', 'aksrt'),
|
||||
];
|
||||
if (isset($map[$reason])) {
|
||||
return $map[$reason];
|
||||
|
||||
@@ -1,363 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Shared forum widget renderers (zibll BBS posts/plate mini parity).
|
||||
*
|
||||
* Used by WidgetForumTopicLists, WidgetForumTabTopics, WidgetForumPlateLists,
|
||||
* WidgetForumBoardList, WidgetForumPlateModerator.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_forum_topic_meta_flags')) :
|
||||
/**
|
||||
* @return array{topping:bool,essence:bool,views:int,replies:int,plate_id:int}
|
||||
*/
|
||||
function aksrt_forum_topic_meta_flags(int $post_id): array {
|
||||
return [
|
||||
'topping' => (string) get_post_meta($post_id, '_aksrt_topping', true) === '1'
|
||||
|| (string) get_post_meta($post_id, '_aksrt_is_top', true) === '1',
|
||||
'essence' => (string) get_post_meta($post_id, '_aksrt_essence', true) === '1',
|
||||
'views' => (int) get_post_meta($post_id, '_aksrt_views', true),
|
||||
'replies' => (int) get_comments_number($post_id),
|
||||
'plate_id' => (int) get_post_meta($post_id, '_aksrt_plate_id', true),
|
||||
];
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_forum_topic_badges_html')) :
|
||||
/**
|
||||
* Topping / essence badges for forum topic rows.
|
||||
*/
|
||||
function aksrt_forum_topic_badges_html(int $post_id, bool $show_topping = true): string {
|
||||
$flags = aksrt_forum_topic_meta_flags($post_id);
|
||||
$html = '';
|
||||
if ($show_topping && !empty($flags['topping'])) {
|
||||
$html .= '<badge class="img-badge left jb-red"><i>' . esc_html__('置顶', 'aksrt') . '</i></badge>';
|
||||
}
|
||||
if (!empty($flags['essence'])) {
|
||||
$html .= '<badge class="img-badge right jb-yellow"><i>' . esc_html__('精华', 'aksrt') . '</i></badge>';
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_forum_topic_mini')) :
|
||||
/**
|
||||
* Mini forum topic row (zibll zib_bbs_get_posts_mini_list parity).
|
||||
*
|
||||
* @param int|null $post_id Null = current post in loop.
|
||||
* @param string $class Extra classes on outer element.
|
||||
* @param bool $show_topping Whether to show topping badge.
|
||||
*/
|
||||
function aksrt_render_forum_topic_mini(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string {
|
||||
$post_id = $post_id ?: (int) get_the_ID();
|
||||
if ($post_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$post = get_post($post_id);
|
||||
if (!$post || $post->post_status !== 'publish') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$author_id = (int) $post->post_author;
|
||||
$flags = aksrt_forum_topic_meta_flags($post_id);
|
||||
$permalink = get_permalink($post_id);
|
||||
$title = get_the_title($post_id);
|
||||
|
||||
$avatar_url = function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($author_id, ['size' => 48])
|
||||
: get_avatar_url($author_id, ['size' => 48]);
|
||||
$home = function_exists('aksrt_get_user_home_url')
|
||||
? aksrt_get_user_home_url($author_id)
|
||||
: get_author_posts_url($author_id);
|
||||
|
||||
$name = function_exists('aksrt_get_user_name')
|
||||
? aksrt_get_user_name([
|
||||
'id' => $author_id,
|
||||
'class' => 'flex ac flex1',
|
||||
'follow' => false,
|
||||
'medal' => false,
|
||||
'level' => false,
|
||||
])
|
||||
: '<a href="' . esc_url($home) . '">' . esc_html(get_the_author_meta('display_name', $author_id)) . '</a>';
|
||||
|
||||
// Prefer last reply time when available.
|
||||
$time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true);
|
||||
if ($time_raw === '' || !strtotime($time_raw)) {
|
||||
$time_raw = (string) $post->post_date;
|
||||
}
|
||||
$time_txt = function_exists('aksrt_time_ago')
|
||||
? aksrt_time_ago($time_raw)
|
||||
: human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt');
|
||||
|
||||
$views_txt = function_exists('aksrt_format_number')
|
||||
? aksrt_format_number($flags['views'])
|
||||
: (string) $flags['views'];
|
||||
|
||||
$cls = trim('forum-posts mini aksrt-ftopic-mini relative ' . $class);
|
||||
$html = '<posts class="' . esc_attr($cls) . '">';
|
||||
$html .= aksrt_forum_topic_badges_html($post_id, $show_topping);
|
||||
$html .= '<div class="forum-posts-header flex ac">';
|
||||
$html .= '<a class="avatar-img mr6" href="' . esc_url($home) . '"><img class="avatar aksrt-lazy lazyload" data-src="' . esc_url($avatar_url) . '" src="' . esc_url(get_template_directory_uri() . '/assets/img/placeholder.svg') . '" alt="" loading="lazy" width="28" height="28" /></a>';
|
||||
$html .= '<div class="flex1 min-w0">';
|
||||
$html .= '<div class="forum-title text-ellipsis"><a class="icon-circle" href="' . esc_url($permalink) . '">' . esc_html($title) . '</a></div>';
|
||||
$html .= '<div class="item-meta em09 muted-2-color flex ac flex-wrap">';
|
||||
$html .= '<span class="forum-user-name">' . $name . '</span>';
|
||||
$html .= '<span class="ml6">' . esc_html($time_txt) . '</span>';
|
||||
$html .= '<span class="ml6"><i class="fa fa-eye" aria-hidden="true"></i> ' . esc_html((string) $views_txt) . '</span>';
|
||||
if ($flags['replies'] > 0) {
|
||||
$html .= '<span class="ml6"><i class="fa fa-comment-o" aria-hidden="true"></i> ' . esc_html((string) $flags['replies']) . '</span>';
|
||||
}
|
||||
$html .= '</div></div></div></posts>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_forum_topic_detail')) :
|
||||
/**
|
||||
* Detail forum topic row (richer entry meta).
|
||||
*
|
||||
* @param int|null $post_id
|
||||
* @param string $class
|
||||
* @param bool $show_topping
|
||||
*/
|
||||
function aksrt_render_forum_topic_detail(?int $post_id = null, string $class = 'ajax-item', bool $show_topping = true): string {
|
||||
$post_id = $post_id ?: (int) get_the_ID();
|
||||
if ($post_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$post = get_post($post_id);
|
||||
if (!$post || $post->post_status !== 'publish') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$author_id = (int) $post->post_author;
|
||||
$flags = aksrt_forum_topic_meta_flags($post_id);
|
||||
$permalink = get_permalink($post_id);
|
||||
$title = get_the_title($post_id);
|
||||
$excerpt = wp_trim_words(wp_strip_all_tags((string) $post->post_content), 36, '…');
|
||||
|
||||
$avatar_url = function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($author_id, ['size' => 64])
|
||||
: get_avatar_url($author_id, ['size' => 64]);
|
||||
$home = function_exists('aksrt_get_user_home_url')
|
||||
? aksrt_get_user_home_url($author_id)
|
||||
: get_author_posts_url($author_id);
|
||||
|
||||
$name = function_exists('aksrt_get_user_name')
|
||||
? aksrt_get_user_name([
|
||||
'id' => $author_id,
|
||||
'class' => 'flex ac',
|
||||
'follow' => false,
|
||||
'medal' => true,
|
||||
'level' => true,
|
||||
])
|
||||
: '<a href="' . esc_url($home) . '">' . esc_html(get_the_author_meta('display_name', $author_id)) . '</a>';
|
||||
|
||||
$time_raw = (string) get_post_meta($post_id, '_aksrt_last_reply_time', true);
|
||||
if ($time_raw === '' || !strtotime($time_raw)) {
|
||||
$time_raw = (string) $post->post_date;
|
||||
}
|
||||
$time_txt = function_exists('aksrt_time_ago')
|
||||
? aksrt_time_ago($time_raw)
|
||||
: human_time_diff(strtotime($time_raw), current_time('timestamp')) . __('前', 'aksrt');
|
||||
|
||||
$views_txt = function_exists('aksrt_format_number')
|
||||
? aksrt_format_number($flags['views'])
|
||||
: (string) $flags['views'];
|
||||
|
||||
// Optional plate name chip.
|
||||
$plate_chip = '';
|
||||
if ($flags['plate_id'] > 0) {
|
||||
$plate = get_post($flags['plate_id']);
|
||||
if ($plate instanceof WP_Post) {
|
||||
$plate_chip = '<a class="but p2-10 em09 mr6" href="' . esc_url(get_permalink($plate)) . '">' . esc_html(get_the_title($plate)) . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
$cls = trim('forum-posts detail aksrt-ftopic-detail relative ' . $class);
|
||||
$html = '<posts class="' . esc_attr($cls) . '">';
|
||||
$html .= aksrt_forum_topic_badges_html($post_id, $show_topping);
|
||||
$html .= '<div class="forum-posts-header flex ac">';
|
||||
$html .= '<a class="avatar-img mr10" href="' . esc_url($home) . '"><img class="avatar aksrt-lazy lazyload" data-src="' . esc_url($avatar_url) . '" src="' . esc_url(get_template_directory_uri() . '/assets/img/placeholder.svg') . '" alt="" loading="lazy" width="40" height="40" /></a>';
|
||||
$html .= '<div class="flex1 min-w0">' . $name . '<div class="em09 muted-2-color mt3">' . esc_html($time_txt) . '</div></div>';
|
||||
$html .= '</div>';
|
||||
$html .= '<h2 class="forum-title mt10"><a href="' . esc_url($permalink) . '">' . esc_html($title) . '</a></h2>';
|
||||
if ($excerpt !== '') {
|
||||
$html .= '<div class="forum-excerpt muted-2-color em09 mt6">' . esc_html($excerpt) . '</div>';
|
||||
}
|
||||
$html .= '<div class="item-meta flex ac flex-wrap mt10 em09 muted-2-color">';
|
||||
$html .= $plate_chip;
|
||||
$html .= '<span class="mr10"><i class="fa fa-eye" aria-hidden="true"></i> ' . esc_html((string) $views_txt) . '</span>';
|
||||
$html .= '<span class="mr10"><i class="fa fa-comment-o" aria-hidden="true"></i> ' . esc_html((string) $flags['replies']) . '</span>';
|
||||
$html .= '</div></posts>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_plate_thumb_html')) :
|
||||
/**
|
||||
* Shared plate thumb with lazy pattern.
|
||||
*/
|
||||
function aksrt_render_plate_thumb_html(int $plate_id, string $size = 'thumbnail'): string {
|
||||
$thumb = get_the_post_thumbnail_url($plate_id, $size);
|
||||
$icon = (string) get_post_meta($plate_id, '_aksrt_icon', true);
|
||||
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
|
||||
if ($thumb) {
|
||||
return '<span class="plate-thumb aksrt-plate-item__thumb"><img class="aksrt-lazy lazyload fit-cover" data-src="' . esc_url($thumb) . '" src="' . esc_url($ph) . '" alt="" loading="lazy" width="48" height="48" /></span>';
|
||||
}
|
||||
if ($icon !== '') {
|
||||
return '<span class="plate-thumb aksrt-plate-item__thumb is-icon"><i class="' . esc_attr($icon) . '" aria-hidden="true"></i></span>';
|
||||
}
|
||||
return '<span class="plate-thumb aksrt-plate-item__thumb is-empty"><i class="fa fa-comments" aria-hidden="true"></i></span>';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_plate_row')) :
|
||||
/**
|
||||
* Plate list row (zibll plate-item flex).
|
||||
*
|
||||
* @param WP_Post|int $plate
|
||||
*/
|
||||
function aksrt_render_plate_row($plate): string {
|
||||
$post = $plate instanceof WP_Post ? $plate : get_post((int) $plate);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return '';
|
||||
}
|
||||
$id = (int) $post->ID;
|
||||
$url = get_permalink($id);
|
||||
$count = (int) get_post_meta($id, '_aksrt_posts_count', true);
|
||||
$follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
|
||||
$desc = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 18);
|
||||
|
||||
$html = '<a class="plate-item flex aksrt-plate-item ajax-item" href="' . esc_url($url) . '">';
|
||||
$html .= aksrt_render_plate_thumb_html($id, 'thumbnail');
|
||||
$html .= '<div class="item-info flex1 aksrt-plate-item__body">';
|
||||
$html .= '<div class="info-header"><h2 class="forum-title aksrt-plate-item__title">' . esc_html(get_the_title($post)) . '</h2></div>';
|
||||
if ($desc !== '') {
|
||||
$html .= '<div class="muted-2-color em09 mt3 text-ellipsis aksrt-plate-item__desc">' . esc_html($desc) . '</div>';
|
||||
}
|
||||
$html .= '<div class="item-meta em09 muted-2-color mt3 aksrt-plate-item__meta">';
|
||||
$html .= '<span>' . esc_html(sprintf(/* translators: %d topic count */ __('%d 帖', 'aksrt'), $count)) . '</span>';
|
||||
if ($follow > 0) {
|
||||
$html .= '<span class="ml10">' . esc_html(sprintf(/* translators: %d follow count */ __('%d 关注', 'aksrt'), $follow)) . '</span>';
|
||||
}
|
||||
$html .= '</div></div></a>';
|
||||
return $html;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_plate_card')) :
|
||||
/**
|
||||
* Plate card slide item.
|
||||
*
|
||||
* @param WP_Post|int $plate
|
||||
*/
|
||||
function aksrt_render_plate_card($plate): string {
|
||||
$post = $plate instanceof WP_Post ? $plate : get_post((int) $plate);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return '';
|
||||
}
|
||||
$id = (int) $post->ID;
|
||||
$url = get_permalink($id);
|
||||
$thumb = get_the_post_thumbnail_url($id, 'medium');
|
||||
$count = (int) get_post_meta($id, '_aksrt_posts_count', true);
|
||||
$follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
|
||||
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
|
||||
|
||||
$html = '<a class="plate-card aksrt-plate-card ajax-item" href="' . esc_url($url) . '">';
|
||||
$html .= '<div class="plate-thumb aksrt-plate-card__cover">';
|
||||
if ($thumb) {
|
||||
$html .= '<img class="aksrt-lazy lazyload fit-cover" data-src="' . esc_url($thumb) . '" src="' . esc_url($ph) . '" alt="" loading="lazy" />';
|
||||
} else {
|
||||
$html .= '<span class="is-empty"><i class="fa fa-comments" aria-hidden="true"></i></span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="aksrt-plate-card__title forum-title mt10">' . esc_html(get_the_title($post)) . '</div>';
|
||||
$html .= '<div class="aksrt-plate-card__meta muted-2-color em09 mt6">';
|
||||
$html .= '<span>' . esc_html(sprintf(__('%d 帖', 'aksrt'), $count)) . '</span>';
|
||||
$html .= '<span class="ml10">' . esc_html(sprintf(__('%d 关注', 'aksrt'), $follow)) . '</span>';
|
||||
$html .= '</div></a>';
|
||||
return $html;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_moderator_row')) :
|
||||
/**
|
||||
* Moderator identity row with optional follow.
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param string $role author|super|moderator
|
||||
* @param bool $show_avatar
|
||||
* @param string $style list|grid
|
||||
*/
|
||||
function aksrt_render_moderator_row(int $user_id, string $role = 'moderator', bool $show_avatar = true, string $style = 'list'): string {
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return '';
|
||||
}
|
||||
$avatar = function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user_id, ['size' => 64])
|
||||
: get_avatar_url($user_id, ['size' => 64]);
|
||||
$home = function_exists('aksrt_get_user_home_url')
|
||||
? aksrt_get_user_home_url($user_id)
|
||||
: get_author_posts_url($user_id);
|
||||
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
|
||||
|
||||
if ($role === 'author' || $role === 'super') {
|
||||
$role_label = __('超级版主', 'aksrt');
|
||||
} else {
|
||||
$role_label = __('版主', 'aksrt');
|
||||
}
|
||||
|
||||
$name = function_exists('aksrt_get_user_name')
|
||||
? aksrt_get_user_name([
|
||||
'id' => $user_id,
|
||||
'class' => 'flex ac flex1',
|
||||
'follow' => false,
|
||||
'medal' => true,
|
||||
'level' => true,
|
||||
])
|
||||
: '<a href="' . esc_url($home) . '">' . esc_html($user->display_name) . '</a>';
|
||||
|
||||
// Follow control (not for self).
|
||||
$follow_html = '';
|
||||
$cuid = get_current_user_id();
|
||||
if ($cuid > 0 && $cuid !== $user_id && class_exists('\AKSRT\User\Follow')) {
|
||||
$following = \AKSRT\User\Follow::get_instance()->is_following($cuid, $user_id);
|
||||
$label = $following ? __('已关注', 'aksrt') : __('关注', 'aksrt');
|
||||
$follow_html = '<button type="button" class="but hollow c-blue p2-10 em09 ml10 flex0' . ($following ? ' is-following' : '') . '" data-aksrt-follow="' . (int) $user_id . '">' . esc_html($label) . '</button>';
|
||||
}
|
||||
|
||||
if ($style === 'grid') {
|
||||
$html = '<div class="aksrt-plate-mods__item role-' . esc_attr($role) . ' is-grid">';
|
||||
if ($show_avatar) {
|
||||
$html .= '<a href="' . esc_url($home) . '"><img class="aksrt-plate-mods__avatar aksrt-lazy lazyload" data-src="' . esc_url($avatar) . '" src="' . esc_url($ph) . '" alt="" loading="lazy" width="48" height="48" /></a>';
|
||||
}
|
||||
$html .= '<div class="aksrt-plate-mods__name">' . $name . '</div>';
|
||||
$html .= '<div class="aksrt-plate-mods__role muted-2-color em09">' . esc_html($role_label) . '</div>';
|
||||
$html .= $follow_html;
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html = '<div class="aksrt-plate-mods__item role-' . esc_attr($role) . ' flex ac">';
|
||||
if ($show_avatar) {
|
||||
$html .= '<a class="avatar-img mr10" href="' . esc_url($home) . '"><img class="aksrt-plate-mods__avatar aksrt-lazy lazyload" data-src="' . esc_url($avatar) . '" src="' . esc_url($ph) . '" alt="" loading="lazy" width="40" height="40" /></a>';
|
||||
}
|
||||
$html .= '<div class="flex1 min-w0">';
|
||||
$html .= '<div class="aksrt-plate-mods__name">' . $name . '</div>';
|
||||
$html .= '<div class="aksrt-plate-mods__role muted-2-color em09">' . esc_html($role_label) . '</div>';
|
||||
$html .= '</div>';
|
||||
$html .= $follow_html;
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
endif;
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
/**
|
||||
* Forum frontend URL helpers.
|
||||
* Generic page-template URL helpers.
|
||||
*
|
||||
* Page-template assignment is the source of truth for forum home / compose URLs.
|
||||
* Page-template assignment is the source of truth for resolving public URLs
|
||||
* for custom page templates (submit form, etc.).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -13,7 +14,7 @@ if (!function_exists('aksrt_get_page_url_by_template')) :
|
||||
/**
|
||||
* Resolve the first published page that uses a given page template file.
|
||||
*
|
||||
* @param string $template_file Relative theme template path (e.g. templates/pages/page-forums.php).
|
||||
* @param string $template_file Relative theme template path (e.g. templates/pages/page-submit.php).
|
||||
*/
|
||||
function aksrt_get_page_url_by_template(string $template_file): string {
|
||||
$template_file = ltrim(str_replace('\\', '/', $template_file), '/');
|
||||
@@ -51,48 +52,3 @@ if (!function_exists('aksrt_get_page_url_by_template')) :
|
||||
return $url;
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_forum_home_url')) :
|
||||
/**
|
||||
* Public forum home URL (page template AKSRT - 论坛).
|
||||
*/
|
||||
function aksrt_get_forum_home_url(): string {
|
||||
$url = aksrt_get_page_url_by_template('templates/pages/page-forums.php');
|
||||
return $url !== '' ? $url : home_url('/');
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_forum_compose_url')) :
|
||||
/**
|
||||
* Public forum compose URL (page template AKSRT - 发帖).
|
||||
*
|
||||
* @param array<string, scalar> $args Optional query args (board, plate, …).
|
||||
*/
|
||||
function aksrt_get_forum_compose_url(array $args = []): string {
|
||||
$url = aksrt_get_page_url_by_template('templates/forum/forum-compose.php');
|
||||
if ($url === '') {
|
||||
$url = home_url('/');
|
||||
}
|
||||
|
||||
// Normalize plate → board for compose form contract.
|
||||
if (isset($args['plate']) && !isset($args['board'])) {
|
||||
$args['board'] = $args['plate'];
|
||||
unset($args['plate']);
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
foreach ($args as $key => $value) {
|
||||
$key = sanitize_key((string) $key);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
if (is_bool($value)) {
|
||||
$clean[$key] = $value ? '1' : '0';
|
||||
} else {
|
||||
$clean[$key] = (string) $value;
|
||||
}
|
||||
}
|
||||
|
||||
return !empty($clean) ? add_query_arg($clean, $url) : $url;
|
||||
}
|
||||
endif;
|
||||
|
||||
@@ -21,17 +21,6 @@ if (!function_exists('aksrt_get_header_newposts_config')) :
|
||||
$types = is_array($cfg['types'] ?? null) ? $cfg['types'] : [];
|
||||
if (!$types) {
|
||||
$types = ['post'];
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$forum = \AKSRT\Settings\Settings::get_forum_settings();
|
||||
$shop = \AKSRT\Settings\Settings::get_shop_settings();
|
||||
if (!empty($forum['enabled'])) {
|
||||
$types[] = 'topic';
|
||||
$types[] = 'plate';
|
||||
}
|
||||
if (!empty($shop['enabled'])) {
|
||||
$types[] = 'product';
|
||||
}
|
||||
}
|
||||
}
|
||||
$cfg['types'] = $types;
|
||||
if (empty($cfg['class'])) {
|
||||
@@ -65,53 +54,6 @@ if (!function_exists('aksrt_get_new_add_actions')) :
|
||||
'label' => __('写文章', 'aksrt'),
|
||||
'icon' => 'fas fa-file-alt',
|
||||
];
|
||||
} elseif ($type === 'topic') {
|
||||
$url = function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : '';
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'type' => 'topic',
|
||||
'url' => $url,
|
||||
'label' => __('发帖', 'aksrt'),
|
||||
'icon' => 'fas fa-comments',
|
||||
];
|
||||
} elseif ($type === 'product') {
|
||||
$url = '';
|
||||
if (function_exists('aksrt_get_product_edit_url')) {
|
||||
$url = (string) aksrt_get_product_edit_url();
|
||||
}
|
||||
if ($url === '') {
|
||||
$url = admin_url('post-new.php?post_type=aksrt_product');
|
||||
}
|
||||
$out[] = [
|
||||
'type' => 'product',
|
||||
'url' => $url,
|
||||
'label' => __('发布商品', 'aksrt'),
|
||||
'icon' => 'fas fa-shopping-bag',
|
||||
];
|
||||
} elseif ($type === 'plate') {
|
||||
// zibll bbs_plate → AKSRT forum plate CPT (admin create for entitled users).
|
||||
$can = current_user_can('manage_options');
|
||||
if (!$can && class_exists('\AKSRT\Forum\Forum')) {
|
||||
try {
|
||||
$forum = \AKSRT\Forum\Forum::get_instance();
|
||||
if (method_exists($forum, 'forum_user_can')) {
|
||||
$can = (bool) $forum->forum_user_can(get_current_user_id(), 'plate_create');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$can = false;
|
||||
}
|
||||
}
|
||||
if (!$can || !post_type_exists('aksrt_forum_plate')) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'type' => 'plate',
|
||||
'url' => admin_url('post-new.php?post_type=aksrt_forum_plate'),
|
||||
'label' => __('创建版块', 'aksrt'),
|
||||
'icon' => 'fas fa-th-large',
|
||||
];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
|
||||
@@ -356,26 +356,7 @@ if (!function_exists('aksrt_content_order_cookie_token_key')) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether guest cash purchase is allowed for forum topic / plate follow.
|
||||
* Reuses global pay_no_logged_in (same switch as content).
|
||||
*
|
||||
* @param WP_Post|int|null $post Topic or plate post (optional filter context).
|
||||
*/
|
||||
if (!function_exists('aksrt_allow_guest_forum_pay')) {
|
||||
function aksrt_allow_guest_forum_pay($post = null): bool {
|
||||
$order = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_order_settings() : [];
|
||||
$paid = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_paid_content_settings() : [];
|
||||
$allow = !empty($order['no_logged_in']) || !empty($paid['no_logged_in']);
|
||||
/**
|
||||
* @param bool $allow
|
||||
* @param WP_Post|int|null $post
|
||||
*/
|
||||
return (bool) apply_filters('aksrt_allow_guest_forum_pay', $allow, $post);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set guest paid cookies after cash order paid (content / topic / plate_follow).
|
||||
* Set guest paid cookies after cash order paid (content).
|
||||
*
|
||||
* @param array<string,mixed> $order
|
||||
*/
|
||||
@@ -495,18 +476,6 @@ if (!function_exists('aksrt_guest_has_paid_access')) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guest paid plate follow access (cookie).
|
||||
*/
|
||||
if (!function_exists('aksrt_guest_has_plate_follow_access')) {
|
||||
function aksrt_guest_has_plate_follow_access(int $plate_id): bool {
|
||||
$type = class_exists('\AKSRT\Core\DomainModel')
|
||||
? \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW
|
||||
: 'plate_follow';
|
||||
return null !== aksrt_get_guest_paid_order($plate_id, $type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the amount a given user should pay for a post (Zibll VIP tier min).
|
||||
*
|
||||
|
||||
@@ -7,51 +7,6 @@
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_get_float_shop_cart_html')) :
|
||||
/**
|
||||
* Floating shop cart button with badge.
|
||||
*/
|
||||
function aksrt_get_float_shop_cart_html(array $item = []): string {
|
||||
if (!is_user_logged_in()) {
|
||||
return '';
|
||||
}
|
||||
$shop_on = true;
|
||||
if (class_exists('\AKSRT\Settings\Settings')) {
|
||||
$shop = \AKSRT\Settings\Settings::get_shop_settings();
|
||||
$shop_on = !empty($shop['enabled']);
|
||||
}
|
||||
if (!$shop_on) {
|
||||
return '';
|
||||
}
|
||||
// Skip on cart page.
|
||||
if (function_exists('is_page_template') && is_page_template('templates/pages/page-cart.php')) {
|
||||
return '';
|
||||
}
|
||||
$url = function_exists('aksrt_get_cart_page_url') ? aksrt_get_cart_page_url() : '';
|
||||
if ($url === '') {
|
||||
return '';
|
||||
}
|
||||
$count = 0;
|
||||
if (class_exists('\AKSRT\Shop\Shop')) {
|
||||
$count = (int) \AKSRT\Shop\Shop::get_instance()->get_cart_count(get_current_user_id());
|
||||
}
|
||||
$icon = !empty($item['icon']) ? (string) $item['icon'] : 'fas fa-shopping-cart';
|
||||
$style = [];
|
||||
if (!empty($item['color'])) {
|
||||
$style[] = '--this-color:' . esc_attr((string) $item['color']);
|
||||
}
|
||||
if (!empty($item['bg'])) {
|
||||
$style[] = '--this-bg:' . esc_attr((string) $item['bg']);
|
||||
}
|
||||
$style_attr = $style ? ' style="' . implode(';', $style) . '"' : '';
|
||||
$badge = $count > 0 ? (string) min(99, $count) : '';
|
||||
return '<a class="float-btn float-btn-shop-cart nowave" href="' . esc_url($url) . '" title="' . esc_attr__('购物车', 'aksrt') . '"' . $style_attr . '>'
|
||||
. '<i class="' . esc_attr($icon) . '" aria-hidden="true"></i>'
|
||||
. '<badge cart-count="' . esc_attr($badge) . '">' . esc_html($badge) . '</badge>'
|
||||
. '</a>';
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_get_float_pay_vip_html')) :
|
||||
/**
|
||||
* Floating VIP open button.
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -485,18 +485,8 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
|
||||
$wallet_balance = (float) ($dashboard['wallet_balance'] ?? 0.0);
|
||||
$runtime_config = aksrt_user_center_runtime_config($user_id, $tabs['default'], $active_tab, $tabs['urls']);
|
||||
$runtime_config['avatarUpload'] = aksrt_user_center_avatar_upload_config($allow_avatar_upload);
|
||||
$shop_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_shop_settings() : [];
|
||||
$forum_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_forum_settings() : [];
|
||||
$revenue_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_revenue_settings() : [];
|
||||
$shop_enabled = !empty($shop_settings['enabled']);
|
||||
$forum_enabled = !empty($forum_settings['enabled']);
|
||||
$new_post_url = function_exists('aksrt_get_new_post_url') ? aksrt_get_new_post_url() : '';
|
||||
$new_product_url = $shop_enabled && function_exists('aksrt_get_page_url_by_template')
|
||||
? aksrt_get_page_url_by_template('templates/pages/page-product-edit.php')
|
||||
: '';
|
||||
$forum_compose_url = $forum_enabled && function_exists('aksrt_get_page_url_by_template')
|
||||
? aksrt_get_page_url_by_template('templates/forum/forum-compose.php')
|
||||
: '';
|
||||
$revenue_url = (!empty($revenue_settings['enabled']) || !empty($affiliate_settings['enabled'])) && function_exists('aksrt_get_revenue_page_url')
|
||||
? aksrt_get_revenue_page_url()
|
||||
: '';
|
||||
@@ -523,8 +513,6 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
|
||||
'notification_unread_count' => function_exists('aksrt_get_user_unread_notification_count') ? aksrt_get_user_unread_notification_count($user_id) : 0,
|
||||
'messages_url' => $messages_enabled && function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : '',
|
||||
'new_post_url' => $new_post_url,
|
||||
'new_product_url' => $new_product_url,
|
||||
'forum_compose_url' => $forum_compose_url,
|
||||
'revenue_url' => $revenue_url,
|
||||
'per_page' => 10,
|
||||
'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1),
|
||||
|
||||
@@ -79,52 +79,16 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
|
||||
$display_tabs = $is_mobile ? $enabled_tabs_mobile : $enabled_tabs_pc;
|
||||
$default_tab = !empty($display_tabs) ? $display_tabs[0] : $enabled_tabs[0];
|
||||
|
||||
$forum_instance = class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null;
|
||||
$forum_stats = $forum_instance ? $forum_instance->get_user_forum_stats($user_id) : [];
|
||||
$forum_topic_count = (int) ($forum_stats['topic_count'] ?? 0);
|
||||
$product_count = 0;
|
||||
if (class_exists('\AKSRT\Shop\Shop')) {
|
||||
$product_count = (int) \AKSRT\Shop\Shop::get_instance()->count_author_products($user_id);
|
||||
}
|
||||
|
||||
$follow_count = 0;
|
||||
if (class_exists('\AKSRT\User\Follow')) {
|
||||
$follow_count = (int) \AKSRT\User\Follow::get_instance()->get_follower_count($user_id);
|
||||
}
|
||||
|
||||
$plate_count = 0;
|
||||
if ($forum_instance) {
|
||||
$seen_plates = [];
|
||||
if (method_exists($forum_instance, 'get_user_followed_plates')) {
|
||||
foreach ((array) $forum_instance->get_user_followed_plates($user_id) as $plate) {
|
||||
$pid = (int) ($plate['id'] ?? 0);
|
||||
if ($pid > 0) {
|
||||
$seen_plates[$pid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (method_exists($forum_instance, 'get_user_managed_plates')) {
|
||||
$managed = (array) $forum_instance->get_user_managed_plates($user_id);
|
||||
foreach (['owned', 'moderating'] as $bucket) {
|
||||
foreach ((array) ($managed[$bucket] ?? []) as $plate) {
|
||||
$pid = (int) ($plate['id'] ?? 0);
|
||||
if ($pid > 0) {
|
||||
$seen_plates[$pid] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$plate_count = count($seen_plates);
|
||||
}
|
||||
|
||||
$tab_counts = [
|
||||
'posts' => $post_count,
|
||||
'comments' => $comment_count,
|
||||
'favorites' => $favorite_count,
|
||||
'credits' => $credits_enabled && $is_own_profile ? (int) round((float) $credit_balance) : 0,
|
||||
'product' => $product_count,
|
||||
'plate' => $plate_count,
|
||||
'forum' => $forum_topic_count,
|
||||
'follow' => $follow_count,
|
||||
];
|
||||
|
||||
@@ -133,9 +97,6 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
|
||||
'comments' => __('评论', 'aksrt'),
|
||||
'favorites' => __('收藏', 'aksrt'),
|
||||
'credits' => sprintf(__('我的%s', 'aksrt'), $currency_name),
|
||||
'product' => __('商品', 'aksrt'),
|
||||
'plate' => __('圈子', 'aksrt'),
|
||||
'forum' => __('论坛', 'aksrt'),
|
||||
'follow' => __('粉丝', 'aksrt'), // Zibll: 粉丝
|
||||
];
|
||||
foreach ($visible_tabs as $tab_key => $tab_config) {
|
||||
@@ -174,9 +135,6 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
|
||||
'active_tab' => $active_tab,
|
||||
'tab_counts' => $tab_counts,
|
||||
'tab_labels' => $tab_labels,
|
||||
'forum_instance' => $forum_instance,
|
||||
'forum_stats' => $forum_stats,
|
||||
'forum_topic_count' => $forum_topic_count,
|
||||
'home_url' => $home_url,
|
||||
'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1),
|
||||
'comments_page' => max(1, isset($_GET['comments_page']) ? (int) $_GET['comments_page'] : 1),
|
||||
|
||||
@@ -208,19 +208,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
if ($pub_text === '') {
|
||||
$pub_text = __('发布内容', 'aksrt');
|
||||
}
|
||||
$pub_type = sanitize_key((string) ($all['header_user_newposts_btn'] ?? 'post'));
|
||||
$pub_url = (string) $ctx['publish_url'];
|
||||
if ($pub_type === 'topic' && function_exists('aksrt_get_forum_compose_url')) {
|
||||
$u = aksrt_get_forum_compose_url();
|
||||
if ($u) { $pub_url = $u; }
|
||||
} elseif ($pub_type === 'product') {
|
||||
if (function_exists('aksrt_get_product_edit_url')) {
|
||||
$u = (string) aksrt_get_product_edit_url();
|
||||
if ($u !== '') { $pub_url = $u; }
|
||||
} else {
|
||||
$pub_url = admin_url('post-new.php?post_type=aksrt_product');
|
||||
}
|
||||
}
|
||||
$href .= '<a rel="nofollow" href="' . esc_url($pub_url) . '">';
|
||||
$href .= '<div class="badg mb6 toggle-radius c-green"><i class="fas fa-pen" aria-hidden="true"></i></div>';
|
||||
$href .= '<div class="c-green">' . esc_html($pub_text) . '</div></a>';
|
||||
@@ -240,13 +228,6 @@ if (!function_exists('aksrt_header_user_box_html')) :
|
||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url('widgets.php')) . '">';
|
||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fas fa-chart-pie" aria-hidden="true"></i></div>';
|
||||
$href .= '<div class="c-yellow">' . esc_html__('模块布局', 'aksrt') . '</div></a>';
|
||||
// zibll: 商城中心.
|
||||
$shop_url = post_type_exists('aksrt_product')
|
||||
? admin_url('edit.php?post_type=aksrt_product')
|
||||
: admin_url('admin.php?page=aksrt_pay');
|
||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url($shop_url) . '">';
|
||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fas fa-shopping-bag" aria-hidden="true"></i></div>';
|
||||
$href .= '<div class="c-yellow">' . esc_html__('商城中心', 'aksrt') . '</div></a>';
|
||||
$href .= '<a rel="nofollow" target="_blank" href="' . esc_url(admin_url()) . '">';
|
||||
$href .= '<div class="badg mb6 toggle-radius c-yellow"><i class="fas fa-cog" aria-hidden="true"></i></div>';
|
||||
$href .= '<div class="c-yellow">' . esc_html__('后台管理', 'aksrt') . '</div></a>';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user