feat(attraction): 实现景点和分类管理功能

- 新增景点分页查询接口和按ID查询功能
- 完善景点分类树形结构管理和父子分类关联
- 优化景点管理页面实现分页、搜索和表格展示
- 添加景点分类层级显示和树形选择器组件
- 集成富文本编辑器用于景点详情内容编辑
- 重构API调用方式统一使用RESTful风格接口
- 更新数据库配置添加MyBatis日志输出支持
- 调整开发环境配置密码和日志级别设置
This commit is contained in:
wyh 2026-05-27 23:02:01 +08:00
parent 47106d88c8
commit 3e72c5b6c7
12 changed files with 1310 additions and 125 deletions

111
AGENTS.md Normal file
View File

@ -0,0 +1,111 @@
# AGENTS.md — Tourism Promotion Management System
> 旅游宣传管理系统 — 毕业设计项目. Full-stack tourism platform with admin dashboard and user-facing web app.
## Project Structure
Three independent subsystems in one repo (not a monorepo with shared packages):
| Directory | Stack | Dev Port | UI Framework |
|-----------|-------|----------|--------------|
| `tourism-server/` | SpringBoot 4.0.6 + JDK 21 | 8080 | — |
| `tourism-admin/` | Vue 3.5 + Vite 6 + TS 5.7 | 3000 | **Ant Design Vue 4** |
| `tourism-web/` | Vue 3.5 + Vite 6 + TS 5.7 | 3001 | **Element Plus 2.9** |
**Critical**: `tourism-admin` and `tourism-web` use **different UI frameworks**. Do not swap components between them.
## Development Commands
### Backend (`tourism-server/`)
```bash
mvn install -DskipTests
mvn spring-boot:run
```
- Main class: `com.tourism.TourismApplication`
- Application config: `application.yml` + `application-dev.yml`
- Active profile: `dev`
- Maven uses Aliyun + HuaweiCloud + Spring Milestones/Snapshots repositories
### Admin Frontend (`tourism-admin/`)
```bash
pnpm install # packageManager is pnpm@10.23.0
pnpm dev # port 3000
```
- Uses **pnpm** (not npm/yarn). `packageManager` field enforces this.
- Build: `pnpm build` (runs `vue-tsc -b && vite build`)
- Lint: `pnpm lint` (eslint with .vue,.ts extensions)
### Web Frontend (`tourism-web/`)
```bash
pnpm install
pnpm dev # port 3001
```
- Build: `pnpm build` (runs `vue-tsc -b && vite build`)
- No lint script configured
## API & Auth
- Both frontends proxy `/api` to `http://localhost:8080`
- Auth: **Sa-Token** (v1.45.0). Token sent in `token` header (not `Authorization`)
- Admin login mock: username `admin` / password `123456`
- Backend response format: `{ code: 200, msg: "...", data: ... }`
- 401 unauthenticated → redirect to login page
## Backend Architecture
- Package root: `com.tourism`
- Modules by domain: `user/`, `attraction/`, `product/`, `order/`, `travel/`, `admin/`, `payment/`, `file/`, `common/`
- Each module: `entity/`, `mapper/`, `service/`, `service/impl/`, `controller/`, `dto/`
- ORM: MyBatis-Plus 3.5.15 with logical delete (`isDeleted` field, 0=not deleted, 1=deleted)
- DB: MySQL 8.0 (`tourism_platform`), init script: `tourism_platform.sql` at repo root
- Passwords encrypted with `spring-security-crypto` (BCrypt)
- File storage: RustFS (configured in `application.yml`)
- Payment: Alipay sandbox SDK
- JSON: Fastjson2
- DTO mapping: MapStruct 1.6.3 + Lombok (both require annotation processor in Maven)
- API docs: Knife4j (Swagger UI alternative), available when server runs
## Database Setup
```bash
mysql -u root -p < tourism_platform.sql
```
Default credentials (override in `application-dev.yml`):
- URL: `jdbc:mysql://localhost:3306/tourism_platform`
- Username: `root`
- Password: `root` (in base config), `123456` (in dev profile)
## Frontend Architecture
### tourism-admin
- Entry: `src/main.ts`
- Router: `src/router/index.ts` (all routes in one file)
- State: Pinia (`stores/user.ts`, `stores/app.ts`)
- Icons: `@ant-design/icons-vue` (all registered globally)
- Charts: ECharts + vue-echarts
- **MockJS**: `src/mock/index.ts` provides mock data for all admin APIs. Imported in `main.ts`.
- Styles: SCSS (`src/assets/styles/index.scss`)
### tourism-web
- Entry: `src/main.ts`
- Router: `src/router/index.ts`
- State: Pinia (`stores/user.ts`, `stores/cart.ts`)
- Icons: `@element-plus/icons-vue`
- Styles: SCSS (`src/assets/styles/index.scss`)
## Design Systems
Design specs are in `design-system/` directory:
- `design-system/tourism-admin/MASTER.md` — Purple theme (#7C3AED), Fira Code/Sans, data-dense dashboard style
- `design-system/tourism-web/MASTER.md` — Sky blue theme (#0EA5E9), Noto Sans Thai, motion-driven storytelling style
## Important Notes
- **No tests exist** in any subsystem. Add tests if required.
- **No CI/CD** configured.
- Both frontends use `noUnusedLocals: false` and `noUnusedParameters: false` in tsconfig.
- Admin uses `unplugin-auto-import` and `unplugin-vue-components` (auto-import Vue APIs and components).
- All `@/` aliases resolve to `src/`.
- `tourism_platform.sql` at repo root is the single source of truth for database schema.
- This is a graduation project (毕业设计). Code may be simplified; prioritize clarity and functionality over enterprise patterns.

View File

@ -11,6 +11,8 @@
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"ant-design-vue": "^4.2.6",
"axios": "^1.7.0",
"dayjs": "^1.11.0",
@ -30,5 +32,6 @@
"unplugin-vue-components": "^32.1.0",
"vite": "^6.1.0",
"vue-tsc": "^2.2.0"
}
},
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
}

