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
+16
View File
@@ -16,6 +16,9 @@
- `app/routers/todos.py`Todo 接口,提供 `/todos` 的增删改查。 - `app/routers/todos.py`Todo 接口,提供 `/todos` 的增删改查。
- `app/routers/temples.py`:寺院管理接口,提供 `/temples` 的增删改查。 - `app/routers/temples.py`:寺院管理接口,提供 `/temples` 的增删改查。
- `app/routers/products.py`:商品管理接口,提供 `/products` 的增删改查。 - `app/routers/products.py`:商品管理接口,提供 `/products` 的增删改查。
- `app/routers/feature_icons.py`:图标管理接口,提供 `/feature-icons` 的增删改查和跨寺院复制。
- `app/routers/ad_slots.py`:广告位管理接口,提供广告位置 `/ad-slots` 的增删改查。
- `app/routers/ads.py`:广告管理接口,提供绑定广告位的 `/ads` 增删改查。
- `app/routers/orders.py`:订单管理接口,提供 `/orders` 的增删改查。 - `app/routers/orders.py`:订单管理接口,提供 `/orders` 的增删改查。
- `app/routers/payments.py`:微信支付接口,提供 JSAPI 预下单和支付通知处理。 - `app/routers/payments.py`:微信支付接口,提供 JSAPI 预下单和支付通知处理。
- `app/routers/deploy.py`:自动部署 webhook 接口,供 Gitea 推送后触发前端构建。 - `app/routers/deploy.py`:自动部署 webhook 接口,供 Gitea 推送后触发前端构建。
@@ -39,6 +42,9 @@ DATABASE_PATH=/tmp/py-web.sqlite3 .venv/bin/uvicorn index:app --reload
- `front/src/views/temples/TempleManagement.vue`:寺院管理页面。 - `front/src/views/temples/TempleManagement.vue`:寺院管理页面。
- `front/src/views/products/ProductManagement.vue`:商品管理页面。 - `front/src/views/products/ProductManagement.vue`:商品管理页面。
- `front/src/views/products/ProductDetail.vue`:商品新建和编辑页面。 - `front/src/views/products/ProductDetail.vue`:商品新建和编辑页面。
- `front/src/views/feature-icons/FeatureIconManagement.vue`:图标管理页面。
- `front/src/views/ad-slots/AdSlotManagement.vue`:广告位管理页面。
- `front/src/views/ads/AdManagement.vue`:广告管理页面。
- `front/src/views/orders/OrderManagement.vue`:订单管理页面。 - `front/src/views/orders/OrderManagement.vue`:订单管理页面。
- `front/src/views/rituals/RitualManagement.vue`:法事管理页面。 - `front/src/views/rituals/RitualManagement.vue`:法事管理页面。
- `front/src/views/users/UserManagement.vue`:用户管理页面,调用后端 `/users` 接口。 - `front/src/views/users/UserManagement.vue`:用户管理页面,调用后端 `/users` 接口。
@@ -47,6 +53,9 @@ DATABASE_PATH=/tmp/py-web.sqlite3 .venv/bin/uvicorn index:app --reload
- `front/src/api/users.ts`:用户接口封装。 - `front/src/api/users.ts`:用户接口封装。
- `front/src/api/uploads.ts`:图片上传接口封装。 - `front/src/api/uploads.ts`:图片上传接口封装。
- `front/src/api/payments.ts`:微信支付接口封装。 - `front/src/api/payments.ts`:微信支付接口封装。
- `front/src/api/featureIcons.ts`:图标管理接口封装。
- `front/src/api/adSlots.ts`:广告位管理接口封装。
- `front/src/api/ads.ts`:广告管理接口封装。
前端更细的模块约定见 `front/AGENTS.md`,后端更细的模块约定见 `app/AGENTS.md` 前端更细的模块约定见 `front/AGENTS.md`,后端更细的模块约定见 `app/AGENTS.md`
@@ -58,6 +67,9 @@ DATABASE_PATH=/tmp/py-web.sqlite3 .venv/bin/uvicorn index:app --reload
/products 商品管理 /products 商品管理
/products/create 新建商品 /products/create 新建商品
/products/:productId/edit 编辑商品 /products/:productId/edit 编辑商品
/feature-icons 图标管理
/ad-slots 广告位管理
/ads 广告管理
/orders 订单管理 /orders 订单管理
/ritual-services 法事管理 /ritual-services 法事管理
/users 用户管理 /users 用户管理
@@ -66,6 +78,7 @@ DATABASE_PATH=/tmp/py-web.sqlite3 .venv/bin/uvicorn index:app --reload
前端通过 Vite 代理把 `/api/*` 转发到 FastAPI 后端,默认目标是 `http://127.0.0.1:8000` 前端通过 Vite 代理把 `/api/*` 转发到 FastAPI 后端,默认目标是 `http://127.0.0.1:8000`
商品、订单、法事属于寺院隔离数据。前端通过顶部寺院选择器维护当前寺院,并在请求头 `X-Temple-Id` 中传给后端;后端按该寺院过滤数据。 商品、订单、法事属于寺院隔离数据。前端通过顶部寺院选择器维护当前寺院,并在请求头 `X-Temple-Id` 中传给后端;后端按该寺院过滤数据。
图标、广告位和广告同样属于寺院隔离数据;广告位是首页广告、详情页广告、支付页广告等位置定义,广告是绑定到广告位的投放内容;图标支持从其他寺院复制到当前寺院,应用类型图标复制后需重新选择当前寺院商品。
## 依赖与运行约定 ## 依赖与运行约定
@@ -116,3 +129,6 @@ pnpm dev
- 2026-07-27:新增 Gitea 自动部署 webhook,后端 `/deploy/webhook` 校验 Gitea HMAC 签名或部署 token 后执行 `scripts/deploy_front.sh`,脚本负责拉取代码、安装前端依赖并执行 `pnpm build`;新增 `.env.example` 记录部署环境变量。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。 - 2026-07-27:新增 Gitea 自动部署 webhook,后端 `/deploy/webhook` 校验 Gitea HMAC 签名或部署 token 后执行 `scripts/deploy_front.sh`,脚本负责拉取代码、安装前端依赖并执行 `pnpm build`;新增 `.env.example` 记录部署环境变量。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。
- 2026-07-27:自动部署 webhook 从同步构建调整为后台构建,接口校验通过后立即返回 `202 accepted`,后台执行结果写入 `DEPLOY_LOG_PATH`,避免 Gitea Delivery 等待前端构建超时。验证:后端编译通过,webhook 后台任务触发测试通过。 - 2026-07-27:自动部署 webhook 从同步构建调整为后台构建,接口校验通过后立即返回 `202 accepted`,后台执行结果写入 `DEPLOY_LOG_PATH`,避免 Gitea Delivery 等待前端构建超时。验证:后端编译通过,webhook 后台任务触发测试通过。
- 2026-07-27:移除前端 Todo 占位模块,删除 `/todos` 前端路由、侧边栏入口和占位页面;后端 Todo 接口保留。验证:前端类型检查和构建通过。 - 2026-07-27:移除前端 Todo 占位模块,删除 `/todos` 前端路由、侧边栏入口和占位页面;后端 Todo 接口保留。验证:前端类型检查和构建通过。
- 2026-07-27:新增图标管理和广告位管理模块,后端新增 `feature_icons``ad_slots` 表及寺院隔离 CRUD 接口,图标支持跨寺院复制;前端新增图标管理、广告位管理 API、页面、路由和菜单入口,图片上传复用 OSS 上传接口。验证:后端编译通过,前端 `pnpm typecheck``pnpm build` 通过。
- 2026-07-27:广告模型拆分为广告位和广告,广告位仅表示首页广告、详情页广告、支付页广告等位置,新增 `ads` 表和 `/ads` 接口维护投放内容并绑定广告位;前端新增广告管理菜单和页面,广告位页面改为位置配置。验证:后端编译通过,广告位与广告接口冒烟测试通过,前端 `pnpm typecheck``pnpm build` 通过。
- 2026-07-27:优化前端构建拆包,路由页面改为懒加载,Ant Design Vue 改为组件级 ES 导入以支持 tree shakingVite `manualChunks` 拆分 Vue、Antd、图标、富文本编辑器和公共依赖包,并按实际常用组件包设置体积提示阈值。验证:`pnpm typecheck``pnpm build` 通过。
+13
View File
@@ -10,6 +10,9 @@
- `routers/todos.py`Todo 管理接口。 - `routers/todos.py`Todo 管理接口。
- `routers/temples.py`:寺院管理接口。 - `routers/temples.py`:寺院管理接口。
- `routers/products.py`:商品管理接口,按寺院隔离。 - `routers/products.py`:商品管理接口,按寺院隔离。
- `routers/feature_icons.py`:图标管理接口,按寺院隔离,支持跨寺院复制图标。
- `routers/ad_slots.py`:广告位管理接口,按寺院隔离,只维护广告位置定义。
- `routers/ads.py`:广告管理接口,按寺院隔离,广告内容绑定到广告位。
- `routers/orders.py`:订单管理接口,按寺院隔离。 - `routers/orders.py`:订单管理接口,按寺院隔离。
- `routers/payments.py`:微信支付接口,按寺院隔离发起 JSAPI 预下单并处理支付通知。 - `routers/payments.py`:微信支付接口,按寺院隔离发起 JSAPI 预下单并处理支付通知。
- `routers/deploy.py`:自动部署 webhook 接口,校验 Gitea 签名或部署 token 后触发前端构建脚本。 - `routers/deploy.py`:自动部署 webhook 接口,校验 Gitea 签名或部署 token 后触发前端构建脚本。
@@ -68,6 +71,14 @@ total_spent 总消费
名称、单价、库存、排序、是否超度 名称、单价、库存、排序、是否超度
``` ```
## 图标与广告位模型
- 图标存储在 `feature_icons` 表,字段包含图标名称、图标地址、跳转类型、跳转链接、关联商品、排序号、是否启用,并按 `temple_id` 隔离。
- 图标跳转类型为 `link``app``link` 必须填写跳转地址,`app` 必须选择当前寺院商品。
- 图标支持从其他寺院复制到当前寺院;应用类型图标复制后不直接跨寺院关联来源商品,需要重新选择当前寺院商品。
- 广告位存储在 `ad_slots` 表,表示首页广告、详情页广告、支付页广告等位置定义,字段包含名称、编码、说明、排序号、是否启用,并按 `temple_id` 隔离。
- 广告内容存储在 `ads` 表,字段包含广告位、广告名称、广告图、跳转类型、跳转链接、关联商品、排序号、是否启用,并按 `temple_id` 隔离。
## 上传与 OSS 配置 ## 上传与 OSS 配置
图片上传接口为 `/uploads/images`,用于商品封面、商品图片等图片资源。OSS 配置预留在 `config.py`,可直接填写或通过同名环境变量覆盖: 图片上传接口为 `/uploads/images`,用于商品封面、商品图片等图片资源。OSS 配置预留在 `config.py`,可直接填写或通过同名环境变量覆盖:
@@ -114,3 +125,5 @@ OSS_ENDPOINT、OSS_BUCKET_NAME、OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS
- 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,真实预下单和通知验签待填写微信商户配置后联调。 - 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,真实预下单和通知验签待填写微信商户配置后联调。
- 2026-07-27:新增自动部署 webhook 模块,`/deploy/webhook` 支持 Gitea HMAC 签名校验、部署分支过滤、并发锁和构建脚本超时控制,`scripts/deploy_front.sh` 负责拉取代码并构建前端。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。 - 2026-07-27:新增自动部署 webhook 模块,`/deploy/webhook` 支持 Gitea HMAC 签名校验、部署分支过滤、并发锁和构建脚本超时控制,`scripts/deploy_front.sh` 负责拉取代码并构建前端。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。
- 2026-07-27:自动部署 webhook 改为后台执行构建,接口在签名校验、分支检查和加锁后立即返回 `202 accepted`,后台任务将构建结果写入部署日志,避免 Gitea Delivery 因等待响应超时。验证:后端编译通过,webhook 后台任务触发测试通过。 - 2026-07-27:自动部署 webhook 改为后台执行构建,接口在签名校验、分支检查和加锁后立即返回 `202 accepted`,后台任务将构建结果写入部署日志,避免 Gitea Delivery 因等待响应超时。验证:后端编译通过,webhook 后台任务触发测试通过。
- 2026-07-27:新增图标管理和广告位管理后端模块,增加 `feature_icons``ad_slots` 表和 `/feature-icons``/ad-slots` 寺院隔离 CRUD 接口,图标支持从其他寺院复制。验证:后端编译通过。
- 2026-07-27:广告模型拆分为广告位和广告,`ad_slots` 改为位置定义,新增 `ads` 表和 `/ads` CRUD 接口,广告内容必须绑定当前寺院广告位。验证:后端编译通过,广告位与广告接口冒烟测试通过。
+92
View File
@@ -150,6 +150,41 @@ def ensure_product_schema(connection: sqlite3.Connection) -> None:
connection.execute(f"ALTER TABLE products ADD COLUMN {column_name} {definition}") connection.execute(f"ALTER TABLE products ADD COLUMN {column_name} {definition}")
def ensure_ad_slot_schema(connection: sqlite3.Connection) -> None:
columns = {
row["name"] if isinstance(row, sqlite3.Row) else row[1]
for row in connection.execute("PRAGMA table_info(ad_slots)").fetchall()
}
additions = {
"name": "TEXT",
"code": "TEXT",
"description": "TEXT",
"title": "TEXT",
"image_url": "TEXT",
"target_type": "TEXT NOT NULL DEFAULT 'link'",
"target_url": "TEXT",
"product_id": "INTEGER",
"sort_order": "INTEGER NOT NULL DEFAULT 0",
"is_active": "INTEGER NOT NULL DEFAULT 1",
}
for column_name, definition in additions.items():
if column_name not in columns:
connection.execute(f"ALTER TABLE ad_slots ADD COLUMN {column_name} {definition}")
connection.execute("UPDATE ad_slots SET name = title WHERE (name IS NULL OR name = '') AND title IS NOT NULL")
connection.execute("UPDATE ad_slots SET name = '广告位' || id WHERE name IS NULL OR name = ''")
connection.execute("UPDATE ad_slots SET code = 'slot_' || id WHERE code IS NULL OR code = ''")
connection.execute("UPDATE ad_slots SET sort_order = 0 WHERE sort_order IS NULL")
connection.execute("UPDATE ad_slots SET is_active = 1 WHERE is_active IS NULL")
connection.execute(
"""
CREATE UNIQUE INDEX IF NOT EXISTS idx_ad_slots_temple_code_unique
ON ad_slots(temple_id, code)
WHERE code IS NOT NULL AND code != ''
"""
)
def init_db() -> None: def init_db() -> None:
DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True) DATABASE_PATH.parent.mkdir(parents=True, exist_ok=True)
@@ -191,6 +226,63 @@ def init_db() -> None:
""" """
) )
ensure_product_schema(connection) ensure_product_schema(connection)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS feature_icons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
temple_id INTEGER NOT NULL,
title TEXT NOT NULL,
icon_url TEXT NOT NULL,
target_type TEXT NOT NULL DEFAULT 'link',
target_url TEXT,
product_id INTEGER,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (temple_id) REFERENCES temples(id),
FOREIGN KEY (product_id) REFERENCES products(id)
)
"""
)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS ad_slots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
temple_id INTEGER NOT NULL,
name TEXT,
code TEXT,
description TEXT,
title TEXT,
image_url TEXT,
target_type TEXT NOT NULL DEFAULT 'link',
target_url TEXT,
product_id INTEGER,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (temple_id) REFERENCES temples(id),
FOREIGN KEY (product_id) REFERENCES products(id)
)
"""
)
ensure_ad_slot_schema(connection)
connection.execute(
"""
CREATE TABLE IF NOT EXISTS ads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
temple_id INTEGER NOT NULL,
slot_id INTEGER NOT NULL,
title TEXT NOT NULL,
image_url TEXT NOT NULL,
target_type TEXT NOT NULL DEFAULT 'link',
target_url TEXT,
product_id INTEGER,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (temple_id) REFERENCES temples(id),
FOREIGN KEY (slot_id) REFERENCES ad_slots(id),
FOREIGN KEY (product_id) REFERENCES products(id)
)
"""
)
connection.execute( connection.execute(
""" """
CREATE TABLE IF NOT EXISTS ritual_services ( CREATE TABLE IF NOT EXISTS ritual_services (
+4 -1
View File
@@ -2,7 +2,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.database import lifespan from app.database import lifespan
from app.routers import auth, deploy, orders, payments, products, rituals, temples, todos, uploads, users from app.routers import ad_slots, ads, auth, deploy, feature_icons, orders, payments, products, rituals, temples, todos, uploads, users
app = FastAPI( app = FastAPI(
@@ -39,6 +39,9 @@ app.include_router(auth.router)
app.include_router(users.router) app.include_router(users.router)
app.include_router(temples.router) app.include_router(temples.router)
app.include_router(products.router) app.include_router(products.router)
app.include_router(feature_icons.router)
app.include_router(ad_slots.router)
app.include_router(ads.router)
app.include_router(orders.router) app.include_router(orders.router)
app.include_router(payments.router) app.include_router(payments.router)
app.include_router(rituals.router) app.include_router(rituals.router)
+146
View File
@@ -212,6 +212,152 @@ class UploadResponse(BaseModel):
object_key: str object_key: str
class FeatureIconBase(BaseModel):
title: str = Field(..., min_length=1, max_length=80)
icon_url: str = Field(..., min_length=1, max_length=500)
target_type: str = Field(default="link", pattern=r"^(link|app)$")
target_url: str | None = Field(default=None, max_length=500)
product_id: int | None = Field(default=None, ge=1)
sort_order: int = Field(default=0, ge=0)
is_active: bool = True
@field_validator("target_url")
@classmethod
def trim_target_url(cls, value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
return value or None
class FeatureIconCreate(FeatureIconBase):
pass
class FeatureIconUpdate(BaseModel):
title: str | None = Field(default=None, min_length=1, max_length=80)
icon_url: str | None = Field(default=None, min_length=1, max_length=500)
target_type: str | None = Field(default=None, pattern=r"^(link|app)$")
target_url: str | None = Field(default=None, max_length=500)
product_id: int | None = Field(default=None, ge=1)
sort_order: int | None = Field(default=None, ge=0)
is_active: bool | None = None
@field_validator("target_url")
@classmethod
def trim_target_url(cls, value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
return value or None
class FeatureIcon(BaseModel):
id: int
temple_id: int
title: str
icon_url: str
target_type: str
target_url: str | None = None
product_id: int | None = None
product_name: str | None = None
sort_order: int = 0
is_active: bool = True
class FeatureIconCopyRequest(BaseModel):
source_temple_id: int = Field(..., ge=1)
icon_ids: list[int] = Field(..., min_length=1)
class AdSlotBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
code: str = Field(..., min_length=1, max_length=80, pattern=r"^[a-zA-Z0-9_-]+$")
description: str | None = Field(default=None, max_length=300)
sort_order: int = Field(default=0, ge=0)
is_active: bool = True
class AdSlotCreate(AdSlotBase):
pass
class AdSlotUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=100)
code: str | None = Field(default=None, min_length=1, max_length=80, pattern=r"^[a-zA-Z0-9_-]+$")
description: str | None = Field(default=None, max_length=300)
sort_order: int | None = Field(default=None, ge=0)
is_active: bool | None = None
class AdSlot(BaseModel):
id: int
temple_id: int
name: str
code: str
description: str | None = None
sort_order: int = 0
is_active: bool = True
class AdBase(BaseModel):
slot_id: int = Field(..., ge=1)
title: str = Field(..., min_length=1, max_length=100)
image_url: str = Field(..., min_length=1, max_length=500)
target_type: str = Field(default="link", pattern=r"^(link|app)$")
target_url: str | None = Field(default=None, max_length=500)
product_id: int | None = Field(default=None, ge=1)
sort_order: int = Field(default=0, ge=0)
is_active: bool = True
@field_validator("target_url")
@classmethod
def trim_target_url(cls, value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
return value or None
class AdCreate(AdBase):
pass
class AdUpdate(BaseModel):
slot_id: int | None = Field(default=None, ge=1)
title: str | None = Field(default=None, min_length=1, max_length=100)
image_url: str | None = Field(default=None, min_length=1, max_length=500)
target_type: str | None = Field(default=None, pattern=r"^(link|app)$")
target_url: str | None = Field(default=None, max_length=500)
product_id: int | None = Field(default=None, ge=1)
sort_order: int | None = Field(default=None, ge=0)
is_active: bool | None = None
@field_validator("target_url")
@classmethod
def trim_target_url(cls, value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
return value or None
class Ad(BaseModel):
id: int
temple_id: int
slot_id: int
slot_name: str | None = None
slot_code: str | None = None
title: str
image_url: str
target_type: str
target_url: str | None = None
product_id: int | None = None
product_name: str | None = None
sort_order: int = 0
is_active: bool = True
class RitualServiceCreate(BaseModel): class RitualServiceCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=120) name: str = Field(..., min_length=1, max_length=120)
description: str | None = Field(default=None, max_length=500) description: str | None = Field(default=None, max_length=500)
+14
View File
@@ -18,6 +18,9 @@
- `src/views/temples/TempleManagement.vue`:寺院管理页。 - `src/views/temples/TempleManagement.vue`:寺院管理页。
- `src/views/products/ProductManagement.vue`:商品管理列表页,按当前寺院隔离。 - `src/views/products/ProductManagement.vue`:商品管理列表页,按当前寺院隔离。
- `src/views/products/ProductDetail.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/orders/OrderManagement.vue`:订单管理页,按当前寺院隔离。
- `src/views/rituals/RitualManagement.vue`:法事管理页,按当前寺院隔离。 - `src/views/rituals/RitualManagement.vue`:法事管理页,按当前寺院隔离。
- `src/views/users/UserManagement.vue`:用户管理页。 - `src/views/users/UserManagement.vue`:用户管理页。
@@ -26,6 +29,9 @@
- `src/api/users.ts`:用户接口封装。 - `src/api/users.ts`:用户接口封装。
- `src/api/temples.ts`:寺院接口封装。 - `src/api/temples.ts`:寺院接口封装。
- `src/api/products.ts`:商品接口封装。 - `src/api/products.ts`:商品接口封装。
- `src/api/featureIcons.ts`:图标管理接口封装。
- `src/api/adSlots.ts`:广告位管理接口封装。
- `src/api/ads.ts`:广告管理接口封装。
- `src/api/uploads.ts`:图片上传接口封装。 - `src/api/uploads.ts`:图片上传接口封装。
- `src/api/orders.ts`:订单接口封装。 - `src/api/orders.ts`:订单接口封装。
- `src/api/payments.ts`:微信支付接口封装。 - `src/api/payments.ts`:微信支付接口封装。
@@ -39,12 +45,16 @@
/products 商品管理 /products 商品管理
/products/create 新建商品 /products/create 新建商品
/products/:productId/edit 编辑商品 /products/:productId/edit 编辑商品
/feature-icons 图标管理
/ad-slots 广告位管理
/ads 广告管理
/orders 订单管理 /orders 订单管理
/ritual-services 法事管理 /ritual-services 法事管理
/users 用户管理 /users 用户管理
``` ```
商品、订单、法事页面使用当前寺院上下文访问数据。当前寺院由后台顶部选择器维护,`src/api/http.ts` 会把 `currentTempleId` 写入请求头 `X-Temple-Id` 商品、订单、法事页面使用当前寺院上下文访问数据。当前寺院由后台顶部选择器维护,`src/api/http.ts` 会把 `currentTempleId` 写入请求头 `X-Temple-Id`
图标、广告位和广告页面也使用当前寺院上下文访问数据;广告位表示首页广告、详情页广告、支付页广告等位置,广告内容必须绑定到广告位;图标同步功能从其他寺院读取图标后复制到当前寺院。
后台页面通过路由守卫检查登录状态;认证令牌只存在后端设置的 HttpOnly Cookie 中,前端 localStorage 只缓存当前用户展示信息,不保存 token。 后台页面通过路由守卫检查登录状态;认证令牌只存在后端设置的 HttpOnly Cookie 中,前端 localStorage 只缓存当前用户展示信息,不保存 token。
@@ -72,6 +82,7 @@
- 新增前端依赖时只写入 `package.json`,由用户执行 `pnpm install` - 新增前端依赖时只写入 `package.json`,由用户执行 `pnpm install`
- `node_modules` 不提交。 - `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:商品编辑基础信息新增商品描述字段,前端商品 API 类型和商品编辑表单同步 `description`。验证:`pnpm typecheck``pnpm build` 通过。
- 2026-07-23:订单管理页新增微信 JSAPI 预下单入口,新增 `src/api/payments.ts` 调用 `/payments/wechat/jsapi`,用于生成支付参数;实际微信调起仍需用户端页面和 openid。验证:`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:移除 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"> <script setup lang="ts">
import { import {
BankOutlined, BankOutlined,
AppstoreOutlined,
FileTextOutlined, FileTextOutlined,
LoginOutlined, LoginOutlined,
PictureOutlined,
ProfileOutlined, ProfileOutlined,
ShoppingOutlined, ShoppingOutlined,
TeamOutlined, TeamOutlined,
@@ -94,6 +96,18 @@ onMounted(fetchTemples);
<template #icon><ShoppingOutlined /></template> <template #icon><ShoppingOutlined /></template>
商品管理 商品管理
</a-menu-item> </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')"> <a-menu-item key="orders" @click="openRoute('/orders')">
<template #icon><FileTextOutlined /></template> <template #icon><FileTextOutlined /></template>
订单管理 订单管理
@@ -120,10 +134,18 @@ onMounted(fetchTemples);
placeholder="选择寺院" placeholder="选择寺院"
@change="changeTemple" @change="changeTemple"
/> />
<a-button type="link" @click="submitLogout"> <a-popconfirm
<template #icon><LoginOutlined /></template> title="确认退出登录?"
退出登录 ok-text="退出"
</a-button> cancel-text="取消"
placement="bottomRight"
@confirm="submitLogout"
>
<a-button type="link">
<template #icon><LoginOutlined /></template>
退出登录
</a-button>
</a-popconfirm>
</a-space> </a-space>
</a-layout-header> </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 { createApp } from 'vue';
import 'ant-design-vue/dist/reset.css'; import 'ant-design-vue/dist/reset.css';
@@ -10,4 +33,35 @@ import router from './router';
document.title = appConfig.name; 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 { createRouter, createWebHistory } from 'vue-router';
import { clearAuthUser, getCurrentUser, getStoredUser, storeAuthUser } from '@/api/auth'; import { clearAuthUser, getCurrentUser, getStoredUser, storeAuthUser } from '@/api/auth';
import AdminLayout from '@/layouts/AdminLayout.vue';
import LoginPage from '@/views/login/LoginPage.vue'; const AdminLayout = () => import('@/layouts/AdminLayout.vue');
import OrderManagement from '@/views/orders/OrderManagement.vue'; const AdSlotManagement = () => import('@/views/ad-slots/AdSlotManagement.vue');
import ProductDetail from '@/views/products/ProductDetail.vue'; const AdManagement = () => import('@/views/ads/AdManagement.vue');
import ProductManagement from '@/views/products/ProductManagement.vue'; const FeatureIconManagement = () => import('@/views/feature-icons/FeatureIconManagement.vue');
import RitualManagement from '@/views/rituals/RitualManagement.vue'; const LoginPage = () => import('@/views/login/LoginPage.vue');
import TempleManagement from '@/views/temples/TempleManagement.vue'; const OrderManagement = () => import('@/views/orders/OrderManagement.vue');
import UserManagement from '@/views/users/UserManagement.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({ export const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
@@ -68,6 +72,33 @@ export const router = createRouter({
title: '编辑商品', 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', path: 'orders',
name: 'orders', name: 'orders',
+165 -150
View File
@@ -1,206 +1,221 @@
:root { :root {
color: #1f2937; color: #1f2937;
font-family: font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Inter,
sans-serif; ui-sans-serif,
background: #f3f5f8; system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background: #f3f5f8;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
margin: 0; margin: 0;
min-width: 320px; min-width: 320px;
} }
.app-shell { .app-shell {
min-height: 100vh; min-height: 100vh;
} }
.sidebar { .sidebar {
background: #fff !important; background: #fff !important;
border-right: 1px solid #e5e7eb; border-right: 1px solid #e5e7eb;
} }
.brand { .brand {
align-items: center; align-items: center;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
display: flex; display: flex;
gap: 12px; gap: 12px;
height: 44px; height: 44px;
padding: 0 20px; padding: 0 20px;
} }
.brand-mark { .brand-mark {
align-items: center; align-items: center;
background: #1677ff; background: #1677ff;
border-radius: 6px; border-radius: 6px;
color: #fff; color: #fff;
display: flex; display: flex;
font-weight: 700; font-weight: 700;
height: 32px; height: 32px;
justify-content: center; justify-content: center;
width: 32px; width: 32px;
} }
.brand-name { .brand-name {
color: #111827; color: #111827;
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
line-height: 20px; line-height: 20px;
} }
.brand-subtitle { .brand-subtitle {
color: #6b7280; color: #6b7280;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
} }
.side-menu { .side-menu {
background: transparent; background: transparent;
border-inline-end: 0 !important; border-inline-end: 0 !important;
} }
.topbar { .topbar {
align-items: center; align-items: center;
background: #fff !important; background: #fff !important;
border-bottom: 1px solid #e5e7eb; border-bottom: 1px solid #e5e7eb;
color: #1f2937; color: #1f2937;
display: flex; display: flex;
height: 64px; justify-content: space-between;
justify-content: space-between; padding: 0 14px;
padding: 0 24px; height: 44px !important;
} }
.content { .content {
padding: 14px; padding: 14px;
} }
.breadcrumb { .breadcrumb {
margin-bottom: 14px; margin-bottom: 14px;
} }
.temple-selector { .temple-selector {
width: 160px; width: 160px;
} }
.workspace { .workspace {
background: #fff; background: #fff;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 8px; border-radius: 8px;
padding: 20px; padding: 20px;
} }
.page-heading { .page-heading {
align-items: center; align-items: center;
display: flex; display: flex;
gap: 16px; gap: 16px;
justify-content: space-between; justify-content: space-between;
margin-bottom: 18px; margin-bottom: 18px;
} }
.page-heading h1 { .page-heading h1 {
font-size: 20px; font-size: 20px;
line-height: 28px; line-height: 28px;
margin: 0; margin: 0;
} }
.page-heading p { .page-heading p {
color: #6b7280; color: #6b7280;
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
margin: 4px 0 0; margin: 4px 0 0;
} }
.query-bar { .query-bar {
margin-bottom: 16px; margin-bottom: 16px;
} }
.table-toolbar { .table-toolbar {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-bottom: 12px; 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 { .login-page {
align-items: center; align-items: center;
background: background: linear-gradient(135deg, rgba(22, 119, 255, 0.12), transparent 36%), #f3f5f8;
linear-gradient(135deg, rgba(22, 119, 255, 0.12), transparent 36%), display: flex;
#f3f5f8; min-height: 100vh;
display: flex; padding: 24px;
min-height: 100vh;
padding: 24px;
} }
.login-panel { .login-panel {
background: #fff; background: #fff;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08); box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08);
margin: 0 auto; margin: 0 auto;
max-width: 420px; max-width: 420px;
padding: 28px; padding: 28px;
width: 100%; width: 100%;
} }
.login-brand { .login-brand {
align-items: center; align-items: center;
display: flex; display: flex;
gap: 14px; gap: 14px;
margin-bottom: 28px; margin-bottom: 28px;
} }
.login-brand h1 { .login-brand h1 {
font-size: 22px; font-size: 22px;
line-height: 30px; line-height: 30px;
margin: 0; margin: 0;
} }
.login-brand p { .login-brand p {
color: #6b7280; color: #6b7280;
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
margin: 2px 0 0; margin: 2px 0 0;
} }
.login-form { .login-form {
width: 100%; width: 100%;
} }
.login-options { .login-options {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin: -4px 0 18px; margin: -4px 0 18px;
} }
.captcha-row { .captcha-row {
display: grid; display: grid;
gap: 10px; gap: 10px;
grid-template-columns: minmax(0, 1fr) 132px; grid-template-columns: minmax(0, 1fr) 132px;
} }
.empty-icon { .empty-icon {
color: #1677ff; color: #1677ff;
font-size: 56px; font-size: 56px;
} }
.full-width-control { .full-width-control {
width: 100%; width: 100%;
} }
.upload-text { .upload-text {
margin-top: 8px; margin-top: 8px;
} }
.product-detail-page { .product-detail-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 142px); height: calc(100vh - 142px);
height: calc(100dvh - 142px); height: calc(100dvh - 142px);
overflow: hidden; overflow: hidden;
} }
.product-detail-body, .product-detail-body,
@@ -208,72 +223,72 @@ body {
.product-detail-body .ant-spin-container, .product-detail-body .ant-spin-container,
.product-detail-form, .product-detail-form,
.product-detail-tabs { .product-detail-tabs {
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
} }
.product-detail-tabs .ant-tabs-content-holder { .product-detail-tabs .ant-tabs-content-holder {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
} }
.product-detail-tabs .ant-tabs-content { .product-detail-tabs .ant-tabs-content {
height: 100%; height: 100%;
} }
.product-detail-tabs .ant-tabs-tabpane { .product-detail-tabs .ant-tabs-tabpane {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
.product-rich-editor { .product-rich-editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
.product-rich-editor .ql-container { .product-rich-editor .ql-container {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.product-rich-editor .ql-editor { .product-rich-editor .ql-editor {
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.app-shell { .app-shell {
display: block; display: block;
} }
.sidebar { .sidebar {
display: none; display: none;
} }
.topbar, .topbar,
.page-heading { .page-heading {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
} }
.content { .content {
padding: 2px; padding: 2px;
} }
.workspace { .workspace {
padding: 14px; padding: 14px;
} }
.product-rich-editor { .product-rich-editor {
min-height: 360px; min-height: 360px;
} }
.login-page { .login-page {
padding: 16px; padding: 16px;
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { LockOutlined, ReloadOutlined, SafetyCertificateOutlined, UserOutlined } from '@ant-design/icons-vue'; 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 { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'; 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 { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table'; import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
+1 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ArrowLeftOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons-vue'; 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 { Rule } from 'ant-design-vue/es/form';
import type { UploadFile } from 'ant-design-vue/es/upload/interface'; import type { UploadFile } from 'ant-design-vue/es/upload/interface';
import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface'; import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'; 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 type { ColumnsType } from 'ant-design-vue/es/table';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'; 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 { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table'; import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'; 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 { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table'; import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, onMounted, reactive, ref } from 'vue';
+2 -1
View File
@@ -6,7 +6,8 @@ import {
ReloadOutlined, ReloadOutlined,
UserOutlined, UserOutlined,
} from '@ant-design/icons-vue'; } 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 { Rule } from 'ant-design-vue/es/form';
import type { ColumnsType } from 'ant-design-vue/es/table'; import type { ColumnsType } from 'ant-design-vue/es/table';
import { computed, onMounted, reactive, ref } from 'vue'; 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 { fileURLToPath, URL } from 'node:url';
import { defineConfig, loadEnv } from 'vite'; 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 }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), ''); const env = loadEnv(mode, process.cwd(), '');
const apiTarget = env.VITE_API_TARGET || 'http://127.0.0.1:8000'; const apiTarget = env.VITE_API_TARGET || 'http://127.0.0.1:8000';
return { return {
build: {
chunkSizeWarningLimit: 900,
rollupOptions: {
output: {
manualChunks,
},
},
},
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {