update
This commit is contained in:
@@ -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 shaking,Vite `manualChunks` 拆分 Vue、Antd、图标、富文本编辑器和公共依赖包,并按实际常用组件包设置体积提示阈值。验证:`pnpm typecheck` 和 `pnpm build` 通过。
|
||||||
|
|||||||
@@ -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 接口,广告内容必须绑定当前寺院广告位。验证:后端编译通过,广告位与广告接口冒烟测试通过。
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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` 通过。
|
||||||
|
|||||||
@@ -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
|
||||||
|
title="确认退出登录?"
|
||||||
|
ok-text="退出"
|
||||||
|
cancel-text="取消"
|
||||||
|
placement="bottomRight"
|
||||||
|
@confirm="submitLogout"
|
||||||
|
>
|
||||||
|
<a-button type="link">
|
||||||
<template #icon><LoginOutlined /></template>
|
<template #icon><LoginOutlined /></template>
|
||||||
退出登录
|
退出登录
|
||||||
</a-button>
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-layout-header>
|
</a-layout-header>
|
||||||
|
|
||||||
|
|||||||
+56
-2
@@ -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');
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
+21
-6
@@ -1,7 +1,12 @@
|
|||||||
:root {
|
:root {
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
font-family:
|
font-family:
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
Inter,
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
background: #f3f5f8;
|
background: #f3f5f8;
|
||||||
}
|
}
|
||||||
@@ -69,9 +74,9 @@ body {
|
|||||||
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 24px;
|
padding: 0 14px;
|
||||||
|
height: 44px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -124,11 +129,21 @@ body {
|
|||||||
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%),
|
|
||||||
#f3f5f8;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
{"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"}
|
||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user