diff --git a/backend/.vite/deps/_metadata.json b/backend/.vite/deps/_metadata.json new file mode 100644 index 0000000..e26bd53 --- /dev/null +++ b/backend/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "e75be2da", + "configHash": "ef9a524a", + "lockfileHash": "19c2bb83", + "browserHash": "dc47218b", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/backend/.vite/deps/package.json b/backend/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/backend/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/backend/app/api/customers.py b/backend/app/api/customers.py index 768f2f4..4dba2d2 100644 --- a/backend/app/api/customers.py +++ b/backend/app/api/customers.py @@ -93,9 +93,21 @@ async def list_customers( offset = (page - 1) * page_size query = base_query.order_by(Customer.name).offset(offset).limit(page_size) result = await db.execute(query) + items = result.scalars().all() + + # Enrich with manager names + if items: + mgr_result = await db.execute( + select(CustomerAssignment.customer_id, User.name) + .join(User, CustomerAssignment.manager_id == User.id) + .where(CustomerAssignment.customer_id.in_([c.id for c in items]), CustomerAssignment.role == "primary") + ) + mgr_map = {str(cid): name for cid, name in mgr_result.all()} + for item in items: + item.primary_manager_name = mgr_map.get(str(item.id), None) return CustomerListResponse( - items=result.scalars().all(), + items=items, total=total, page=page, page_size=page_size, @@ -350,6 +362,8 @@ async def create_customer( current_user: dict = Depends(require_any_role), db: AsyncSession = Depends(get_db), ): + if current_user["role"] == "leader": + raise HTTPException(status_code=403, detail="分管领导无法创建客户") """Create a new customer with optional contacts and manager assignment.""" import uuid customer = Customer( @@ -390,6 +404,8 @@ async def update_customer( current_user: dict = Depends(require_any_role), db: AsyncSession = Depends(get_db), ): + if current_user["role"] == "leader": + raise HTTPException(status_code=403, detail="分管领导无法编辑客户") result = await db.execute( select(Customer).where(Customer.id == customer_id).options(selectinload(Customer.contacts)) ) diff --git a/backend/app/api/import_data.py b/backend/app/api/import_data.py index e0ff7ad..4843670 100644 --- a/backend/app/api/import_data.py +++ b/backend/app/api/import_data.py @@ -1,5 +1,7 @@ +import io import uuid from fastapi import APIRouter, Depends, File, UploadFile, HTTPException +from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.middleware.auth import get_current_user, require_director @@ -8,6 +10,54 @@ from app.services.excel_import import import_from_excel router = APIRouter(prefix="/import", tags=["Import"]) +@router.get("/template") +async def download_weekly_report_template(): + """Download a 4-sheet weekly report import template.""" + from openpyxl import Workbook + from openpyxl.styles import Font + + wb = Workbook() + header_font = Font(bold=True) + + # Sheet 1: 每日拜访记录 + ws1 = wb.active + ws1.title = "每日拜访记录" + ws1.append(["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "同访人员", "客户经理"]) + for c in ws1[1]: c.font = header_font + ws1.append(["XX科技有限公司", "2026-06-23", "上门", "9:00-10:00", "韦柳柏", "13800000000", "沟通了解云桌面需求", "希望扩容", "", "韦矍森"]) + ws1.column_dimensions['A'].width = 20; ws1.column_dimensions['E'].width = 30; ws1.column_dimensions['F'].width = 20 + + # Sheet 2: 下周工作计划 + ws2 = wb.create_sheet("下周工作计划") + ws2.append(["客户单位", "工作计划", "计划拜访时间", "客户经理", "状态"]) + for c in ws2[1]: c.font = header_font + ws2.append(["XX科技有限公司", "跟进云桌面扩容方案", "2026-06-30", "韦柳柏", "计划中"]) + ws2.column_dimensions['A'].width = 20; ws2.column_dimensions['B'].width = 35 + + # Sheet 3: 小微业务商机 + ws3 = wb.create_sheet("小微业务商机") + ws3.append(["客户单位", "产品类型", "金额", "跟进内容具体情况", "跟进状态", "客户经理", "预计列收时间"]) + for c in ws3[1]: c.font = header_font + ws3.append(["XX科技有限公司", "云桌面", "5000元/月", "确认技术方案中", "跟进中", "韦柳柏", "2026Q3"]) + ws3.column_dimensions['A'].width = 20; ws3.column_dimensions['D'].width = 30 + + # Sheet 4: 要客拜访计划 + ws4 = wb.create_sheet("要客拜访计划") + ws4.append(["客户单位", "紧急重要度", "内容描述", "进展状态", "计划拜访时间", "计划拜访人", "拜访对象", "客户经理"]) + for c in ws4[1]: c.font = header_font + ws4.append(["XX科技有限公司", "重要", "拜访技术负责人确认方案", "未开始", "2026-07-01", "韦柳柏", "王局长", "韦矍森"]) + ws4.column_dimensions['A'].width = 20; ws4.column_dimensions['C'].width = 30 + + output = io.BytesIO() + wb.save(output) + output.seek(0) + return StreamingResponse( + output, + media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + headers={"Content-Disposition": "attachment; filename=weekly_report_template.xlsx"}, + ) + + @router.post("/weekly-report") async def import_weekly_report( file: UploadFile = File(...), @@ -20,7 +70,6 @@ async def import_weekly_report( content = await file.read() - # Preview first: parse headers try: import openpyxl wb = openpyxl.load_workbook(io.BytesIO(content), data_only=True) @@ -28,16 +77,12 @@ async def import_weekly_report( for sheet_name in wb.sheetnames: ws = wb[sheet_name] headers = [str(cell.value) for cell in ws[1]] - row_count = ws.max_row - 1 # minus header + row_count = ws.max_row - 1 preview[sheet_name] = {"headers": headers, "row_count": row_count} except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to parse Excel: {str(e)}") - # Import data stats = await import_from_excel(db, content, uuid.UUID(current_user["user_id"])) stats["preview"] = preview return stats - - -import io diff --git a/backend/app/api/visits.py b/backend/app/api/visits.py index 5f09e10..6bc5bb6 100644 --- a/backend/app/api/visits.py +++ b/backend/app/api/visits.py @@ -34,6 +34,8 @@ async def _enrich_visit(visit: Visit, db: AsyncSession) -> dict: "visit_date": visit.visit_date, "visit_method": visit.visit_method, "time_range": visit.time_range, + "visitor_name": visit.visitor_name or "", + "visitor_phone": visit.visitor_phone or "", "communication_content": visit.communication_content, "customer_demand": visit.customer_demand, "companions": visit.companions, diff --git a/backend/app/main.py b/backend/app/main.py index 72527bd..337be54 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -17,6 +17,12 @@ async def lifespan(app: FastAPI): await conn.run_sync(lambda c: c.exec_driver_sql( "ALTER TABLE customers ADD COLUMN IF NOT EXISTS remarks TEXT DEFAULT ''" )) + await conn.run_sync(lambda c: c.exec_driver_sql( + "ALTER TABLE visits ADD COLUMN IF NOT EXISTS visitor_name VARCHAR(50) DEFAULT ''" + )) + await conn.run_sync(lambda c: c.exec_driver_sql( + "ALTER TABLE visits ADD COLUMN IF NOT EXISTS visitor_phone VARCHAR(20) DEFAULT ''" + )) yield # Shutdown await engine.dispose() diff --git a/backend/app/models/visit.py b/backend/app/models/visit.py index d0af6e8..48a1798 100644 --- a/backend/app/models/visit.py +++ b/backend/app/models/visit.py @@ -16,6 +16,8 @@ class Visit(Base): time_range: Mapped[str] = mapped_column(String(30), default="") communication_content: Mapped[str] = mapped_column(Text, default="") customer_demand: Mapped[str] = mapped_column(Text, default="") + visitor_name: Mapped[str] = mapped_column(String(50), default="") + visitor_phone: Mapped[str] = mapped_column(String(20), default="") companions: Mapped[list | None] = mapped_column(ARRAY(UUID(as_uuid=True)), nullable=True) photos: Mapped[list | None] = mapped_column(ARRAY(Text), nullable=True) manager_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), index=True) diff --git a/backend/app/schemas/customer.py b/backend/app/schemas/customer.py index 2f096ef..cfc8166 100644 --- a/backend/app/schemas/customer.py +++ b/backend/app/schemas/customer.py @@ -84,6 +84,7 @@ class CustomerListOut(BaseModel): name: str industry: str in_use_services: str + primary_manager_name: Optional[str] = None model_config = {"from_attributes": True} diff --git a/backend/app/schemas/visit.py b/backend/app/schemas/visit.py index dd18d89..8d4d14f 100644 --- a/backend/app/schemas/visit.py +++ b/backend/app/schemas/visit.py @@ -9,6 +9,8 @@ class VisitCreate(BaseModel): visit_date: str # "YYYY-MM-DD" visit_method: str = "上门" time_range: str = "" + visitor_name: str = "" + visitor_phone: str = "" communication_content: str = "" customer_demand: str = "" companions: list[uuid.UUID] = [] @@ -20,6 +22,8 @@ class VisitUpdate(BaseModel): visit_date: Optional[str] = None visit_method: Optional[str] = None time_range: Optional[str] = None + visitor_name: Optional[str] = None + visitor_phone: Optional[str] = None communication_content: Optional[str] = None customer_demand: Optional[str] = None companions: Optional[list[uuid.UUID]] = None @@ -32,6 +36,8 @@ class VisitOut(BaseModel): visit_date: date visit_method: str time_range: str + visitor_name: str = "" + visitor_phone: str = "" communication_content: str customer_demand: str companions: Optional[list[uuid.UUID]] = None diff --git a/backend/app/services/excel_export.py b/backend/app/services/excel_export.py index 37ef246..ea6d965 100644 --- a/backend/app/services/excel_export.py +++ b/backend/app/services/excel_export.py @@ -40,7 +40,7 @@ async def export_weekly_report(db: AsyncSession, reference_date: date | None = N # ── Sheet 1: 每日拜访记录 ── ws1 = wb.active ws1.title = "每日拜访记录" - headers1 = ["客户单位", "拜访日期", "拜访方式", "时间范围", "沟通内容", "客户需求", "同访人员", "客户经理"] + headers1 = ["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "同访人员", "客户经理"] ws1.append(headers1) for col in range(1, len(headers1) + 1): cell = ws1.cell(row=1, column=col) @@ -57,6 +57,8 @@ async def export_weekly_report(db: AsyncSession, reference_date: date | None = N str(v.visit_date), v.visit_method, v.time_range, + v.visitor_name or "", + v.visitor_phone or "", v.communication_content, v.customer_demand, ", ".join(companions_names), diff --git a/backend/app/services/excel_import.py b/backend/app/services/excel_import.py index 875a71d..d44916a 100644 --- a/backend/app/services/excel_import.py +++ b/backend/app/services/excel_import.py @@ -28,24 +28,23 @@ async def import_from_excel(db: AsyncSession, file_bytes: bytes, manager_id: uui if not row[0]: continue try: - cust_name, visit_date_str, visit_method, time_range, content, demand, companions_str, mgr_str = \ + cust_name, visit_date_str, visit_method, time_range, visitor_name, visitor_phone, content, demand, companions_str, mgr_str = \ row[0], str(row[1]) if row[1] else str(date.today()), \ str(row[2]) if row[2] else "上门", str(row[3]) if row[3] else "", \ str(row[4]) if row[4] else "", str(row[5]) if row[5] else "", \ - str(row[6]) if row[6] else "", str(row[7]) if row[7] else "" + str(row[6]) if row[6] else "", str(row[7]) if row[7] else "", \ + str(row[8]) if row[8] else "", str(row[9]) if row[9] else "" customer_id = customer_map.get(cust_name) if not customer_id: stats["skipped"] += 1 continue - # Parse date try: visit_date = datetime.strptime(visit_date_str[:10], "%Y-%m-%d").date() except ValueError: visit_date = date.today() - # Check for duplicates (same day, same person, same customer) existing = await db.execute( select(Visit).where( Visit.visit_date == visit_date, @@ -62,6 +61,8 @@ async def import_from_excel(db: AsyncSession, file_bytes: bytes, manager_id: uui visit_date=visit_date, visit_method=visit_method if visit_method in ["上门", "电话", "微信", "出差"] else "上门", time_range=time_range, + visitor_name=visitor_name, + visitor_phone=visitor_phone, communication_content=content, customer_demand=demand, manager_id=manager_id, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b135bf2..f7086c9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,12 +15,29 @@ "vue-router": "^4.5.0" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.20", "@vitejs/plugin-vue": "^5.2.1", + "autoprefixer": "^10.5.0", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", "typescript": "~5.6.0", "vite": "^6.0.5", "vue-tsc": "^2.2.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -552,12 +569,82 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@popperjs/core": { "name": "@sxzz/popperjs-es", "version": "2.11.8", @@ -919,6 +1006,19 @@ "win32" ] }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.20", + "resolved": "https://registry.npmmirror.com/@tailwindcss/typography/-/typography-0.5.20.tgz", + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", @@ -1189,6 +1289,47 @@ "dev": true, "license": "MIT" }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/async-validator": { "version": "4.2.5", "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", @@ -1201,6 +1342,43 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/axios": { "version": "1.18.0", "resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.0.tgz", @@ -1220,6 +1398,32 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", @@ -1230,6 +1434,53 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1243,6 +1494,75 @@ "node": ">= 0.4" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1255,6 +1575,29 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", @@ -1300,6 +1643,20 @@ "node": ">=0.4.0" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1314,6 +1671,13 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.376", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "dev": true, + "license": "ISC" + }, "node_modules/element-plus": { "version": "2.14.2", "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.2.tgz", @@ -1439,12 +1803,62 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", @@ -1463,6 +1877,19 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -1499,6 +1926,20 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1560,6 +2001,19 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", @@ -1634,6 +2088,98 @@ "node": ">= 6" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", @@ -1681,6 +2227,43 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", @@ -1731,6 +2314,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.15", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", @@ -1749,12 +2344,52 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-wheel-es": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", "license": "BSD-3-Clause" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", @@ -1762,6 +2397,13 @@ "dev": true, "license": "MIT" }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", @@ -1781,6 +2423,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pinia": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", @@ -1803,6 +2455,16 @@ } } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", @@ -1831,6 +2493,154 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -1840,6 +2650,96 @@ "node": ">=10" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", @@ -1885,6 +2785,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1894,6 +2818,117 @@ "node": ">=0.10.0" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -1911,6 +2946,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.6.3.tgz", @@ -1925,6 +2980,44 @@ "node": ">=14.17" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7b2f91f..49bce70 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,11 @@ "vue-router": "^4.5.0" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.20", "@vitejs/plugin-vue": "^5.2.1", + "autoprefixer": "^10.5.0", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", "typescript": "~5.6.0", "vite": "^6.0.5", "vue-tsc": "^2.2.0" diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 37c7100..b5f4cac 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -4,7 +4,6 @@ import { computed } from 'vue' const auth = useAuthStore() const isMobile = computed(() => { - // Simple mobile detection — can be refined return /Android|iPhone|iPad|iPod|webOS/i.test(navigator.userAgent) || window.innerWidth < 768 }) @@ -14,11 +13,249 @@ const isMobile = computed(() => { diff --git a/frontend/src/components/DesktopLayout.vue b/frontend/src/components/DesktopLayout.vue index fe4e0c4..d42b706 100644 --- a/frontend/src/components/DesktopLayout.vue +++ b/frontend/src/components/DesktopLayout.vue @@ -1,55 +1,89 @@ diff --git a/frontend/src/components/MobileLayout.vue b/frontend/src/components/MobileLayout.vue index 42addf8..2a2c382 100644 --- a/frontend/src/components/MobileLayout.vue +++ b/frontend/src/components/MobileLayout.vue @@ -8,10 +8,23 @@ const router = useRouter() const auth = useAuthStore() const tabs = [ - { path: '/m', label: '首页', icon: 'HomeFilled' }, - { path: '/m/visit/new', label: '今日拜访', icon: 'Edit' }, - { path: '/m/note/new', label: '今日纪要', icon: 'Notebook' }, + { + path: '/m', + label: '首页', + icon: 'home', + }, + { + path: '/m/visit/new', + label: '拜访', + icon: 'visit', + }, + { + path: '/m/note/new', + label: '纪要', + icon: 'note', + }, ] + const activeTab = computed(() => { if (route.path === '/m') return '/m' if (route.path.includes('/visit')) return '/m/visit/new' @@ -22,71 +35,247 @@ const activeTab = computed(() => { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index a917c40..1236ef4 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,6 +3,7 @@ import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/dist/locale/zh-cn.mjs' +import './tailwind.css' import App from './App.vue' import router from './router' diff --git a/frontend/src/tailwind.css b/frontend/src/tailwind.css new file mode 100644 index 0000000..f615245 --- /dev/null +++ b/frontend/src/tailwind.css @@ -0,0 +1,34 @@ +/* ── Google Fonts import for Editorial Chinese fonts ── */ +@import url('https://fonts.googleapis.com/css2?family=ZCOOL+XiaoWei&family=Noto+Serif+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ── Base layer custom styles ── */ +@layer base { + :root { + --ink: #1C3738; + --ink-light: #2A4F50; + --vermilion: #B8472E; + --vermilion-light: #D46A4F; + --gold: #C4934A; + --gold-light: #D4AD6E; + --paper: #F5F0E8; + --paper-dark: #EBE4D8; + --sage: #4A6741; + --amber: #C68B3C; + --warm-gray: #7B7568; + --warm-border: #E5DFD3; + --text-primary: #1A1A1A; + } + + html, body, #app { + margin: 0; + padding: 0; + height: 100%; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + background: var(--paper); + } +} diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 2e3f92f..be98454 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -92,14 +92,29 @@ function goCasdoorLogin() { justify-content: center; align-items: center; height: 100vh; - background: linear-gradient(135deg, #1a73e8 0%, #4080ff 50%, #69a0ff 100%); + background: linear-gradient(135deg, #4f6ef7 0%, #7b93fa 40%, #a5b4fc 100%); + position: relative; + overflow: hidden; +} +.login-page::before { + content: ''; position: absolute; width: 600px; height: 600px; + background: rgba(255,255,255,0.05); border-radius: 50%; + top: -200px; right: -200px; +} +.login-page::after { + content: ''; position: absolute; width: 400px; height: 400px; + background: rgba(255,255,255,0.04); border-radius: 50%; + bottom: -100px; left: -100px; } .login-card { background: white; - border-radius: 16px; + border-radius: 20px; padding: 48px 40px; - width: 360px; - box-shadow: 0 20px 60px rgba(0,0,0,0.2); + width: 380px; + max-width: 90vw; + box-shadow: 0 25px 60px rgba(0,0,0,0.15); + position: relative; + z-index: 1; } .brand { text-align: center; @@ -107,18 +122,24 @@ function goCasdoorLogin() { } .brand h1 { margin: 0; - font-size: 32px; - color: #1a73e8; + font-size: 36px; + font-weight: 800; + letter-spacing: 4px; + background: linear-gradient(135deg, #4f6ef7, #7b93fa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .brand p { - margin: 8px 0 0; - color: #909399; + margin: 10px 0 0; + color: var(--c-text-secondary); font-size: 14px; + letter-spacing: 1px; } .hint { text-align: center; - color: #c0c4cc; + color: var(--c-text-muted); font-size: 12px; - margin-top: 16px; + margin-top: 20px; } diff --git a/frontend/src/views/desktop/CustomerManage.vue b/frontend/src/views/desktop/CustomerManage.vue index f30cd9b..92fc10d 100644 --- a/frontend/src/views/desktop/CustomerManage.vue +++ b/frontend/src/views/desktop/CustomerManage.vue @@ -30,16 +30,13 @@ const existingContacts = ref([]) const feeUnits = ['元/月', '元/年', '自定义'] const feeCustomUnit = ref('') -// Detail const detailVisible = ref(false) const detailCustomer = ref(null) -// Managers const managers = ref([]) const selectedIds = ref([]) const batchManagerId = ref('') -// Import const importDialogVisible = ref(false) const importFile = ref(null) const importLoading = ref(false) @@ -48,18 +45,15 @@ const importResult = ref(null) onMounted(async () => { await loadCustomers() try { - const res = await api.get('/users/', { params: { role: 'manager' } }) - managers.value = res.data + const res = await api.get('/users/') + managers.value = (res.data || []).filter((u: any) => u.role !== 'leader') } catch (_) {} }) async function loadCustomers() { loading.value = true try { - const params: any = { - page: currentPage.value, - page_size: pageSize.value, - } + const params: any = { page: currentPage.value, page_size: pageSize.value } if (search.value) params.search = search.value if (filterIndustry.value) params.industry = filterIndustry.value if (filterService.value) params.service = filterService.value @@ -67,17 +61,14 @@ async function loadCustomers() { const res = await customersApi.list(params) customers.value = res.data.items total.value = res.data.total - } catch (e: any) { - ElMessage.error('加载失败') - } finally { loading.value = false } + } catch (e: any) { ElMessage.error('加载失败') } + finally { loading.value = false } } function onPageChange(page: number) { currentPage.value = page; loadCustomers() } function onPageSizeChange(size: number) { pageSize.value = size; currentPage.value = 1; loadCustomers() } function onFilterChange() { currentPage.value = 1; loadCustomers() } -// ── Form helpers ── - function buildMonthlyFee(): string { const amt = form.value.fee_amount.trim() if (!amt) return '' @@ -92,35 +83,20 @@ function parseMonthlyFee(fee: string) { feeCustomUnit.value = '' if (!fee) { form.value.fee_amount = ''; form.value.fee_unit = '元/月'; return } for (const u of ['元/月', '元/年']) { - if (fee.endsWith(u)) { - form.value.fee_amount = fee.slice(0, -u.length).trim() - form.value.fee_unit = u - return - } + if (fee.endsWith(u)) { form.value.fee_amount = fee.slice(0, -u.length).trim(); form.value.fee_unit = u; return } } - // Try to separate trailing non-numeric chars as custom unit const m = fee.match(/^(.+?)\s*([^\d]+)$/) - if (m) { - form.value.fee_amount = m[1].trim() - feeCustomUnit.value = m[2].trim() - form.value.fee_unit = '自定义' - } else { - form.value.fee_amount = fee - form.value.fee_unit = '自定义' - } + if (m) { form.value.fee_amount = m[1].trim(); feeCustomUnit.value = m[2].trim(); form.value.fee_unit = '自定义' } + else { form.value.fee_amount = fee; form.value.fee_unit = '自定义' } } -// ── Create / Edit Dialog ── - function resetForm() { form.value = { name: '', industry: '', address: '', in_use_services: '', fee_amount: '', fee_unit: '元/月', remarks: '', contacts: [], assignee_id: '' } feeCustomUnit.value = '' existingContacts.value = [] } -function openCreate() { - dialogTitle.value = '新建客户'; editId.value = ''; resetForm(); dialogVisible.value = true -} +function openCreate() { dialogTitle.value = '新建客户'; editId.value = ''; resetForm(); dialogVisible.value = true } async function openEdit(customer: any) { dialogTitle.value = '编辑客户'; editId.value = customer.id @@ -145,9 +121,7 @@ async function handleSubmit() { if (form.value.assignee_id) body.assignee_id = form.value.assignee_id await customersApi.update(editId.value, body) for (const c of form.value.contacts) { - if (c.name.trim()) { - await api.post(`/customers/${editId.value}/contacts`, { name: c.name.trim(), phone: c.phone.trim(), role_desc: c.role_desc.trim() }) - } + if (c.name.trim()) await api.post(`/customers/${editId.value}/contacts`, { name: c.name.trim(), phone: c.phone.trim(), role_desc: c.role_desc.trim() }) } ElMessage.success('已更新') } else { @@ -168,17 +142,16 @@ async function removeExistingContact(contactId: string) { } catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') } } -// ── Detail ── - -async function openDetail(customer: any) { - try { - const res = await customersApi.get(customer.id) - detailCustomer.value = res.data - detailVisible.value = true - } catch (_) {} +const mgrColors = ['#1C3738','#4A6741','#5B7FA5','#7B7568','#8B6F47','#6B5B4F','#3D5A5C','#5C4A3D'] +function mgrColor(name: string) { + if (!name) return '#909399' + let h = 0; for (let i=0;i r.id) } async function handleBatchAssign() { @@ -204,8 +175,6 @@ async function handleBatchAssign() { } catch (e: any) { ElMessage.error('批量分配失败') } } -// ── Import / Export ── - async function handleExport() { try { const res = await api.get('/customers/export', { responseType: 'blob' }) @@ -242,14 +211,38 @@ async function handleImport() { + @@ -418,7 +397,14 @@ async function handleImport() { diff --git a/frontend/src/views/desktop/Dashboard.vue b/frontend/src/views/desktop/Dashboard.vue index 7c26156..1b650fd 100644 --- a/frontend/src/views/desktop/Dashboard.vue +++ b/frontend/src/views/desktop/Dashboard.vue @@ -27,68 +27,146 @@ onMounted(async () => { } }) -function goWeeklyReport() { - router.push('/weekly-report') +function goWeeklyReport(managerId?: string) { + if (managerId) router.push({ path: '/weekly-report', query: { manager_id: managerId } }) + else router.push('/weekly-report') +} + +function rowState(p: any): 'full' | 'catching' | 'missing' { + if (p.has_reported_today && p.completed) return 'full' + if (p.has_reported_today && !p.completed) return 'catching' + return 'missing' } diff --git a/frontend/src/views/desktop/ManagerWorkspace.vue b/frontend/src/views/desktop/ManagerWorkspace.vue index 2fde565..00d6e3c 100644 --- a/frontend/src/views/desktop/ManagerWorkspace.vue +++ b/frontend/src/views/desktop/ManagerWorkspace.vue @@ -13,8 +13,10 @@ const miniBusiness = ref([]) const dailyNotes = ref([]) const keyVisits = ref([]) const customers = ref([]) +const allUsers = ref([]) +const plannedVisitors = ref([]) +const dialogTimeRange = ref(null) -// Dialog const dialogVisible = ref(false) const dialogMode = ref<'create' | 'edit'>('create') const dialogType = ref('') @@ -22,10 +24,23 @@ const form = ref({}) const categories = ['行政事务', '合同整理', '发票处理', '内部会议', '培训学习', '其他'] +const statusPick = ref>({}) + +const planStatuses = ['计划中', '已完成', '已取消'] +const miniStatuses = ['跟进中', '已签约', '已流失'] +const keyStatuses = ['未开始', '进行中', '已完成'] + onMounted(async () => { - await Promise.all([loadAll(), loadCustomers()]) + await Promise.all([loadAll(), loadCustomers(), loadUsers()]) }) +async function loadUsers() { + try { + const res = await api.get('/users/') + allUsers.value = res.data || [] + } catch (_) {} +} + function typeLabel(t: string) { const labels: Record = { visit: '拜访记录', note: '今日纪要', plan: '工作计划', mini: '小微商机', key: '要客拜访' } return labels[t] || '' @@ -57,23 +72,31 @@ async function loadAll() { function openCreate(type: string) { dialogMode.value = 'create'; dialogType.value = type - if (type === 'visit') form.value = { customer_id: '', visit_date: todayStr(), visit_method: '上门', time_range: '', communication_content: '', customer_demand: '' } + dialogTimeRange.value = null + if (type === 'visit') form.value = { customer_id: '', visit_date: todayStr(), visit_method: '上门', time_range: '', visitor_name: '', visitor_phone: '', communication_content: '', customer_demand: '' } else if (type === 'note') form.value = { note_date: todayStr(), category: '其他', content: '', time_range: '' } else if (type === 'plan') form.value = { customer_id: '', plan_content: '', plan_date: todayStr(), status: '计划中' } else if (type === 'mini') form.value = { customer_id: '', product_type: '', amount: '', follow_up_detail: '', status: '跟进中', expected_revenue_date: '' } - else if (type === 'key') form.value = { customer_id: '', urgency_level: '一般', description: '', progress_status: '未开始', planned_date: '', planned_visitor: '', visit_target: '' } + else if (type === 'key') { form.value = { customer_id: '', urgency_level: '一般', description: '', progress_status: '未开始', planned_date: '', planned_visitor: '', visit_target: '' }; plannedVisitors.value = [] } dialogVisible.value = true } function openEdit(type: string, item: any) { dialogMode.value = 'edit'; dialogType.value = type form.value = { ...item } + dialogTimeRange.value = null + if ((type === 'visit' || type === 'note') && item.time_range && item.time_range.includes('-')) { + const parts = item.time_range.split('-') + dialogTimeRange.value = [parts[0], parts[1]] + } + if (type === 'key') { + plannedVisitors.value = item.planned_visitor ? item.planned_visitor.split('、') : [] + } dialogVisible.value = true } async function handleSave() { - const t = dialogType.value - const d = form.value + const t = dialogType.value; const d = form.value try { if (dialogMode.value === 'create') { switch (t) { @@ -113,6 +136,36 @@ async function handleDelete(type: string, id: string) { } catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') } } +function onVisitorsChange(val: string[]) { + form.value.planned_visitor = val.join('、') +} + +function onDialogTimeChange(val: [string, string] | null) { + form.value.time_range = val ? val.join('-') : '' +} + +// Quick status change +async function quickStatusChange(type: string, row: any, newStatus: string) { + try { + await ElMessageBox.confirm(`确定将状态改为「${newStatus}」?`, '确认', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }) + const payload: any = {} + if (type === 'plan') payload.status = newStatus + else if (type === 'mini') payload.status = newStatus + else if (type === 'key') payload.progress_status = newStatus + let url = '' + if (type === 'plan') url = `/work-plans/${row.id}` + else if (type === 'mini') url = `/mini-business/${row.id}` + else if (type === 'key') url = `/key-visits/${row.id}` + await api.put(url, payload) + ElMessage.success('状态已更新') + await loadAll() + } catch (e: any) { + if (e !== 'cancel') ElMessage.error('更新失败: ' + (e.response?.data?.detail || e.message)) + // Reset statusPick back to original on cancel/error + delete statusPick.value[row.id] + } +} + const visitsByDate = computed(() => { const g: Record = {} for (const v of visits.value) { const d = v.visit_date; if (!g[d]) g[d] = []; g[d].push(v) } @@ -127,24 +180,32 @@ const notesByDate = computed(() => { diff --git a/frontend/src/views/desktop/Settings.vue b/frontend/src/views/desktop/Settings.vue index 5cd6a0d..27c8c44 100644 --- a/frontend/src/views/desktop/Settings.vue +++ b/frontend/src/views/desktop/Settings.vue @@ -11,11 +11,9 @@ const remindLoading = ref(false) const announceLoading = ref(false) const dailyCheckLoading = ref(false) -// Import -const importDialogVisible = ref(false) +const importLoading = ref(false) const importFile = ref(null) const importPreview = ref(null) -const importLoading = ref(false) const importResult = ref(null) onMounted(async () => { @@ -29,32 +27,21 @@ async function handleRemind() { if (!selectedUserIds.value.length) { ElMessage.warning('请选择要提醒的人员'); return } remindLoading.value = true try { - const res = await api.post('/wecom/remind', { - user_ids: selectedUserIds.value, - message: remindMessage.value || undefined, - }) + const res = await api.post('/wecom/remind', { user_ids: selectedUserIds.value, message: remindMessage.value || undefined }) ElMessage.success(`已发送提醒给 ${res.data.sent_to} 人`) - } catch (e: any) { - ElMessage.error('发送失败') - } finally { - remindLoading.value = false - } + } catch (e: any) { ElMessage.error('发送失败') } + finally { remindLoading.value = false } } async function handleAnnouncement() { if (!announcementContent.value) { ElMessage.warning('请输入公告内容'); return } announceLoading.value = true try { - const res = await api.post('/wecom/announcement', { - content: announcementContent.value, - }) + const res = await api.post('/wecom/announcement', { content: announcementContent.value }) ElMessage.success(`公告已推送给 ${res.data.sent_to} 人`) announcementContent.value = '' - } catch (e: any) { - ElMessage.error('推送失败') - } finally { - announceLoading.value = false - } + } catch (e: any) { ElMessage.error('推送失败') } + finally { announceLoading.value = false } } async function handleDailyCheck() { @@ -62,18 +49,22 @@ async function handleDailyCheck() { try { const res = await api.post('/wecom/trigger-daily-check') ElMessage.success(`已执行:${res.data.reported}/${res.data.total_managers} 人已填报`) - } catch (e: any) { - ElMessage.error('执行失败') - } finally { - dailyCheckLoading.value = false - } + } catch (e: any) { ElMessage.error('执行失败') } + finally { dailyCheckLoading.value = false } } function handleImportFile(e: Event) { const target = e.target as HTMLInputElement - if (target.files?.[0]) { - importFile.value = target.files[0] - } + if (target.files?.[0]) importFile.value = target.files[0] +} + +async function downloadWeeklyTemplate() { + try { + const res = await api.get('/import/template', { responseType: 'blob' }) + const url = URL.createObjectURL(res.data) + const a = document.createElement('a'); a.href = url; a.download = 'weekly_report_template.xlsx'; a.click() + URL.revokeObjectURL(url) + } catch (e: any) { ElMessage.error('下载失败') } } async function handleImportPreview() { @@ -82,26 +73,31 @@ async function handleImportPreview() { const formData = new FormData() formData.append('file', importFile.value) try { - const res = await api.post('/import/weekly-report', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }) + const res = await api.post('/import/weekly-report', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) importPreview.value = res.data.preview importResult.value = res.data - } catch (e: any) { - ElMessage.error('解析失败') - } finally { - importLoading.value = false - } + } catch (e: any) { ElMessage.error('解析失败') } + finally { importLoading.value = false } } diff --git a/frontend/src/views/desktop/UserManage.vue b/frontend/src/views/desktop/UserManage.vue index 2076ae6..5a650e7 100644 --- a/frontend/src/views/desktop/UserManage.vue +++ b/frontend/src/views/desktop/UserManage.vue @@ -17,14 +17,10 @@ const roleOptions = [ ] const roleTagType: Record = { - manager: '', - director: 'warning', - leader: 'info', + manager: '', director: 'warning', leader: 'info', } const roleLabel: Record = { - manager: '客户经理', - director: '支局长', - leader: '分管领导', + manager: '客户经理', director: '支局长', leader: '分管领导', } async function loadUsers() { @@ -32,11 +28,8 @@ async function loadUsers() { try { const res = await api.get('/users/') users.value = res.data - } catch (e: any) { - ElMessage.error('加载用户列表失败') - } finally { - loading.value = false - } + } catch (e: any) { ElMessage.error('加载用户列表失败') } + finally { loading.value = false } } onMounted(loadUsers) @@ -51,34 +44,29 @@ function openEdit(user: any) { async function handleSave() { if (!editUser.value) return try { - await api.put(`/users/${editUser.value.id}/role`, { - role: editRole.value, - department: editDepartment.value, - }) + await api.put(`/users/${editUser.value.id}/role`, { role: editRole.value, department: editDepartment.value }) ElMessage.success('角色已更新') editDialogVisible.value = false await loadUsers() - } catch (e: any) { - ElMessage.error('更新失败: ' + (e.response?.data?.detail || e.message)) - } + } catch (e: any) { ElMessage.error('更新失败: ' + (e.response?.data?.detail || e.message)) } }