View File

@ -11,6 +11,12 @@ importers:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.34(typescript@5.7.3))
'@wangeditor/editor':
specifier: ^5.1.23
version: 5.1.23
'@wangeditor/editor-for-vue':
specifier: ^5.1.12
version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@5.7.3))
ant-design-vue:
specifier: ^4.2.6
version: 4.2.6(vue@3.5.34(typescript@5.7.3))
@ -309,42 +315,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
@ -402,79 +402,66 @@ packages:
resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.4':
resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.4':
resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.4':
resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.4':
resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.4':
resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.4':
resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.4':
resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.4':
resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.4':
resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.4':
resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.4':
resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.4':
resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.4':
resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==}
@ -509,12 +496,35 @@ packages:
'@simonwep/pickr@1.8.2':
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
'@transloadit/prettier-bytes@0.0.7':
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/event-emitter@0.3.5':
resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
'@types/node@25.9.1':
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
'@uppy/companion-client@2.2.2':
resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
'@uppy/core@2.3.4':
resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==}
'@uppy/store-default@2.1.1':
resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==}
'@uppy/utils@4.1.3':
resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==}
'@uppy/xhr-upload@2.1.3':
resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==}
peerDependencies:
'@uppy/core': ^2.3.3
'@vitejs/plugin-vue@5.2.4':
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -574,6 +584,93 @@ packages:
'@vue/shared@3.5.34':
resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==}
'@wangeditor/basic-modules@1.1.7':
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.throttle: ^4.1.1
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/code-highlight@1.0.3':
resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/core@1.1.19':
resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==}
peerDependencies:
'@uppy/core': ^2.1.1
'@uppy/xhr-upload': ^2.0.3
dom7: ^3.0.0
is-hotkey: ^0.2.0
lodash.camelcase: ^4.3.0
lodash.clonedeep: ^4.5.0
lodash.debounce: ^4.0.8
lodash.foreach: ^4.5.0
lodash.isequal: ^4.5.0
lodash.throttle: ^4.1.1
lodash.toarray: ^4.4.0
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/editor-for-vue@5.1.12':
resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==}
peerDependencies:
'@wangeditor/editor': '>=5.1.0'
vue: ^3.0.5
'@wangeditor/editor@5.1.23':
resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==}
'@wangeditor/list-module@1.0.5':
resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/table-module@1.1.4':
resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.isequal: ^4.5.0
lodash.throttle: ^4.1.1
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/upload-image-module@1.0.2':
resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==}
peerDependencies:
'@uppy/core': ^2.0.3
'@uppy/xhr-upload': ^2.0.3
'@wangeditor/basic-modules': 1.x
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.foreach: ^4.5.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/video-module@1.1.4':
resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==}
peerDependencies:
'@uppy/core': ^2.1.4
'@uppy/xhr-upload': ^2.0.7
'@wangeditor/core': 1.x
dom7: ^3.0.0
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
@ -641,6 +738,10 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'}
dayjs@1.11.21:
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
@ -670,6 +771,9 @@ packages:
dom-scroll-into-view@2.0.1:
resolution: {integrity: sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==}
dom7@3.0.0:
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@ -697,6 +801,17 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
es5-ext@0.10.64:
resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
engines: {node: '>=0.10'}
es6-iterator@2.0.3:
resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
es6-symbol@3.1.4:
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
engines: {node: '>=0.12'}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
@ -706,15 +821,25 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
esniff@2.0.1:
resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
engines: {node: '>=0.10'}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
event-emitter@0.3.5:
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
ext@1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@ -773,10 +898,19 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
html-void-elements@2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
i18next@20.6.1:
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
immutable@5.1.5:
resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==}
@ -788,10 +922,20 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-hotkey@0.2.0:
resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==}
is-plain-object@3.0.1:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -805,6 +949,28 @@ packages:
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.clonedeep@4.5.0:
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.foreach@4.5.0:
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash.toarray@4.4.0:
resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@ -823,6 +989,9 @@ packages:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-match@1.0.2:
resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
@ -844,6 +1013,9 @@ packages:
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
namespace-emitter@2.0.1:
resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -852,6 +1024,9 @@ packages:
nanopop@2.4.2:
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
next-tick@1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
@ -890,6 +1065,13 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
preact@10.29.2:
resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==}
prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
@ -923,10 +1105,25 @@ packages:
shallow-equal@1.2.1:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
slate-history@0.66.0:
resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==}
peerDependencies:
slate: '>=0.65.3'
slate@0.72.8:
resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==}
snabbdom@3.6.3:
resolution: {integrity: sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==}
engines: {node: '>=12.17.0'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
ssr-window@3.0.0:
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
@ -937,6 +1134,9 @@ packages:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
@ -944,6 +1144,9 @@ packages:
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
type@2.7.3:
resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
typescript@5.7.3:
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
engines: {node: '>=14.17'}
@ -1099,6 +1302,9 @@ packages:
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
wildcard@1.1.2:
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
@ -1375,12 +1581,45 @@ snapshots:
core-js: 3.49.0
nanopop: 2.4.2
'@transloadit/prettier-bytes@0.0.7': {}
'@types/estree@1.0.8': {}
'@types/event-emitter@0.3.5': {}
'@types/node@25.9.1':
dependencies:
undici-types: 7.24.6
'@uppy/companion-client@2.2.2':
dependencies:
'@uppy/utils': 4.1.3
namespace-emitter: 2.0.1
'@uppy/core@2.3.4':
dependencies:
'@transloadit/prettier-bytes': 0.0.7
'@uppy/store-default': 2.1.1
'@uppy/utils': 4.1.3
lodash.throttle: 4.1.1
mime-match: 1.0.2
namespace-emitter: 2.0.1
nanoid: 3.3.12
preact: 10.29.2
'@uppy/store-default@2.1.1': {}
'@uppy/utils@4.1.3':
dependencies:
lodash.throttle: 4.1.1
'@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)':
dependencies:
'@uppy/companion-client': 2.2.2
'@uppy/core': 2.3.4
'@uppy/utils': 4.1.3
nanoid: 3.3.12
'@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@25.9.1)(sass@1.100.0))(vue@3.5.34(typescript@5.7.3))':
dependencies:
vite: 6.4.2(@types/node@25.9.1)(sass@1.100.0)
@ -1472,6 +1711,114 @@ snapshots:
'@vue/shared@3.5.34': {}
'@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
dom7: 3.0.0
is-url: 1.2.4
lodash.throttle: 4.1.1
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.3
'@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
dom7: 3.0.0
prismjs: 1.30.0
slate: 0.72.8
snabbdom: 3.6.3
'@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@types/event-emitter': 0.3.5
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
dom7: 3.0.0
event-emitter: 0.3.5
html-void-elements: 2.0.1
i18next: 20.6.1
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8
lodash.foreach: 4.5.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.12
scroll-into-view-if-needed: 2.2.31
slate: 0.72.8
slate-history: 0.66.0(slate@0.72.8)
snabbdom: 3.6.3
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@5.7.3))':
dependencies:
'@wangeditor/editor': 5.1.23
vue: 3.5.34(typescript@5.7.3)
'@wangeditor/editor@5.1.23':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
dom7: 3.0.0
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8
lodash.foreach: 4.5.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.3
'@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
dom7: 3.0.0
slate: 0.72.8
snabbdom: 3.6.3
'@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
dom7: 3.0.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.3
'@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
dom7: 3.0.0
lodash.foreach: 4.5.0
slate: 0.72.8
snabbdom: 3.6.3
'@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3))(dom7@3.0.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.3)
dom7: 3.0.0
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.3
acorn@8.16.0: {}
agent-base@6.0.2:
@ -1555,6 +1902,11 @@ snapshots:
csstype@3.2.3: {}
d@1.0.2:
dependencies:
es5-ext: 0.10.64
type: 2.7.3
dayjs@1.11.21: {}
de-indent@1.0.2: {}
@ -1572,6 +1924,10 @@ snapshots:
dom-scroll-into-view@2.0.1: {}
dom7@3.0.0:
dependencies:
ssr-window: 3.0.0
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@ -1600,6 +1956,24 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.3
es5-ext@0.10.64:
dependencies:
es6-iterator: 2.0.3
es6-symbol: 3.1.4
esniff: 2.0.1
next-tick: 1.1.0
es6-iterator@2.0.3:
dependencies:
d: 1.0.2
es5-ext: 0.10.64
es6-symbol: 3.1.4
es6-symbol@3.1.4:
dependencies:
d: 1.0.2
ext: 1.7.0
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
@ -1631,14 +2005,30 @@ snapshots:
escape-string-regexp@5.0.0: {}
esniff@2.0.1:
dependencies:
d: 1.0.2
es5-ext: 0.10.64
event-emitter: 0.3.5
type: 2.7.3
estree-walker@2.0.2: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
event-emitter@0.3.5:
dependencies:
d: 1.0.2
es5-ext: 0.10.64
exsolve@1.0.8: {}
ext@1.7.0:
dependencies:
type: 2.7.3
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
@ -1690,6 +2080,8 @@ snapshots:
he@1.2.0: {}
html-void-elements@2.0.1: {}
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
@ -1697,6 +2089,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
i18next@20.6.1:
dependencies:
'@babel/runtime': 7.29.7
immer@9.0.21: {}
immutable@5.1.5: {}
is-extglob@2.1.1:
@ -1707,8 +2105,14 @@ snapshots:
is-extglob: 2.1.1
optional: true
is-hotkey@0.2.0: {}
is-plain-object@3.0.1: {}
is-plain-object@5.0.0: {}
is-url@1.2.4: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@ -1721,6 +2125,20 @@ snapshots:
lodash-es@4.18.1: {}
lodash.camelcase@4.3.0: {}
lodash.clonedeep@4.5.0: {}
lodash.debounce@4.0.8: {}
lodash.foreach@4.5.0: {}
lodash.isequal@4.5.0: {}
lodash.throttle@4.1.1: {}
lodash.toarray@4.4.0: {}
lodash@4.18.1: {}
loose-envify@1.4.0:
@ -1735,6 +2153,10 @@ snapshots:
mime-db@1.52.0: {}
mime-match@1.0.2:
dependencies:
wildcard: 1.1.2
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
@ -1758,10 +2180,14 @@ snapshots:
muggle-string@0.4.1: {}
namespace-emitter@2.0.1: {}
nanoid@3.3.12: {}
nanopop@2.4.2: {}
next-tick@1.1.0: {}
node-addon-api@7.1.1:
optional: true
@ -1803,6 +2229,10 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
preact@10.29.2: {}
prismjs@1.30.0: {}
proxy-from-env@2.1.0: {}
quansync@0.2.11: {}
@ -1858,8 +2288,23 @@ snapshots:
shallow-equal@1.2.1: {}
slate-history@0.66.0(slate@0.72.8):
dependencies:
is-plain-object: 5.0.0
slate: 0.72.8
slate@0.72.8:
dependencies:
immer: 9.0.21
is-plain-object: 5.0.0
tiny-warning: 1.0.3
snabbdom@3.6.3: {}
source-map-js@1.2.1: {}
ssr-window@3.0.0: {}
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
@ -1868,6 +2313,8 @@ snapshots:
throttle-debounce@5.0.2: {}
tiny-warning@1.0.3: {}
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
@ -1875,6 +2322,8 @@ snapshots:
tslib@2.3.0: {}
type@2.7.3: {}
typescript@5.7.3: {}
ufo@1.6.4: {}
@ -2003,6 +2452,8 @@ snapshots:
webpack-virtual-modules@0.6.2: {}
wildcard@1.1.2: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0

