This commit is contained in:
wenfp
2026-07-27 16:36:33 +08:00
parent 53f48de2b7
commit 7f819cd775
19 changed files with 623 additions and 173 deletions
+14
View File
@@ -18,6 +18,9 @@
- `src/views/temples/TempleManagement.vue`:寺院管理页。
- `src/views/products/ProductManagement.vue`:商品管理列表页,按当前寺院隔离。
- `src/views/products/ProductDetail.vue`:商品新建和编辑页,按当前寺院隔离。
- `src/views/feature-icons/FeatureIconManagement.vue`:图标管理页,按当前寺院隔离,支持同步其他寺院图标。
- `src/views/ad-slots/AdSlotManagement.vue`:广告位管理页,按当前寺院隔离,只维护广告位置定义。
- `src/views/ads/AdManagement.vue`:广告管理页,按当前寺院隔离,广告内容绑定到广告位。
- `src/views/orders/OrderManagement.vue`:订单管理页,按当前寺院隔离。
- `src/views/rituals/RitualManagement.vue`:法事管理页,按当前寺院隔离。
- `src/views/users/UserManagement.vue`:用户管理页。
@@ -26,6 +29,9 @@
- `src/api/users.ts`:用户接口封装。
- `src/api/temples.ts`:寺院接口封装。
- `src/api/products.ts`:商品接口封装。
- `src/api/featureIcons.ts`:图标管理接口封装。
- `src/api/adSlots.ts`:广告位管理接口封装。
- `src/api/ads.ts`:广告管理接口封装。
- `src/api/uploads.ts`:图片上传接口封装。
- `src/api/orders.ts`:订单接口封装。
- `src/api/payments.ts`:微信支付接口封装。
@@ -39,12 +45,16 @@
/products 商品管理
/products/create 新建商品
/products/:productId/edit 编辑商品
/feature-icons 图标管理
/ad-slots 广告位管理
/ads 广告管理
/orders 订单管理
/ritual-services 法事管理
/users 用户管理
```
商品、订单、法事页面使用当前寺院上下文访问数据。当前寺院由后台顶部选择器维护,`src/api/http.ts` 会把 `currentTempleId` 写入请求头 `X-Temple-Id`
图标、广告位和广告页面也使用当前寺院上下文访问数据;广告位表示首页广告、详情页广告、支付页广告等位置,广告内容必须绑定到广告位;图标同步功能从其他寺院读取图标后复制到当前寺院。
后台页面通过路由守卫检查登录状态;认证令牌只存在后端设置的 HttpOnly Cookie 中,前端 localStorage 只缓存当前用户展示信息,不保存 token。
@@ -72,6 +82,7 @@
- 新增前端依赖时只写入 `package.json`,由用户执行 `pnpm install`
- `node_modules` 不提交。
- Vite 构建通过路由懒加载和 `manualChunks` 拆分 Vue、Ant Design Vue、图标、富文本编辑器和公共依赖;Ant Design Vue 组件在 `src/main.ts` 使用 `ant-design-vue/es/*` 子路径导入以支持 tree shaking。
## 验证
@@ -96,3 +107,6 @@ pnpm build
- 2026-07-23:商品编辑基础信息新增商品描述字段,前端商品 API 类型和商品编辑表单同步 `description`。验证:`pnpm typecheck``pnpm build` 通过。
- 2026-07-23:订单管理页新增微信 JSAPI 预下单入口,新增 `src/api/payments.ts` 调用 `/payments/wechat/jsapi`,用于生成支付参数;实际微信调起仍需用户端页面和 openid。验证:`pnpm typecheck``pnpm build` 通过。
- 2026-07-27:移除 Todo 占位页面,删除 `/todos` 前端路由和侧边栏菜单入口。验证:`pnpm typecheck``pnpm build` 通过。
- 2026-07-27:新增图标管理和广告位管理页面、API、路由和侧边栏入口;图标支持上传 120px * 120px 提示、链接/应用类型切换、商品选择和同步其他寺院图标。验证:`pnpm typecheck``pnpm build` 通过。
- 2026-07-27:广告模型拆分为广告位和广告,广告位页面改为位置配置,新增广告管理 API、页面、路由和菜单入口,广告内容绑定广告位并支持图片上传与链接/应用跳转。验证:`pnpm typecheck``pnpm build` 通过。
- 2026-07-27:优化前端构建包体积,路由页面改为懒加载,Ant Design Vue 改为组件级 ES 导入,Vite 增加 `manualChunks` 拆分常用依赖包并设置合理体积提示阈值。验证:`pnpm typecheck``pnpm build` 通过。
+26 -4
View File
@@ -1,8 +1,10 @@
<script setup lang="ts">
import {
BankOutlined,
AppstoreOutlined,
FileTextOutlined,
LoginOutlined,
PictureOutlined,
ProfileOutlined,
ShoppingOutlined,
TeamOutlined,
@@ -94,6 +96,18 @@ onMounted(fetchTemples);
<template #icon><ShoppingOutlined /></template>
商品管理
</a-menu-item>
<a-menu-item key="feature-icons" @click="openRoute('/feature-icons')">
<template #icon><AppstoreOutlined /></template>
图标管理
</a-menu-item>
<a-menu-item key="ad-slots" @click="openRoute('/ad-slots')">
<template #icon><PictureOutlined /></template>
广告位管理
</a-menu-item>
<a-menu-item key="ads" @click="openRoute('/ads')">
<template #icon><PictureOutlined /></template>
广告管理
</a-menu-item>
<a-menu-item key="orders" @click="openRoute('/orders')">
<template #icon><FileTextOutlined /></template>
订单管理
@@ -120,10 +134,18 @@ onMounted(fetchTemples);
placeholder="选择寺院"
@change="changeTemple"
/>
<a-button type="link" @click="submitLogout">
<template #icon><LoginOutlined /></template>
退出登录
</a-button>
<a-popconfirm
title="确认退出登录?"
ok-text="退出"
cancel-text="取消"
placement="bottomRight"
@confirm="submitLogout"
>
<a-button type="link">
<template #icon><LoginOutlined /></template>
退出登录
</a-button>
</a-popconfirm>
</a-space>
</a-layout-header>
+56 -2
View File
@@ -1,4 +1,27 @@
import Antd from 'ant-design-vue';
import Avatar from 'ant-design-vue/es/avatar';
import Breadcrumb from 'ant-design-vue/es/breadcrumb';
import Button from 'ant-design-vue/es/button';
import Col from 'ant-design-vue/es/col';
import DatePicker from 'ant-design-vue/es/date-picker';
import Form from 'ant-design-vue/es/form';
import Image from 'ant-design-vue/es/image';
import Input from 'ant-design-vue/es/input';
import InputNumber from 'ant-design-vue/es/input-number';
import Layout from 'ant-design-vue/es/layout';
import Menu from 'ant-design-vue/es/menu';
import Modal from 'ant-design-vue/es/modal';
import Popconfirm from 'ant-design-vue/es/popconfirm';
import Row from 'ant-design-vue/es/row';
import Segmented from 'ant-design-vue/es/segmented';
import Select from 'ant-design-vue/es/select';
import Space from 'ant-design-vue/es/space';
import Spin from 'ant-design-vue/es/spin';
import Switch from 'ant-design-vue/es/switch';
import Table from 'ant-design-vue/es/table';
import Tabs from 'ant-design-vue/es/tabs';
import Tag from 'ant-design-vue/es/tag';
import Tooltip from 'ant-design-vue/es/tooltip';
import Upload from 'ant-design-vue/es/upload';
import { createApp } from 'vue';
import 'ant-design-vue/dist/reset.css';
@@ -10,4 +33,35 @@ import router from './router';
document.title = appConfig.name;
createApp(App).use(router).use(Antd).mount('#app');
const app = createApp(App);
[
Avatar,
Breadcrumb,
Button,
Col,
DatePicker,
Form,
Image,
Input,
InputNumber,
Layout,
Menu,
Modal,
Popconfirm,
Row,
Segmented,
Select,
Space,
Spin,
Switch,
Table,
Tabs,
Tag,
Tooltip,
Upload,
].forEach((component) => {
app.use(component);
});
app.use(router).mount('#app');
+39 -8
View File
@@ -1,14 +1,18 @@
import { createRouter, createWebHistory } from 'vue-router';
import { clearAuthUser, getCurrentUser, getStoredUser, storeAuthUser } from '@/api/auth';
import AdminLayout from '@/layouts/AdminLayout.vue';
import LoginPage from '@/views/login/LoginPage.vue';
import OrderManagement from '@/views/orders/OrderManagement.vue';
import ProductDetail from '@/views/products/ProductDetail.vue';
import ProductManagement from '@/views/products/ProductManagement.vue';
import RitualManagement from '@/views/rituals/RitualManagement.vue';
import TempleManagement from '@/views/temples/TempleManagement.vue';
import UserManagement from '@/views/users/UserManagement.vue';
const AdminLayout = () => import('@/layouts/AdminLayout.vue');
const AdSlotManagement = () => import('@/views/ad-slots/AdSlotManagement.vue');
const AdManagement = () => import('@/views/ads/AdManagement.vue');
const FeatureIconManagement = () => import('@/views/feature-icons/FeatureIconManagement.vue');
const LoginPage = () => import('@/views/login/LoginPage.vue');
const OrderManagement = () => import('@/views/orders/OrderManagement.vue');
const ProductDetail = () => import('@/views/products/ProductDetail.vue');
const ProductManagement = () => import('@/views/products/ProductManagement.vue');
const RitualManagement = () => import('@/views/rituals/RitualManagement.vue');
const TempleManagement = () => import('@/views/temples/TempleManagement.vue');
const UserManagement = () => import('@/views/users/UserManagement.vue');
export const router = createRouter({
history: createWebHistory(),
@@ -68,6 +72,33 @@ export const router = createRouter({
title: '编辑商品',
},
},
{
path: 'feature-icons',
name: 'feature-icons',
component: FeatureIconManagement,
meta: {
module: 'feature-icons',
title: '图标管理',
},
},
{
path: 'ad-slots',
name: 'ad-slots',
component: AdSlotManagement,
meta: {
module: 'ad-slots',
title: '广告位管理',
},
},
{
path: 'ads',
name: 'ads',
component: AdManagement,
meta: {
module: 'ads',
title: '广告管理',
},
},
{
path: 'orders',
name: 'orders',
+165 -150
View File
@@ -1,206 +1,221 @@
:root {
color: #1f2937;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
background: #f3f5f8;
color: #1f2937;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background: #f3f5f8;
}
* {
box-sizing: border-box;
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
margin: 0;
min-width: 320px;
}
.app-shell {
min-height: 100vh;
min-height: 100vh;
}
.sidebar {
background: #fff !important;
border-right: 1px solid #e5e7eb;
background: #fff !important;
border-right: 1px solid #e5e7eb;
}
.brand {
align-items: center;
border-bottom: 1px solid #e5e7eb;
display: flex;
gap: 12px;
height: 44px;
padding: 0 20px;
align-items: center;
border-bottom: 1px solid #e5e7eb;
display: flex;
gap: 12px;
height: 44px;
padding: 0 20px;
}
.brand-mark {
align-items: center;
background: #1677ff;
border-radius: 6px;
color: #fff;
display: flex;
font-weight: 700;
height: 32px;
justify-content: center;
width: 32px;
align-items: center;
background: #1677ff;
border-radius: 6px;
color: #fff;
display: flex;
font-weight: 700;
height: 32px;
justify-content: center;
width: 32px;
}
.brand-name {
color: #111827;
font-size: 15px;
font-weight: 700;
line-height: 20px;
color: #111827;
font-size: 15px;
font-weight: 700;
line-height: 20px;
}
.brand-subtitle {
color: #6b7280;
font-size: 12px;
line-height: 18px;
color: #6b7280;
font-size: 12px;
line-height: 18px;
}
.side-menu {
background: transparent;
border-inline-end: 0 !important;
background: transparent;
border-inline-end: 0 !important;
}
.topbar {
align-items: center;
background: #fff !important;
border-bottom: 1px solid #e5e7eb;
color: #1f2937;
display: flex;
height: 64px;
justify-content: space-between;
padding: 0 24px;
align-items: center;
background: #fff !important;
border-bottom: 1px solid #e5e7eb;
color: #1f2937;
display: flex;
justify-content: space-between;
padding: 0 14px;
height: 44px !important;
}
.content {
padding: 14px;
padding: 14px;
}
.breadcrumb {
margin-bottom: 14px;
margin-bottom: 14px;
}
.temple-selector {
width: 160px;
width: 160px;
}
.workspace {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
}
.page-heading {
align-items: center;
display: flex;
gap: 16px;
justify-content: space-between;
margin-bottom: 18px;
align-items: center;
display: flex;
gap: 16px;
justify-content: space-between;
margin-bottom: 18px;
}
.page-heading h1 {
font-size: 20px;
line-height: 28px;
margin: 0;
font-size: 20px;
line-height: 28px;
margin: 0;
}
.page-heading p {
color: #6b7280;
font-size: 13px;
line-height: 20px;
margin: 4px 0 0;
color: #6b7280;
font-size: 13px;
line-height: 20px;
margin: 4px 0 0;
}
.query-bar {
margin-bottom: 16px;
margin-bottom: 16px;
}
.table-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.square-preview,
.square-preview img {
border-radius: 6px;
object-fit: cover;
}
.ad-preview,
.ad-preview img {
border-radius: 6px;
object-fit: cover;
}
.login-page {
align-items: center;
background:
linear-gradient(135deg, rgba(22, 119, 255, 0.12), transparent 36%),
#f3f5f8;
display: flex;
min-height: 100vh;
padding: 24px;
align-items: center;
background: linear-gradient(135deg, rgba(22, 119, 255, 0.12), transparent 36%), #f3f5f8;
display: flex;
min-height: 100vh;
padding: 24px;
}
.login-panel {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08);
margin: 0 auto;
max-width: 420px;
padding: 28px;
width: 100%;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08);
margin: 0 auto;
max-width: 420px;
padding: 28px;
width: 100%;
}
.login-brand {
align-items: center;
display: flex;
gap: 14px;
margin-bottom: 28px;
align-items: center;
display: flex;
gap: 14px;
margin-bottom: 28px;
}
.login-brand h1 {
font-size: 22px;
line-height: 30px;
margin: 0;
font-size: 22px;
line-height: 30px;
margin: 0;
}
.login-brand p {
color: #6b7280;
font-size: 13px;
line-height: 20px;
margin: 2px 0 0;
color: #6b7280;
font-size: 13px;
line-height: 20px;
margin: 2px 0 0;
}
.login-form {
width: 100%;
width: 100%;
}
.login-options {
align-items: center;
display: flex;
justify-content: space-between;
margin: -4px 0 18px;
align-items: center;
display: flex;
justify-content: space-between;
margin: -4px 0 18px;
}
.captcha-row {
display: grid;
gap: 10px;
grid-template-columns: minmax(0, 1fr) 132px;
display: grid;
gap: 10px;
grid-template-columns: minmax(0, 1fr) 132px;
}
.empty-icon {
color: #1677ff;
font-size: 56px;
color: #1677ff;
font-size: 56px;
}
.full-width-control {
width: 100%;
width: 100%;
}
.upload-text {
margin-top: 8px;
margin-top: 8px;
}
.product-detail-page {
display: flex;
flex-direction: column;
height: calc(100vh - 142px);
height: calc(100dvh - 142px);
overflow: hidden;
display: flex;
flex-direction: column;
height: calc(100vh - 142px);
height: calc(100dvh - 142px);
overflow: hidden;
}
.product-detail-body,
@@ -208,72 +223,72 @@ body {
.product-detail-body .ant-spin-container,
.product-detail-form,
.product-detail-tabs {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.product-detail-tabs .ant-tabs-content-holder {
flex: 1;
min-height: 0;
overflow: hidden;
flex: 1;
min-height: 0;
overflow: hidden;
}
.product-detail-tabs .ant-tabs-content {
height: 100%;
height: 100%;
}
.product-detail-tabs .ant-tabs-tabpane {
height: 100%;
overflow: auto;
height: 100%;
overflow: auto;
}
.product-rich-editor {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.product-rich-editor .ql-container {
flex: 1;
min-height: 0;
flex: 1;
min-height: 0;
}
.product-rich-editor .ql-editor {
min-height: 0;
overflow-y: auto;
min-height: 0;
overflow-y: auto;
}
@media (max-width: 768px) {
.app-shell {
display: block;
}
.app-shell {
display: block;
}
.sidebar {
display: none;
}
.sidebar {
display: none;
}
.topbar,
.page-heading {
align-items: flex-start;
flex-direction: column;
}
.topbar,
.page-heading {
align-items: flex-start;
flex-direction: column;
}
.content {
padding: 2px;
}
.content {
padding: 2px;
}
.workspace {
padding: 14px;
}
.workspace {
padding: 14px;
}
.product-rich-editor {
min-height: 360px;
}
.product-rich-editor {
min-height: 360px;
}
.login-page {
padding: 16px;
}
.login-page {
padding: 16px;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { LockOutlined, ReloadOutlined, SafetyCertificateOutlined, UserOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import message from 'ant-design-vue/es/message';
import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
import Modal from 'ant-design-vue/es/modal';
import message from 'ant-design-vue/es/message';
import type { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue';
+1 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ArrowLeftOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import message from 'ant-design-vue/es/message';
import type { Rule } from 'ant-design-vue/es/form';
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
import Modal from 'ant-design-vue/es/modal';
import message from 'ant-design-vue/es/message';
import type { ColumnsType } from 'ant-design-vue/es/table';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
import Modal from 'ant-design-vue/es/modal';
import message from 'ant-design-vue/es/message';
import type { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue';
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
import Modal from 'ant-design-vue/es/modal';
import message from 'ant-design-vue/es/message';
import type { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue';
+2 -1
View File
@@ -6,7 +6,8 @@ import {
ReloadOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
import Modal from 'ant-design-vue/es/modal';
import message from 'ant-design-vue/es/message';
import type { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue';
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/http.ts","./src/api/orders.ts","./src/api/payments.ts","./src/api/products.ts","./src/api/rituals.ts","./src/api/temples.ts","./src/api/uploads.ts","./src/api/users.ts","./src/config/app.ts","./src/router/index.ts","./src/app.vue","./src/layouts/adminlayout.vue","./src/views/login/loginpage.vue","./src/views/orders/ordermanagement.vue","./src/views/products/productdetail.vue","./src/views/products/productmanagement.vue","./src/views/rituals/ritualmanagement.vue","./src/views/temples/templemanagement.vue","./src/views/users/usermanagement.vue"],"version":"5.9.3"}
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/adslots.ts","./src/api/ads.ts","./src/api/auth.ts","./src/api/featureicons.ts","./src/api/http.ts","./src/api/orders.ts","./src/api/payments.ts","./src/api/products.ts","./src/api/rituals.ts","./src/api/temples.ts","./src/api/uploads.ts","./src/api/users.ts","./src/config/app.ts","./src/router/index.ts","./src/app.vue","./src/layouts/adminlayout.vue","./src/views/ad-slots/adslotmanagement.vue","./src/views/ads/admanagement.vue","./src/views/feature-icons/featureiconmanagement.vue","./src/views/login/loginpage.vue","./src/views/orders/ordermanagement.vue","./src/views/products/productdetail.vue","./src/views/products/productmanagement.vue","./src/views/rituals/ritualmanagement.vue","./src/views/temples/templemanagement.vue","./src/views/users/usermanagement.vue"],"version":"5.9.3"}
+39
View File
@@ -2,11 +2,50 @@ import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';
import { defineConfig, loadEnv } from 'vite';
function manualChunks(id: string) {
if (!id.includes('node_modules')) {
return undefined;
}
if (id.includes('/vue/') || id.includes('/vue-router/')) {
return 'vendor-vue';
}
if (id.includes('/@vueup/vue-quill/') || id.includes('/quill/') || id.includes('/parchment/')) {
return 'vendor-editor';
}
if (
id.includes('/@ant-design/icons-vue/') ||
id.includes('/@ant-design/icons-svg/')
) {
return 'vendor-antd-icons';
}
if (id.includes('/ant-design-vue/')) {
return 'vendor-antd';
}
if (id.includes('/@ant-design/')) {
return 'vendor-antd-core';
}
return 'vendor-common';
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const apiTarget = env.VITE_API_TARGET || 'http://127.0.0.1:8000';
return {
build: {
chunkSizeWarningLimit: 900,
rollupOptions: {
output: {
manualChunks,
},
},
},
plugins: [vue()],
resolve: {
alias: {