View File

@ -1,29 +1,40 @@
import http from './index'
export function getAttractionList() {
return http.get('/attraction/list')
// 景点管理
export function getAttractionPage(params: any) {
return http.get('/attraction/page', { params })
}
export function getAttractionById(id: number) {
return http.get(`/attraction/${id}`)
}
export function createAttraction(data: any) {
return http.post('/attraction/create', data)
return http.post('/attraction', data)
}
export function updateAttraction(data: any) {
return http.post('/attraction/update', data)
return http.put('/attraction', data)
}
export function deleteAttraction(id: number) {
return http.post('/attraction/delete', { id })
return http.delete(`/attraction/${id}`)
}
// 景点分类管理
export function getAttractionCategoryList() {
return http.get('/attraction/category/list')
}
export function getAttractionCategoryTree() {
return http.get('/attraction/category/tree')
}
export function getAttractionCategoryById(id: number) {
return http.get(`/attraction/category/${id}`)
}
export function createAttractionCategory(data: any) {
return http.post('/attraction/category/create', data)
return http.post('/attraction/category', data)
}
export function updateAttractionCategory(data: any) {
return http.post('/attraction/category/update', data)
return http.put('/attraction/category', data)
}
export function deleteAttractionCategory(id: number) {
return http.post('/attraction/category/delete', { id })
return http.delete(`/attraction/category/${id}`)
}
export function getTicketList() {

View File

@ -130,21 +130,28 @@ Mock.mock(/\/api\/user\/update/, 'post', () => success(null))
Mock.mock(/\/api\/user\/status/, 'post', () => success(null))
// 景点管理
Mock.mock(/\/api\/attraction\/list/, 'get', () => {
// 景点管理 - 分页列表
Mock.mock(/\/api\/attraction\/page/, 'get', () => {
const list = Mock.mock({
'records|10-15': [
{
id: '@id',
name: '@pick(["黄山风景区", "故宫博物院", "西湖景区", "张家界国家森林公园", "九寨沟", "鼓浪屿", "丽江古城", "长城", "颐和园", "泰山风景区", "峨眉山", "布达拉宫", "敦煌莫高窟", "桂林漓江", "三亚天涯海角"])',
category: '@pick(["自然风光", "人文历史", "城市地标", "主题乐园", "自然风光", "人文历史"])',
level: '@pick(["5A", "4A", "5A"])',
province: '@pick(["安徽", "北京", "浙江", "湖南", "四川", "福建", "云南", "山东", "西藏", "甘肃", "广西", "海南"])',
categoryId: '@integer(1, 5)',
categoryName: '@pick(["自然风光", "人文历史", "城市地标", "主题乐园", "博物馆"])',
city: '@pick(["黄山市", "北京市", "杭州市", "张家界市", "阿坝州", "厦门市", "丽江市", "泰安市", "桂林市"])',
ticketPrice: '@integer(30, 300)',
recommended: '@pick([0, 1])',
status: '@pick([0, 1])',
salesCount: '@integer(1000, 50000)',
address: '@county(true)',
coverImage: '@image("300x200")',
description: '@csentence(20, 50)',
openTime: '@pick(["08:00-17:00", "08:30-17:30", "09:00-18:00"])',
playTime: '@pick(["2-3小时", "半天", "1天", "2天"])',
tags: '@pick(["5A景区", "世界遗产", "国家地质公园", "历史文化", "自然风光"])',
isHot: '@pick([0, 1])',
isRecommend: '@pick([0, 1])',
status: '@pick([0, 1])',
viewCount: '@integer(1000, 50000)',
favoriteCount: '@integer(100, 5000)',
score: '@float(3, 5, 1, 1)',
createTime: '@datetime("yyyy-MM-dd HH:mm:ss")',
},
],
@ -152,9 +159,38 @@ Mock.mock(/\/api\/attraction\/list/, 'get', () => {
return pageSuccess(list.records, list.records.length)
})
Mock.mock(/\/api\/attraction\/create/, 'post', () => success(null))
Mock.mock(/\/api\/attraction\/update/, 'post', () => success(null))
Mock.mock(/\/api\/attraction\/delete/, 'post', () => success(null))
// 景点管理 - 根据ID查询
Mock.mock(/\/api\/attraction\/\d+/, 'get', () => {
return success({
id: '@id',
name: '@pick(["黄山风景区", "故宫博物院", "西湖景区"])',
categoryId: 1,
categoryName: '自然风光',
city: '黄山市',
address: '@county(true)',
latitude: '@float(25, 40, 2, 6)',
longitude: '@float(100, 120, 2, 6)',
coverImage: '@image("300x200")',
images: '[]',
videoUrl: '',
description: '@cparagraph(2, 5)',
openTime: '08:00-17:00',
playTime: '2-3小时',
trafficInfo: '@cparagraph(1, 3)',
tags: '5A景区,世界遗产',
isHot: 1,
isRecommend: 1,
status: 1,
viewCount: 12560,
favoriteCount: 3200,
score: 4.8,
createTime: '@datetime("yyyy-MM-dd HH:mm:ss")',
})
})
Mock.mock(/\/api\/attraction/, 'post', () => success(null))
Mock.mock(/\/api\/attraction/, 'put', () => success(null))
Mock.mock(/\/api\/attraction\/\d+/, 'delete', () => success(null))
// 景点分类
Mock.mock(/\/api\/attraction\/category\/list/, 'get', () => {

View File

@ -48,6 +48,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/attraction/AttractionView.vue'),
meta: { title: '景点管理', icon: 'Location' },
},
{
path: 'attraction/form',
name: 'AttractionForm',
component: () => import('@/views/attraction/AttractionFormView.vue'),
meta: { title: '景点表单', icon: 'Location', hidden: true },
},
{
path: 'attraction/category',
name: 'AttractionCategory',

View File

@ -0,0 +1,377 @@
<template>
<a-card :title="isEdit ? '编辑景点' : '新增景点'">
<a-form :model="form" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<a-row :gutter="24">
<a-col :span="16">
<a-divider orientation="left">基本信息</a-divider>
<a-form-item label="景点名称" required>
<a-input v-model:value="form.name" placeholder="请输入景点名称" />
</a-form-item>
<a-form-item label="所属分类" required>
<a-tree-select
v-model:value="form.categoryId"
:tree-data="categoryTree"
:field-names="{ children: 'children', label: 'name', value: 'id' }"
placeholder="请选择分类"
allow-clear
tree-default-expand-all
style="width: 100%"
/>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="所在城市" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" required>
<a-input v-model:value="form.city" placeholder="如:杭州市" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="详细地址" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" required>
<a-input v-model:value="form.address" placeholder="请输入详细地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="纬度" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input-number v-model:value="form.latitude" :precision="6" style="width: 100%" placeholder="纬度" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="经度" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input-number v-model:value="form.longitude" :precision="6" style="width: 100%" placeholder="经度" />
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="left">媒体资源</a-divider>
<a-form-item label="封面图片">
<a-input v-model:value="form.coverImage" placeholder="请输入封面图片URL" @change="updatePreview" />
</a-form-item>
<a-form-item label="图片列表">
<a-textarea
v-model:value="form.images"
:rows="3"
placeholder='JSON数组格式["url1", "url2", "url3"]'
@change="updatePreview"
/>
</a-form-item>
<a-form-item label="宣传视频">
<a-input v-model:value="form.videoUrl" placeholder="请输入视频URL" />
</a-form-item>
<a-divider orientation="left">详情信息</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="开放时间" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input v-model:value="form.openTime" placeholder="如08:00-17:00" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="游玩时长" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input v-model:value="form.playTime" placeholder="如2-3小时" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="景点介绍">
<a-textarea v-model:value="form.description" :rows="5" placeholder="请输入景点介绍" />
</a-form-item>
<a-form-item label="交通信息">
<a-textarea v-model:value="form.trafficInfo" :rows="3" placeholder="请输入交通信息" />
</a-form-item>
<a-form-item label="标签">
<a-input v-model:value="form.tags" placeholder="多个标签用逗号分隔5A景区,世界遗产,自然风光" />
</a-form-item>
<a-divider orientation="left">其他设置</a-divider>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="热门">
<a-switch v-model:checked="form.isHot" :checked-value="1" :unchecked-value="0" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="推荐">
<a-switch v-model:checked="form.isRecommend" :checked-value="1" :unchecked-value="0" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="上架状态">
<a-switch
v-model:checked="form.status"
:checked-value="1"
:unchecked-value="0"
checked-children="上架"
un-checked-children="下架"
/>
</a-form-item>
</a-col>
</a-row>
</a-col>
<a-col :span="8">
<a-card title="预览" size="small">
<div class="preview-cover">
<img v-if="preview.coverImage" :src="preview.coverImage" alt="封面预览" />
<div v-else class="preview-placeholder">封面预览</div>
</div>
<div class="preview-info">
<h3>{{ form.name || '景点名称' }}</h3>
<p class="preview-city">{{ form.city || '所在城市' }} · {{ form.address || '详细地址' }}</p>
<p class="preview-desc">{{ form.description || '景点介绍...' }}</p>
<div class="preview-tags">
<a-tag v-for="tag in previewTags" :key="tag" size="small">{{ tag }}</a-tag>
</div>
</div>
<a-divider />
<div class="preview-images">
<p class="preview-section-title">图片列表</p>
<div v-if="previewImages.length > 0" class="preview-image-list">
<img v-for="(img, idx) in previewImages" :key="idx" :src="img" alt="景点图片" />
</div>
<div v-else class="preview-placeholder-small">暂无图片</div>
</div>
</a-card>
</a-col>
</a-row>
<a-form-item :wrapper-col="{ offset: 3, span: 21 }" style="margin-top: 24px;">
<a-space>
<a-button type="primary" :loading="saveLoading" @click="handleSave">保存</a-button>
<a-button @click="handleCancel">取消</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
createAttraction,
updateAttraction,
getAttractionById,
getAttractionCategoryTree,
} from '@/api/attraction'
import { message } from 'ant-design-vue'
const route = useRoute()
const router = useRouter()
const isEdit = ref(false)
const saveLoading = ref(false)
const categoryTree = ref<any[]>([])
const form = reactive({
id: undefined as number | undefined,
name: '',
categoryId: undefined as number | undefined,
city: '',
address: '',
latitude: undefined as number | undefined,
longitude: undefined as number | undefined,
coverImage: '',
images: '',
videoUrl: '',
description: '',
openTime: '',
playTime: '',
trafficInfo: '',
tags: '',
isHot: 0,
isRecommend: 0,
status: 1,
})
const preview = reactive({
coverImage: '',
})
const previewTags = computed(() => {
if (!form.tags) return []
return form.tags.split(',').map((t) => t.trim()).filter(Boolean)
})
const previewImages = computed(() => {
try {
const arr = JSON.parse(form.images || '[]')
return Array.isArray(arr) ? arr : []
} catch {
return []
}
})
function updatePreview() {
preview.coverImage = form.coverImage
}
async function fetchCategoryTree() {
try {
const res: any = await getAttractionCategoryTree()
categoryTree.value = res.data || []
} catch (e) {
console.error('获取分类失败', e)
}
}
async function fetchDetail(id: number) {
try {
const res: any = await getAttractionById(id)
const data = res.data
Object.assign(form, {
id: data.id,
name: data.name,
categoryId: data.categoryId,
city: data.city,
address: data.address,
latitude: data.latitude,
longitude: data.longitude,
coverImage: data.coverImage || '',
images: data.images || '',
videoUrl: data.videoUrl || '',
description: data.description || '',
openTime: data.openTime || '',
playTime: data.playTime || '',
trafficInfo: data.trafficInfo || '',
tags: data.tags || '',
isHot: data.isHot,
isRecommend: data.isRecommend,
status: data.status,
})
updatePreview()
} catch (e) {
message.error('获取景点详情失败')
console.error(e)
}
}
async function handleSave() {
if (!form.name.trim()) {
message.error('请输入景点名称')
return
}
if (!form.categoryId) {
message.error('请选择分类')
return
}
if (!form.city.trim()) {
message.error('请输入所在城市')
return
}
if (!form.address.trim()) {
message.error('请输入详细地址')
return
}
saveLoading.value = true
try {
const data = { ...form }
if (isEdit.value) {
await updateAttraction(data)
message.success('更新成功')
} else {
await createAttraction(data)
message.success('创建成功')
}
router.push('/attraction/list')
} finally {
saveLoading.value = false
}
}
function handleCancel() {
router.push('/attraction/list')
}
onMounted(() => {
fetchCategoryTree()
const id = route.query.id
if (id) {
isEdit.value = true
fetchDetail(Number(id))
}
})
</script>
<style scoped lang="scss">
.preview-cover {
width: 100%;
height: 180px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.preview-placeholder {
color: #999;
font-size: 14px;
}
.preview-info {
h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.preview-city {
margin: 0 0 8px 0;
font-size: 13px;
color: #666;
}
.preview-desc {
margin: 0 0 12px 0;
font-size: 13px;
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.preview-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.preview-section-title {
font-size: 13px;
font-weight: 500;
color: #666;
margin: 0 0 8px 0;
}
.preview-image-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
img {
width: 80px;
height: 60px;
object-fit: cover;
border-radius: 4px;
}
}
.preview-placeholder-small {
color: #bbb;
font-size: 12px;
padding: 8px 0;
}
</style>

View File

@ -1,88 +1,140 @@
<template>
<a-card title="景点列表">
<template #extra>
<a-input-search v-model:value="search" placeholder="搜索景点" style="width: 200px; margin-right: 8px" @search="fetchData" />
<a-button type="primary" @click="openDialog(false)">新增景点</a-button>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索景点名称"
style="width: 220px; margin-right: 8px"
@search="handleSearch"
/>
<a-button type="primary" @click="handleAdd">新增景点</a-button>
</template>
<a-table :data-source="tableData" :columns="columns" :loading="loading" :pagination="false" row-key="id" />
<a-table
:data-source="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
/>
</a-card>
<a-modal v-model:visible="showDialog" :title="isEdit ? '编辑景点' : '新增景点'" width="600px" @ok="handleSave" @cancel="showDialog = false">
<a-form :model="form" label-col="{ span: 4 }" wrapper-col="{ span: 20 }">
<a-form-item label="景点名称"><a-input v-model:value="form.name" /></a-form-item>
<a-form-item label="分类">
<a-select v-model:value="form.category" style="width: 100%">
<a-select-option value="自然风光">自然风光</a-select-option>
<a-select-option value="人文历史">人文历史</a-select-option>
<a-select-option value="城市地标">城市地标</a-select-option>
<a-select-option value="主题乐园">主题乐园</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="等级">
<a-select v-model:value="form.level" style="width: 100%">
<a-select-option value="5A">5A</a-select-option>
<a-select-option value="4A">4A</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="门票价格"><a-input-number v-model:value="form.ticketPrice" :min="0" /></a-form-item>
<a-form-item label="推荐"><a-switch v-model:checked="form.recommended" :checked-value="1" :unchecked-value="0" /></a-form-item>
<a-form-item label="描述"><a-textarea v-model:value="form.description" /></a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, h, onMounted, reactive } from 'vue'
import { getAttractionList, createAttraction, updateAttraction, deleteAttraction } from '@/api/attraction'
import { useRouter } from 'vue-router'
import {
getAttractionPage,
deleteAttraction,
} from '@/api/attraction'
import { message, Modal } from 'ant-design-vue'
const router = useRouter()
const tableData = ref<any[]>([])
const loading = ref(false)
const search = ref('')
const showDialog = ref(false)
const isEdit = ref(false)
const form = reactive({ id: 0, name: '', category: '自然风光', level: '4A', ticketPrice: 80, recommended: 0, description: '' })
const searchKeyword = ref('')
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total: number) => `${total}`,
})
const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '景点名称', dataIndex: 'name', minWidth: 160 },
{ title: '分类', dataIndex: 'category', width: 100 },
{ title: '等级', dataIndex: 'level', width: 80 },
{ title: '省份', dataIndex: 'province', width: 80 },
{ title: '门票价格', dataIndex: 'ticketPrice', width: 100, customRender: ({ text }: any) => '¥' + text },
{ title: '销量', dataIndex: 'salesCount', width: 100 },
{ title: '推荐', dataIndex: 'recommended', width: 80, customRender: ({ text }: any) => text === 1 ? h('a-tag', { color: 'gold' }, '是') : h('a-tag', { color: 'default' }, '否') },
{ title: '状态', dataIndex: 'status', width: 80, customRender: ({ text }: any) => text === 1 ? h('a-tag', { color: 'green' }, '上架') : h('a-tag', { color: 'red' }, '下架') },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{ title: '操作', width: 180, fixed: 'right', customRender: ({ record }: any) => h('span', null, [h('a', { onClick: () => handleEdit(record) }, '编辑'), h('a-divider', { type: 'vertical' }), h('a', { onClick: () => handleDelete(record) }, '删除')]) },
{ title: 'ID', dataIndex: 'id', width: 70 },
{ title: '景点名称', dataIndex: 'name', width: 160, ellipsis: true },
{ title: '分类', dataIndex: 'categoryName', width: 100 },
{ title: '城市', dataIndex: 'city', width: 100 },
{
title: '封面',
dataIndex: 'coverImage',
width: 80,
customRender: ({ text }: any) =>
text ? h('img', { src: text, style: 'width: 50px; height: 36px; object-fit: cover; border-radius: 4px;' }) : h('span', { style: 'color: #999;' }, '无'),
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text }: any) =>
text === 1 ? h('a-tag', { color: 'green' }, '上架') : h('a-tag', { color: 'red' }, '下架'),
},
{
title: '热门',
dataIndex: 'isHot',
width: 70,
customRender: ({ text }: any) =>
text === 1 ? h('a-tag', { color: 'red' }, '是') : h('a-tag', { color: 'default' }, '否'),
},
{
title: '推荐',
dataIndex: 'isRecommend',
width: 70,
customRender: ({ text }: any) =>
text === 1 ? h('a-tag', { color: 'gold' }, '是') : h('a-tag', { color: 'default' }, '否'),
},
{ title: '浏览量', dataIndex: 'viewCount', width: 90 },
{ title: '收藏量', dataIndex: 'favoriteCount', width: 90 },
{ title: '评分', dataIndex: 'score', width: 70, customRender: ({ text }: any) => text },
{ title: '创建时间', dataIndex: 'createTime', width: 170 },
{
title: '操作',
width: 160,
fixed: 'right',
customRender: ({ record }: any) =>
h('span', { style: { display: 'flex', gap: '12px' } }, [
h('a', { onClick: () => handleEdit(record) }, '编辑'),
h('a', { style: { color: '#ff4d4f' }, onClick: () => handleDelete(record) }, '删除'),
]),
},
]
async function fetchData() {
loading.value = true
try {
const res: any = await getAttractionList()
let list = res.data.records
if (search.value) list = list.filter((r: any) => r.name.includes(search.value))
tableData.value = list
} finally { loading.value = false }
const res: any = await getAttractionPage({
current: pagination.current,
size: pagination.pageSize,
keyword: searchKeyword.value || undefined,
})
tableData.value = res.data.records || []
pagination.total = res.data.total || 0
} finally {
loading.value = false
}
}
function openDialog(edit: boolean) {
isEdit.value = edit
if (!edit) Object.assign(form, { id: 0, name: '', category: '自然风光', level: '4A', ticketPrice: 80, recommended: 0, description: '' })
showDialog.value = true
function handleSearch() {
pagination.current = 1
fetchData()
}
function handleEdit(row: any) { Object.assign(form, row); openDialog(true) }
function handleTableChange(pag: any) {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
fetchData()
}
function handleAdd() {
router.push('/attraction/form')
}
function handleEdit(row: any) {
router.push(`/attraction/form?id=${row.id}`)
}
function handleDelete(row: any) {
Modal.confirm({ title: '确认删除该景点?', onOk: async () => { await deleteAttraction(row.id); message.success('删除成功'); fetchData() } })
}
async function handleSave() {
if (isEdit.value) await updateAttraction(form); else await createAttraction(form)
message.success(isEdit.value ? '更新成功' : '创建成功')
showDialog.value = false; fetchData()
Modal.confirm({
title: '确认删除',
content: `确定删除景点「${row.name}」吗?`,
onOk: async () => {
await deleteAttraction(row.id)
message.success('删除成功')
fetchData()
},
})
}
onMounted(fetchData)

View File

@ -1,55 +1,188 @@
<template>
<a-card title="景点分类">
<template #extra><a-button type="primary" @click="openDialog(false)">新增分类</a-button></template>
<a-table :data-source="tableData" :columns="columns" :loading="loading" :pagination="false" row-key="id" />
<template #extra>
<a-button type="primary" @click="openDialog(false)">新增分类</a-button>
</template>
<a-table
:data-source="tableData"
:columns="columns"
:loading="loading"
:pagination="false"
row-key="id"
:children-column-name="'children'"
default-expand-all-rows
/>
</a-card>
<a-modal v-model:visible="showDialog" :title="isEdit ? '编辑分类' : '新增分类'" @ok="handleSave" @cancel="showDialog = false">
<a-form :model="form" label-col="{ span: 4 }" wrapper-col="{ span: 20 }">
<a-form-item label="分类名称"><a-input v-model:value="form.name" /></a-form-item>
<a-form-item label="排序"><a-input-number v-model:value="form.sort" :min="1" /></a-form-item>
<a-form-item label="状态"><a-switch v-model:checked="form.status" :checked-value="1" :unchecked-value="0" /></a-form-item>
<a-modal
v-model:visible="showDialog"
:title="isEdit ? '编辑分类' : '新增分类'"
width="520px"
@ok="handleSave"
@cancel="showDialog = false"
>
<a-form :model="form" :label-col="{ span: 5 }" :wrapper-col="{ span: 19 }">
<a-form-item label="分类名称" required>
<a-input v-model:value="form.name" placeholder="请输入分类名称" />
</a-form-item>
<a-form-item label="父级分类">
<a-tree-select
v-model:value="form.parentId"
:tree-data="categoryOptions"
:field-names="{ children: 'children', label: 'name', value: 'id' }"
placeholder="请选择父级分类(不选则为顶级分类)"
allow-clear
tree-default-expand-all
style="width: 100%"
/>
</a-form-item>
<a-form-item label="排序">
<a-input-number v-model:value="form.sort" :min="0" style="width: 100%" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, h, onMounted, reactive } from 'vue'
import { getAttractionCategoryList, createAttractionCategory, updateAttractionCategory, deleteAttractionCategory } from '@/api/attraction'
import { ref, h, onMounted, reactive, computed } from 'vue'
import {
getAttractionCategoryTree,
createAttractionCategory,
updateAttractionCategory,
deleteAttractionCategory,
} from '@/api/attraction'
import { message, Modal } from 'ant-design-vue'
const tableData = ref<any[]>([])
const loading = ref(false)
const showDialog = ref(false)
const isEdit = ref(false)
const form = reactive({ id: 0, name: '', sort: 1, status: 1 })
const currentEditId = ref<number | undefined>(undefined)
const form = reactive({
id: undefined as number | undefined,
name: '',
parentId: undefined as number | undefined,
sort: 0,
})
const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '分类名称', dataIndex: 'name' },
{ title: '分类名称', dataIndex: 'name', width: 200 },
{ title: '层级', dataIndex: 'level', width: 80, customRender: ({ text }: any) => `${text}` },
{ title: '排序', dataIndex: 'sort', width: 80 },
{ title: '状态', dataIndex: 'status', width: 100, customRender: ({ text }: any) => text === 1 ? h('a-tag', { color: 'green' }, '启用') : h('a-tag', { color: 'red' }, '禁用') },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{ title: '操作', width: 180, customRender: ({ record }: any) => h('span', null, [h('a', { onClick: () => handleEdit(record) }, '编辑'), h('a-divider', { type: 'vertical' }), h('a', { onClick: () => handleDelete(record) }, '删除')]) },
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
customRender: ({ text }: any) => text || '-',
},
{
title: '操作',
width: 240,
customRender: ({ record }: any) =>
h('span', { style: { display: 'flex', gap: '12px' } }, [
h('a', { onClick: () => handleAddChild(record) }, '添加子类'),
h('a', { onClick: () => handleEdit(record) }, '编辑'),
h('a', { style: { color: '#ff4d4f' }, onClick: () => handleDelete(record) }, '删除'),
]),
},
]
//
const categoryOptions = computed(() => {
if (!currentEditId.value) return tableData.value
return filterTree(tableData.value, currentEditId.value)
})
function filterTree(tree: any[], excludeId: number): any[] {
return tree
.filter((node) => node.id !== excludeId)
.map((node) => {
const copy = { ...node }
if (copy.children && copy.children.length > 0) {
copy.children = filterTree(copy.children, excludeId)
}
return copy
})
}
async function fetchData() {
loading.value = true
try { const res: any = await getAttractionCategoryList(); tableData.value = res.data.records } finally { loading.value = false }
try {
const res: any = await getAttractionCategoryTree()
tableData.value = res.data || []
} finally {
loading.value = false
}
}
function openDialog(edit: boolean) {
function openDialog(edit: boolean, parentId?: number) {
isEdit.value = edit
if (!edit) Object.assign(form, { id: 0, name: '', sort: 1, status: 1 })
if (!edit) {
currentEditId.value = undefined
Object.assign(form, {
id: undefined,
name: '',
parentId: parentId ?? undefined,
sort: 0,
})
}
showDialog.value = true
}
function handleEdit(row: any) { Object.assign(form, row); openDialog(true) }
function handleDelete(row: any) {
Modal.confirm({ title: '确认删除该分类?', onOk: async () => { await deleteAttractionCategory(row.id); message.success('删除成功'); fetchData() } })
function handleAddChild(row: any) {
currentEditId.value = undefined
openDialog(false, row.id)
}
function handleEdit(row: any) {
isEdit.value = true
currentEditId.value = row.id
Object.assign(form, {
id: row.id,
name: row.name,
parentId: row.parentId === 0 ? undefined : row.parentId,
sort: row.sort,
})
showDialog.value = true
}
function handleDelete(row: any) {
if (row.children && row.children.length > 0) {
message.error('请先删除子分类')
return
}
Modal.confirm({
title: '确认删除',
content: `确定删除分类「${row.name}」吗?`,
onOk: async () => {
await deleteAttractionCategory(row.id)
message.success('删除成功')
fetchData()
},
})
}
async function handleSave() {
if (isEdit.value) await updateAttractionCategory(form); else await createAttractionCategory(form)
message.success(isEdit.value ? '更新成功' : '创建成功'); showDialog.value = false; fetchData()
if (!form.name.trim()) {
message.error('请输入分类名称')
return
}
const data = {
...form,
parentId: form.parentId ?? 0,
}
if (isEdit.value) {
await updateAttractionCategory(data)
message.success('更新成功')
} else {
await createAttractionCategory(data)
message.success('创建成功')
}
showDialog.value = false
fetchData()
}
onMounted(fetchData)

View File

@ -51,6 +51,10 @@ public class AttractionCategoryServiceImpl extends ServiceImpl<AttractionCategor
BeanUtils.copyProperties(dto, category, "id");
if (category.getParentId() == null) {
category.setParentId(0L);
}
// 重新计算层级
if (category.getParentId() == 0L) {
category.setLevel(1);

View File

@ -1,9 +1,9 @@
spring:
datasource:
username: root
password: orbit123
password: 123456
logging:
level:
com.tourism: debug
com.baomidou.mybatisplus: debug
#logging:
# level:
# com.tourism: debug
# com.baomidou.mybatisplus: debug

View File

@ -27,6 +27,7 @@ mybatis-plus:
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
sa-token:
token-name: token