Spaces:
Sleeping
Sleeping
Trae Assistant commited on
Commit ·
0f22fe9
0
Parent(s):
Initial commit with enhancements
Browse files- Dockerfile +16 -0
- README.md +76 -0
- app.py +375 -0
- instance/permit_flow.db +0 -0
- requirements.txt +3 -0
- templates/index.html +495 -0
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
# Create instance directory for SQLite
|
| 11 |
+
RUN mkdir -p instance
|
| 12 |
+
RUN chmod 777 instance
|
| 13 |
+
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Permit Flow Agent
|
| 3 |
+
emoji: 📜
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
+
short_description: 智能商业证照合规导航与资产生成专家
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Permit Flow Agent - 智能商业证照导航
|
| 13 |
+
|
| 14 |
+
**Permit Flow Agent** 是一个专为创业者和企业主设计的 AI 智能体,旨在简化复杂的商业行政审批流程。它能根据您的业务场景(如餐饮、医疗、物流等),智能分析所需的许可证照路径,生成合规清单,并辅助撰写申请文书。
|
| 15 |
+
|
| 16 |
+
## 核心功能
|
| 17 |
+
|
| 18 |
+
1. **智能合规分析 (AI Compliance Analysis)**
|
| 19 |
+
* 输入您的商业计划(如“在成都高新区开一家猫咖”)。
|
| 20 |
+
* AI 自动分析所需的证照(营业执照、食品经营许可证、动物防疫条件合格证等)。
|
| 21 |
+
* 评估合规风险指数。
|
| 22 |
+
|
| 23 |
+
2. **办证路径规划 (License Roadmap)**
|
| 24 |
+
* 生成可视化的办证流程图。
|
| 25 |
+
* 明确审批部门、优先级、预计耗时。
|
| 26 |
+
* 提供详细的材料清单(Requirements Checklist)。
|
| 27 |
+
|
| 28 |
+
3. **资产生成与管理 (Asset Vault)**
|
| 29 |
+
* AI 辅助生成申请文书、制度规范(如“食品安全管理制度”)。
|
| 30 |
+
* 支持 Markdown 格式的文书预览与导出。
|
| 31 |
+
* 集中管理所有申请材料。
|
| 32 |
+
|
| 33 |
+
4. **Mock Mode (模拟模式)**
|
| 34 |
+
* 内置高可用模拟数据,确保在无网络或 API 受限环境下也能完整体验流程。
|
| 35 |
+
|
| 36 |
+
## 技术栈
|
| 37 |
+
|
| 38 |
+
* **Backend**: Python Flask
|
| 39 |
+
* **Frontend**: Vue.js 3 + Tailwind CSS (Single File Component)
|
| 40 |
+
* **AI Engine**: SiliconFlow API (Qwen/Qwen2.5-7B-Instruct)
|
| 41 |
+
* **Database**: SQLite (Local Persistence)
|
| 42 |
+
* **Deployment**: Docker
|
| 43 |
+
|
| 44 |
+
## 快速开始
|
| 45 |
+
|
| 46 |
+
### 本地运行
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
# 安装依赖
|
| 50 |
+
pip install -r requirements.txt
|
| 51 |
+
|
| 52 |
+
# 运行应用
|
| 53 |
+
python app.py
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
访问: `http://localhost:7860`
|
| 57 |
+
|
| 58 |
+
### Docker 运行
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
docker build -t permit-flow-agent .
|
| 62 |
+
docker run -p 7860:7860 permit-flow-agent
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## 场景示例
|
| 66 |
+
|
| 67 |
+
**用户输入**:
|
| 68 |
+
> "我想在上海浦东新区开一家提供医美服务的诊所,大概300平米。"
|
| 69 |
+
|
| 70 |
+
**AI 分析结果**:
|
| 71 |
+
* **核心证照**: 医疗机构执业许可证、医师执业证书、护士执业证书。
|
| 72 |
+
* **前置审批**: 消防验收、环境影响评价。
|
| 73 |
+
* **风险提示**: 医美行业监管严格,需特别注意广告合规。
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
*Powered by SiliconFlow & Trae IDE*
|
app.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import sqlite3
|
| 5 |
+
import requests
|
| 6 |
+
import datetime
|
| 7 |
+
from flask import Flask, render_template, request, jsonify, send_from_directory
|
| 8 |
+
from werkzeug.utils import secure_filename
|
| 9 |
+
|
| 10 |
+
# Configuration
|
| 11 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
INSTANCE_PATH = os.path.join(BASE_DIR, "instance")
|
| 13 |
+
DB_PATH = os.path.join(INSTANCE_PATH, "permit_flow.db")
|
| 14 |
+
os.makedirs(INSTANCE_PATH, exist_ok=True)
|
| 15 |
+
|
| 16 |
+
app = Flask(__name__, instance_path=INSTANCE_PATH)
|
| 17 |
+
app.config['SECRET_KEY'] = 'dev-secret-key-permit-flow'
|
| 18 |
+
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
|
| 19 |
+
|
| 20 |
+
# Logging
|
| 21 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# SiliconFlow API Configuration
|
| 25 |
+
SILICONFLOW_API_KEY = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
|
| 26 |
+
SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
|
| 27 |
+
MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct"
|
| 28 |
+
|
| 29 |
+
# Database Setup
|
| 30 |
+
def get_db():
|
| 31 |
+
conn = sqlite3.connect(DB_PATH)
|
| 32 |
+
conn.row_factory = sqlite3.Row
|
| 33 |
+
return conn
|
| 34 |
+
|
| 35 |
+
def init_db():
|
| 36 |
+
conn = get_db()
|
| 37 |
+
c = conn.cursor()
|
| 38 |
+
|
| 39 |
+
# Projects table (e.g., "Open a Cafe")
|
| 40 |
+
c.execute('''CREATE TABLE IF NOT EXISTS projects (
|
| 41 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 42 |
+
name TEXT NOT NULL,
|
| 43 |
+
description TEXT,
|
| 44 |
+
industry TEXT,
|
| 45 |
+
location TEXT,
|
| 46 |
+
status TEXT DEFAULT 'planning',
|
| 47 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 48 |
+
risk_score INTEGER DEFAULT 0
|
| 49 |
+
)''')
|
| 50 |
+
|
| 51 |
+
# Permits/Licenses table
|
| 52 |
+
c.execute('''CREATE TABLE IF NOT EXISTS permits (
|
| 53 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 54 |
+
project_id INTEGER,
|
| 55 |
+
name TEXT NOT NULL,
|
| 56 |
+
authority TEXT,
|
| 57 |
+
status TEXT DEFAULT 'pending', -- pending, preparing, submitted, approved
|
| 58 |
+
priority TEXT DEFAULT 'medium',
|
| 59 |
+
requirements TEXT, -- JSON list of requirements
|
| 60 |
+
estimated_time TEXT,
|
| 61 |
+
FOREIGN KEY (project_id) REFERENCES projects (id)
|
| 62 |
+
)''')
|
| 63 |
+
|
| 64 |
+
# Assets table (generated docs, uploaded files)
|
| 65 |
+
c.execute('''CREATE TABLE IF NOT EXISTS assets (
|
| 66 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 67 |
+
project_id INTEGER,
|
| 68 |
+
permit_id INTEGER,
|
| 69 |
+
name TEXT NOT NULL,
|
| 70 |
+
type TEXT, -- document, form, guide
|
| 71 |
+
content TEXT, -- Markdown content or file path
|
| 72 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 73 |
+
FOREIGN KEY (project_id) REFERENCES projects (id)
|
| 74 |
+
)''')
|
| 75 |
+
|
| 76 |
+
conn.commit()
|
| 77 |
+
conn.close()
|
| 78 |
+
|
| 79 |
+
# Initialize DB on start
|
| 80 |
+
init_db()
|
| 81 |
+
|
| 82 |
+
def populate_default_data():
|
| 83 |
+
conn = get_db()
|
| 84 |
+
cursor = conn.cursor()
|
| 85 |
+
# Check if projects exist
|
| 86 |
+
cursor.execute('SELECT count(*) FROM projects')
|
| 87 |
+
if cursor.fetchone()[0] == 0:
|
| 88 |
+
logger.info("Populating default data...")
|
| 89 |
+
# Create a default project
|
| 90 |
+
default_project = {
|
| 91 |
+
"name": "示例:成都市高新区精品咖啡馆",
|
| 92 |
+
"description": "计划在成都市高新区天府三街开设一家主要经营精品手冲咖啡和少量西式甜点的咖啡馆,面积约80平方米。",
|
| 93 |
+
"industry": "餐饮/食品",
|
| 94 |
+
"location": "成都市高新区",
|
| 95 |
+
"risk_score": 45
|
| 96 |
+
}
|
| 97 |
+
cursor.execute('INSERT INTO projects (name, description, industry, location, risk_score) VALUES (?, ?, ?, ?, ?)',
|
| 98 |
+
(default_project['name'], default_project['description'], default_project['industry'], default_project['location'], default_project['risk_score']))
|
| 99 |
+
project_id = cursor.lastrowid
|
| 100 |
+
|
| 101 |
+
# Default permits
|
| 102 |
+
permits = [
|
| 103 |
+
{"name": "营业执照", "authority": "市场监督管理局", "priority": "high", "estimated_time": "3个工作日", "requirements": ["身份证原件", "经营场所证明", "名称预先核准通知书"]},
|
| 104 |
+
{"name": "食品经营许可证", "authority": "市场监督管理局", "priority": "high", "estimated_time": "15个工作日", "requirements": ["营业执照", "健康证", "食品安全规章制度", "平面布局图"]},
|
| 105 |
+
{"name": "招牌设置许可", "authority": "城市管理行政执法局", "priority": "medium", "estimated_time": "5个工作日", "requirements": ["效果图", "租赁合同", "营业执照复印件"]}
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
for p in permits:
|
| 109 |
+
reqs_json = json.dumps(p['requirements'], ensure_ascii=False)
|
| 110 |
+
cursor.execute('INSERT INTO permits (project_id, name, authority, priority, estimated_time, requirements) VALUES (?, ?, ?, ?, ?, ?)',
|
| 111 |
+
(project_id, p['name'], p['authority'], p['priority'], p['estimated_time'], reqs_json))
|
| 112 |
+
permit_id = cursor.lastrowid
|
| 113 |
+
|
| 114 |
+
# Add a sample asset for the first permit
|
| 115 |
+
if p['name'] == "食品经营许可证":
|
| 116 |
+
content = "# 食品安全管理制度\n\n## 第一章 总则\n\n第一条 为保障食品安全,根据《食品安全法》制定本制��。\n\n## 第二章 从业人员健康管理\n\n第二条 从业人员必须持有效健康证上岗..."
|
| 117 |
+
cursor.execute('INSERT INTO assets (project_id, permit_id, name, type, content) VALUES (?, ?, ?, ?, ?)',
|
| 118 |
+
(project_id, permit_id, "食品安全管理制度范本", "document", content))
|
| 119 |
+
|
| 120 |
+
conn.commit()
|
| 121 |
+
conn.close()
|
| 122 |
+
|
| 123 |
+
populate_default_data()
|
| 124 |
+
|
| 125 |
+
# --- AI Service ---
|
| 126 |
+
def call_ai_service(system_prompt, user_prompt, temperature=0.7):
|
| 127 |
+
headers = {
|
| 128 |
+
"Authorization": f"Bearer {SILICONFLOW_API_KEY}",
|
| 129 |
+
"Content-Type": "application/json"
|
| 130 |
+
}
|
| 131 |
+
payload = {
|
| 132 |
+
"model": MODEL_NAME,
|
| 133 |
+
"messages": [
|
| 134 |
+
{"role": "system", "content": system_prompt},
|
| 135 |
+
{"role": "user", "content": user_prompt}
|
| 136 |
+
],
|
| 137 |
+
"temperature": temperature,
|
| 138 |
+
"max_tokens": 2048
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
response = requests.post(SILICONFLOW_API_URL, headers=headers, json=payload, timeout=30)
|
| 143 |
+
response.raise_for_status()
|
| 144 |
+
result = response.json()
|
| 145 |
+
content = result['choices'][0]['message']['content']
|
| 146 |
+
return content
|
| 147 |
+
except Exception as e:
|
| 148 |
+
logger.error(f"AI Service Error: {str(e)}")
|
| 149 |
+
# Mock Fallback
|
| 150 |
+
return None
|
| 151 |
+
|
| 152 |
+
def analyze_business_scenario(name, description, location):
|
| 153 |
+
system_prompt = """
|
| 154 |
+
你是一个专业的商业合规与行政审批专家智能体 (Permit Flow Agent)。
|
| 155 |
+
你的任务是根据用户的商业计划,分析需要办理的行政许可证照。
|
| 156 |
+
|
| 157 |
+
请以 JSON 格式输出分析结果,不要包含 markdown 代码块标记,直接返回 JSON 对象。
|
| 158 |
+
JSON 结构如下:
|
| 159 |
+
{
|
| 160 |
+
"analysis": "对商业计划的简短合规性分析",
|
| 161 |
+
"risk_score": 0-100 (风险评分,越高越难),
|
| 162 |
+
"permits": [
|
| 163 |
+
{
|
| 164 |
+
"name": "许可证名称 (如:食品经营许可证)",
|
| 165 |
+
"authority": "审批部门 (如:市场监督管理局)",
|
| 166 |
+
"priority": "high/medium/low",
|
| 167 |
+
"estimated_time": "预计耗时 (如:15个工作日)",
|
| 168 |
+
"requirements": ["要求1", "要求2"]
|
| 169 |
+
}
|
| 170 |
+
],
|
| 171 |
+
"suggested_assets": [
|
| 172 |
+
{
|
| 173 |
+
"name": "资产名称 (如:食品安全管理制度范本)",
|
| 174 |
+
"type": "document",
|
| 175 |
+
"content_brief": "文档的大致内容描述"
|
| 176 |
+
}
|
| 177 |
+
]
|
| 178 |
+
}
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
user_prompt = f"我的商业计划是:{name}。\n详细描述:{description}。\n所在地:{location}。\n请帮我分析需要办理哪些证照。"
|
| 182 |
+
|
| 183 |
+
ai_response = call_ai_service(system_prompt, user_prompt)
|
| 184 |
+
|
| 185 |
+
if ai_response:
|
| 186 |
+
try:
|
| 187 |
+
# Clean up markdown code blocks if present
|
| 188 |
+
cleaned_response = ai_response.replace("```json", "").replace("```", "").strip()
|
| 189 |
+
return json.loads(cleaned_response)
|
| 190 |
+
except json.JSONDecodeError:
|
| 191 |
+
logger.error("Failed to parse AI JSON response")
|
| 192 |
+
pass # Fallthrough to mock
|
| 193 |
+
|
| 194 |
+
# Mock Data Fallback
|
| 195 |
+
logger.warning("Using Mock Data for Analysis")
|
| 196 |
+
return {
|
| 197 |
+
"analysis": "模拟模式:基于您的描述,这是一个标准的商业实体注册流程。系统已为您生成预设的合规路径。",
|
| 198 |
+
"risk_score": 35,
|
| 199 |
+
"permits": [
|
| 200 |
+
{
|
| 201 |
+
"name": "营业执照",
|
| 202 |
+
"authority": "市场监督管理局",
|
| 203 |
+
"priority": "high",
|
| 204 |
+
"estimated_time": "3个工作日",
|
| 205 |
+
"requirements": ["身份证原件", "场地证明", "核名通知书"]
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"name": "行业综合许可证",
|
| 209 |
+
"authority": "行政审批局",
|
| 210 |
+
"priority": "medium",
|
| 211 |
+
"estimated_time": "10个工作日",
|
| 212 |
+
"requirements": ["申请表", "平面图", "承诺书"]
|
| 213 |
+
}
|
| 214 |
+
],
|
| 215 |
+
"suggested_assets": [
|
| 216 |
+
{
|
| 217 |
+
"name": "企业设立登记申请书",
|
| 218 |
+
"type": "form",
|
| 219 |
+
"content_brief": "标准申请表格模板"
|
| 220 |
+
}
|
| 221 |
+
]
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
def generate_asset_content(asset_name, context):
|
| 225 |
+
system_prompt = """
|
| 226 |
+
你是一个专业的商业文书撰写助手。请根据上下文生成详细的文档内容。
|
| 227 |
+
输出格式为 Markdown。
|
| 228 |
+
"""
|
| 229 |
+
user_prompt = f"请为项目撰写一份'{asset_name}'。\n背景信息:{context}"
|
| 230 |
+
|
| 231 |
+
content = call_ai_service(system_prompt, user_prompt)
|
| 232 |
+
if not content:
|
| 233 |
+
return f"# {asset_name}\n\n*(模拟模式生成内容)*\n\n这是系统自动生成的{asset_name}模板。\n\n1. 请填写相关信息...\n2. 签字盖章..."
|
| 234 |
+
return content
|
| 235 |
+
|
| 236 |
+
# --- Routes ---
|
| 237 |
+
|
| 238 |
+
@app.route('/')
|
| 239 |
+
def index():
|
| 240 |
+
return render_template('index.html')
|
| 241 |
+
|
| 242 |
+
@app.route('/api/projects', methods=['GET'])
|
| 243 |
+
def list_projects():
|
| 244 |
+
conn = get_db()
|
| 245 |
+
projects = conn.execute('SELECT * FROM projects ORDER BY created_at DESC').fetchall()
|
| 246 |
+
conn.close()
|
| 247 |
+
return jsonify([dict(p) for p in projects])
|
| 248 |
+
|
| 249 |
+
@app.route('/api/projects', methods=['POST'])
|
| 250 |
+
def create_project():
|
| 251 |
+
data = request.json
|
| 252 |
+
name = data.get('name')
|
| 253 |
+
description = data.get('description')
|
| 254 |
+
location = data.get('location', 'Unknown')
|
| 255 |
+
industry = data.get('industry', 'General')
|
| 256 |
+
|
| 257 |
+
# 1. AI Analysis
|
| 258 |
+
analysis = analyze_business_scenario(name, description, location)
|
| 259 |
+
|
| 260 |
+
conn = get_db()
|
| 261 |
+
cursor = conn.cursor()
|
| 262 |
+
|
| 263 |
+
# 2. Save Project
|
| 264 |
+
cursor.execute('INSERT INTO projects (name, description, industry, location, risk_score) VALUES (?, ?, ?, ?, ?)',
|
| 265 |
+
(name, analysis['analysis'], industry, location, analysis['risk_score']))
|
| 266 |
+
project_id = cursor.lastrowid
|
| 267 |
+
|
| 268 |
+
# 3. Save Permits
|
| 269 |
+
for permit in analysis['permits']:
|
| 270 |
+
reqs_json = json.dumps(permit['requirements'], ensure_ascii=False)
|
| 271 |
+
cursor.execute('INSERT INTO permits (project_id, name, authority, priority, estimated_time, requirements) VALUES (?, ?, ?, ?, ?, ?)',
|
| 272 |
+
(project_id, permit['name'], permit['authority'], permit['priority'], permit['estimated_time'], reqs_json))
|
| 273 |
+
permit_id = cursor.lastrowid
|
| 274 |
+
|
| 275 |
+
# Create initial asset for this permit if available
|
| 276 |
+
# (Simplified: just create one general asset for now based on suggestions)
|
| 277 |
+
|
| 278 |
+
# 4. Create Suggested Assets
|
| 279 |
+
for asset in analysis.get('suggested_assets', []):
|
| 280 |
+
# Generate content immediately or later? Let's generate a placeholder first to be fast
|
| 281 |
+
content = f"# {asset['name']}\n\n等待生成..."
|
| 282 |
+
cursor.execute('INSERT INTO assets (project_id, permit_id, name, type, content) VALUES (?, ?, ?, ?, ?)',
|
| 283 |
+
(project_id, 0, asset['name'], asset['type'], content))
|
| 284 |
+
|
| 285 |
+
conn.commit()
|
| 286 |
+
conn.close()
|
| 287 |
+
|
| 288 |
+
return jsonify({"success": True, "project_id": project_id})
|
| 289 |
+
|
| 290 |
+
@app.route('/api/projects/<int:project_id>', methods=['GET'])
|
| 291 |
+
def get_project(project_id):
|
| 292 |
+
conn = get_db()
|
| 293 |
+
project = conn.execute('SELECT * FROM projects WHERE id = ?', (project_id,)).fetchone()
|
| 294 |
+
permits = conn.execute('SELECT * FROM permits WHERE project_id = ?', (project_id,)).fetchall()
|
| 295 |
+
assets = conn.execute('SELECT * FROM assets WHERE project_id = ?', (project_id,)).fetchall()
|
| 296 |
+
conn.close()
|
| 297 |
+
|
| 298 |
+
if not project:
|
| 299 |
+
return jsonify({"error": "Not found"}), 404
|
| 300 |
+
|
| 301 |
+
return jsonify({
|
| 302 |
+
"project": dict(project),
|
| 303 |
+
"permits": [dict(p) for p in permits],
|
| 304 |
+
"assets": [dict(a) for a in assets]
|
| 305 |
+
})
|
| 306 |
+
|
| 307 |
+
@app.route('/api/upload', methods=['POST'])
|
| 308 |
+
def upload_file():
|
| 309 |
+
if 'file' not in request.files:
|
| 310 |
+
return jsonify({"error": "No file part"}), 400
|
| 311 |
+
file = request.files['file']
|
| 312 |
+
if file.filename == '':
|
| 313 |
+
return jsonify({"error": "No selected file"}), 400
|
| 314 |
+
if file:
|
| 315 |
+
filename = secure_filename(file.filename)
|
| 316 |
+
# In a real app, save to disk or S3. Here we just return success and mock a DB entry
|
| 317 |
+
# save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 318 |
+
# file.save(save_path)
|
| 319 |
+
|
| 320 |
+
# Add to assets table as a record
|
| 321 |
+
project_id = request.form.get('project_id')
|
| 322 |
+
if project_id:
|
| 323 |
+
conn = get_db()
|
| 324 |
+
cursor = conn.cursor()
|
| 325 |
+
cursor.execute('INSERT INTO assets (project_id, permit_id, name, type, content) VALUES (?, ?, ?, ?, ?)',
|
| 326 |
+
(project_id, 0, filename, 'file', f"已上传文件: {filename} (存储于服务器)"))
|
| 327 |
+
conn.commit()
|
| 328 |
+
conn.close()
|
| 329 |
+
|
| 330 |
+
return jsonify({"success": True, "filename": filename})
|
| 331 |
+
return jsonify({"error": "Upload failed"}), 500
|
| 332 |
+
|
| 333 |
+
@app.errorhandler(404)
|
| 334 |
+
def page_not_found(e):
|
| 335 |
+
return render_template('index.html'), 200 # Support SPA routing if needed, or just return index
|
| 336 |
+
|
| 337 |
+
@app.errorhandler(500)
|
| 338 |
+
def internal_server_error(e):
|
| 339 |
+
return jsonify(error=str(e)), 500
|
| 340 |
+
|
| 341 |
+
@app.route('/api/assets/generate', methods=['POST'])
|
| 342 |
+
def generate_asset():
|
| 343 |
+
data = request.json
|
| 344 |
+
asset_id = data.get('asset_id')
|
| 345 |
+
project_context = data.get('context', '')
|
| 346 |
+
|
| 347 |
+
conn = get_db()
|
| 348 |
+
asset = conn.execute('SELECT * FROM assets WHERE id = ?', (asset_id,)).fetchone()
|
| 349 |
+
|
| 350 |
+
if not asset:
|
| 351 |
+
conn.close()
|
| 352 |
+
return jsonify({"error": "Asset not found"}), 404
|
| 353 |
+
|
| 354 |
+
content = generate_asset_content(asset['name'], project_context)
|
| 355 |
+
|
| 356 |
+
conn.execute('UPDATE assets SET content = ? WHERE id = ?', (content, asset_id))
|
| 357 |
+
conn.commit()
|
| 358 |
+
conn.close()
|
| 359 |
+
|
| 360 |
+
return jsonify({"success": True, "content": content})
|
| 361 |
+
|
| 362 |
+
@app.route('/api/permits/<int:permit_id>/status', methods=['POST'])
|
| 363 |
+
def update_permit_status(permit_id):
|
| 364 |
+
data = request.json
|
| 365 |
+
status = data.get('status')
|
| 366 |
+
|
| 367 |
+
conn = get_db()
|
| 368 |
+
conn.execute('UPDATE permits SET status = ? WHERE id = ?', (status, permit_id))
|
| 369 |
+
conn.commit()
|
| 370 |
+
conn.close()
|
| 371 |
+
|
| 372 |
+
return jsonify({"success": True})
|
| 373 |
+
|
| 374 |
+
if __name__ == '__main__':
|
| 375 |
+
app.run(host='0.0.0.0', port=7860, debug=True)
|
instance/permit_flow.db
ADDED
|
Binary file (20.5 kB). View file
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==3.0.0
|
| 2 |
+
requests==2.31.0
|
| 3 |
+
gunicorn==21.2.0
|
templates/index.html
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Permit Flow Agent - 智能商业证照导航</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 10 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 11 |
+
<style>
|
| 12 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 13 |
+
body { font-family: 'Inter', sans-serif; background-color: #f3f4f6; }
|
| 14 |
+
.markdown-body h1 { font-size: 1.5em; font-weight: bold; margin-bottom: 0.5em; margin-top: 1em; }
|
| 15 |
+
.markdown-body h2 { font-size: 1.25em; font-weight: bold; margin-bottom: 0.5em; margin-top: 1em; }
|
| 16 |
+
.markdown-body p { margin-bottom: 1em; line-height: 1.6; }
|
| 17 |
+
.markdown-body ul { list-style-type: disc; padding-left: 1.5em; margin-bottom: 1em; }
|
| 18 |
+
.markdown-body ol { list-style-type: decimal; padding-left: 1.5em; margin-bottom: 1em; }
|
| 19 |
+
.markdown-body blockquote { border-left: 4px solid #e5e7eb; padding-left: 1em; color: #6b7280; }
|
| 20 |
+
[v-cloak] { display: none; }
|
| 21 |
+
</style>
|
| 22 |
+
<script>
|
| 23 |
+
tailwind.config = {
|
| 24 |
+
theme: {
|
| 25 |
+
extend: {
|
| 26 |
+
colors: {
|
| 27 |
+
primary: '#2563eb',
|
| 28 |
+
secondary: '#4f46e5',
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
</script>
|
| 34 |
+
</head>
|
| 35 |
+
<body class="bg-gray-50 text-gray-800">
|
| 36 |
+
<div id="app" v-cloak class="min-h-screen flex flex-col">
|
| 37 |
+
<!-- Navigation -->
|
| 38 |
+
<nav class="bg-white shadow-sm border-b border-gray-200 z-10">
|
| 39 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 40 |
+
<div class="flex justify-between h-16">
|
| 41 |
+
<div class="flex">
|
| 42 |
+
<div class="flex-shrink-0 flex items-center">
|
| 43 |
+
<i class="fas fa-certificate text-primary text-2xl mr-2"></i>
|
| 44 |
+
<span class="font-bold text-xl text-gray-900">Permit Flow Agent</span>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="flex items-center space-x-4">
|
| 48 |
+
<button @click="currentView = 'list'" class="text-gray-500 hover:text-gray-700 px-3 py-2 rounded-md text-sm font-medium">
|
| 49 |
+
<i class="fas fa-home mr-1"></i> 首页
|
| 50 |
+
</button>
|
| 51 |
+
<a href="#" class="text-gray-500 hover:text-gray-700 px-3 py-2 rounded-md text-sm font-medium">
|
| 52 |
+
<i class="fas fa-book mr-1"></i> 法规库
|
| 53 |
+
</a>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</nav>
|
| 58 |
+
|
| 59 |
+
<!-- Main Content -->
|
| 60 |
+
<main class="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
| 61 |
+
|
| 62 |
+
<!-- Project List View -->
|
| 63 |
+
<div v-if="currentView === 'list'" class="space-y-6">
|
| 64 |
+
<div class="flex justify-between items-center">
|
| 65 |
+
<div>
|
| 66 |
+
<h1 class="text-2xl font-bold text-gray-900">我的项目</h1>
|
| 67 |
+
<p class="mt-1 text-sm text-gray-500">管理您的商业许可证照申请流程</p>
|
| 68 |
+
</div>
|
| 69 |
+
<button @click="showNewProjectModal = true" class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-sm flex items-center transition">
|
| 70 |
+
<i class="fas fa-plus mr-2"></i> 新建项目
|
| 71 |
+
</button>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div v-if="loading" class="flex justify-center py-12">
|
| 75 |
+
<i class="fas fa-spinner fa-spin text-4xl text-gray-300"></i>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div v-else-if="projects.length === 0" class="text-center py-12 bg-white rounded-lg shadow border border-gray-100">
|
| 79 |
+
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-blue-100">
|
| 80 |
+
<i class="fas fa-folder-open text-primary"></i>
|
| 81 |
+
</div>
|
| 82 |
+
<h3 class="mt-2 text-sm font-medium text-gray-900">暂无项目</h3>
|
| 83 |
+
<p class="mt-1 text-sm text-gray-500">开始一个新的商业合规规划吧。</p>
|
| 84 |
+
<div class="mt-6">
|
| 85 |
+
<button @click="showNewProjectModal = true" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary hover:bg-blue-700">
|
| 86 |
+
<i class="fas fa-plus mr-2"></i> 新建项目
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div v-else class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
| 92 |
+
<div v-for="project in projects" :key="project.id"
|
| 93 |
+
@click="openProject(project.id)"
|
| 94 |
+
class="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition cursor-pointer border border-gray-100 group">
|
| 95 |
+
<div class="px-4 py-5 sm:p-6">
|
| 96 |
+
<div class="flex items-center justify-between">
|
| 97 |
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
| 98 |
+
:class="project.risk_score > 60 ? 'bg-red-100 text-red-800' : (project.risk_score > 30 ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800')">
|
| 99 |
+
风险指数: ${ project.risk_score }
|
| 100 |
+
</span>
|
| 101 |
+
<span class="text-xs text-gray-400">${ formatDate(project.created_at) }</span>
|
| 102 |
+
</div>
|
| 103 |
+
<h3 class="mt-2 text-lg font-medium text-gray-900 group-hover:text-primary transition">${ project.name }</h3>
|
| 104 |
+
<p class="mt-1 text-sm text-gray-500 truncate">${ project.location } · ${ project.industry }</p>
|
| 105 |
+
<p class="mt-3 text-sm text-gray-600 line-clamp-2">${ project.description }</p>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="bg-gray-50 px-4 py-4 sm:px-6 border-t border-gray-100 flex justify-between items-center">
|
| 108 |
+
<span class="text-sm font-medium text-primary hover:text-blue-500">查看详情 →</span>
|
| 109 |
+
<span class="text-xs text-gray-400">ID: ${ project.id }</span>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<!-- Project Detail View -->
|
| 116 |
+
<div v-if="currentView === 'detail' && activeProject" class="space-y-6">
|
| 117 |
+
<!-- Header -->
|
| 118 |
+
<div class="bg-white shadow rounded-lg px-4 py-5 sm:px-6 flex justify-between items-start">
|
| 119 |
+
<div>
|
| 120 |
+
<div class="flex items-center space-x-3">
|
| 121 |
+
<button @click="currentView = 'list'" class="text-gray-400 hover:text-gray-600">
|
| 122 |
+
<i class="fas fa-arrow-left"></i>
|
| 123 |
+
</button>
|
| 124 |
+
<h2 class="text-2xl font-bold text-gray-900">${ activeProject.project.name }</h2>
|
| 125 |
+
<span class="px-2 py-1 text-xs rounded-md bg-gray-100 text-gray-600">${ activeProject.project.industry }</span>
|
| 126 |
+
</div>
|
| 127 |
+
<p class="mt-1 max-w-2xl text-sm text-gray-500 ml-7">${ activeProject.project.location }</p>
|
| 128 |
+
</div>
|
| 129 |
+
<div class="text-right">
|
| 130 |
+
<span class="block text-sm text-gray-500">合规分析结论</span>
|
| 131 |
+
<span class="text-sm font-medium text-gray-900">${ activeProject.project.description }</span>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 136 |
+
<!-- Left: Permits Path -->
|
| 137 |
+
<div class="lg:col-span-2 space-y-6">
|
| 138 |
+
<div class="bg-white shadow rounded-lg overflow-hidden">
|
| 139 |
+
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 flex justify-between items-center">
|
| 140 |
+
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
| 141 |
+
<i class="fas fa-road text-primary mr-2"></i> 办证路径
|
| 142 |
+
</h3>
|
| 143 |
+
<span class="text-xs text-gray-500">共 ${ activeProject.permits.length } 项</span>
|
| 144 |
+
</div>
|
| 145 |
+
<ul class="divide-y divide-gray-200">
|
| 146 |
+
<li v-for="permit in activeProject.permits" :key="permit.id" class="px-4 py-4 sm:px-6 hover:bg-gray-50 transition">
|
| 147 |
+
<div class="flex items-center justify-between">
|
| 148 |
+
<div class="flex-1 min-w-0">
|
| 149 |
+
<div class="flex items-center justify-between">
|
| 150 |
+
<p class="text-sm font-medium text-primary truncate">${ permit.name }</p>
|
| 151 |
+
<div class="ml-2 flex-shrink-0 flex">
|
| 152 |
+
<p class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
| 153 |
+
:class="{
|
| 154 |
+
'bg-green-100 text-green-800': permit.status === 'approved',
|
| 155 |
+
'bg-yellow-100 text-yellow-800': permit.status === 'pending',
|
| 156 |
+
'bg-blue-100 text-blue-800': permit.status === 'submitted'
|
| 157 |
+
}">
|
| 158 |
+
${ permitStatusMap[permit.status] || permit.status }
|
| 159 |
+
</p>
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
<div class="mt-2 sm:flex sm:justify-between">
|
| 163 |
+
<div class="sm:flex">
|
| 164 |
+
<p class="flex items-center text-sm text-gray-500">
|
| 165 |
+
<i class="fas fa-building flex-shrink-0 mr-1.5 text-gray-400"></i>
|
| 166 |
+
${ permit.authority }
|
| 167 |
+
</p>
|
| 168 |
+
<p class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0 sm:ml-6">
|
| 169 |
+
<i class="fas fa-clock flex-shrink-0 mr-1.5 text-gray-400"></i>
|
| 170 |
+
预计 ${ permit.estimated_time }
|
| 171 |
+
</p>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
<!-- Requirements -->
|
| 175 |
+
<div class="mt-2">
|
| 176 |
+
<p class="text-xs text-gray-500 mb-1">所需材料:</p>
|
| 177 |
+
<div class="flex flex-wrap gap-2">
|
| 178 |
+
<span v-for="req in parseRequirements(permit.requirements)" :key="req"
|
| 179 |
+
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
|
| 180 |
+
${ req }
|
| 181 |
+
</span>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
<!-- Actions -->
|
| 186 |
+
<div class="ml-4 flex-shrink-0">
|
| 187 |
+
<select @change="updatePermitStatus(permit.id, $event.target.value)"
|
| 188 |
+
class="text-xs border-gray-300 rounded-md shadow-sm focus:ring-primary focus:border-primary">
|
| 189 |
+
<option value="pending" :selected="permit.status === 'pending'">待办理</option>
|
| 190 |
+
<option value="preparing" :selected="permit.status === 'preparing'">准备中</option>
|
| 191 |
+
<option value="submitted" :selected="permit.status === 'submitted'">已提交</option>
|
| 192 |
+
<option value="approved" :selected="permit.status === 'approved'">已获批</option>
|
| 193 |
+
</select>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
</li>
|
| 197 |
+
</ul>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<!-- Right: Assets & Chat -->
|
| 202 |
+
<div class="space-y-6">
|
| 203 |
+
<!-- Asset Vault -->
|
| 204 |
+
<div class="bg-white shadow rounded-lg overflow-hidden">
|
| 205 |
+
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
| 206 |
+
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
| 207 |
+
<i class="fas fa-folder text-yellow-500 mr-2"></i> 资产库
|
| 208 |
+
</h3>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="px-4 py-4 sm:px-6">
|
| 211 |
+
<ul class="space-y-3">
|
| 212 |
+
<li v-for="asset in activeProject.assets" :key="asset.id"
|
| 213 |
+
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer"
|
| 214 |
+
@click="viewAsset(asset)">
|
| 215 |
+
<div class="flex items-center">
|
| 216 |
+
<i class="fas fa-file-alt text-gray-400 mr-3"></i>
|
| 217 |
+
<span class="text-sm font-medium text-gray-700">${ asset.name }</span>
|
| 218 |
+
</div>
|
| 219 |
+
<i class="fas fa-chevron-right text-gray-400 text-xs"></i>
|
| 220 |
+
</li>
|
| 221 |
+
</ul>
|
| 222 |
+
<button @click="triggerUpload" class="mt-4 w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
| 223 |
+
<i class="fas fa-upload mr-2 text-gray-400"></i> 上传文件
|
| 224 |
+
</button>
|
| 225 |
+
<input type="file" ref="fileInput" @change="handleFileUpload" style="display: none" />
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
<!-- Quick Assistant -->
|
| 230 |
+
<div class="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg shadow-lg text-white p-6">
|
| 231 |
+
<h3 class="text-lg font-bold mb-2">AI 咨询助手</h3>
|
| 232 |
+
<p class="text-sm opacity-90 mb-4">对流程有疑问?我可以为您解读政策或生成文书。</p>
|
| 233 |
+
<button class="w-full bg-white text-indigo-600 py-2 px-4 rounded-md font-medium hover:bg-opacity-90 transition">
|
| 234 |
+
开始对话
|
| 235 |
+
</button>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
</main>
|
| 241 |
+
|
| 242 |
+
<!-- New Project Modal -->
|
| 243 |
+
<div v-if="showNewProjectModal" class="fixed z-20 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
| 244 |
+
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
| 245 |
+
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" @click="showNewProjectModal = false"></div>
|
| 246 |
+
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
| 247 |
+
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full">
|
| 248 |
+
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
| 249 |
+
<div class="sm:flex sm:items-start">
|
| 250 |
+
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
| 251 |
+
<i class="fas fa-magic text-primary"></i>
|
| 252 |
+
</div>
|
| 253 |
+
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
|
| 254 |
+
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">创建新合规项目</h3>
|
| 255 |
+
<div class="mt-4 space-y-4">
|
| 256 |
+
<div>
|
| 257 |
+
<label class="block text-sm font-medium text-gray-700">项目名称</label>
|
| 258 |
+
<input type="text" v-model="newProject.name" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm" placeholder="例如:成都高新区猫咖">
|
| 259 |
+
</div>
|
| 260 |
+
<div>
|
| 261 |
+
<label class="block text-sm font-medium text-gray-700">所在城市/区域</label>
|
| 262 |
+
<input type="text" v-model="newProject.location" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm" placeholder="例如:成都市高新区">
|
| 263 |
+
</div>
|
| 264 |
+
<div>
|
| 265 |
+
<label class="block text-sm font-medium text-gray-700">所属行业</label>
|
| 266 |
+
<select v-model="newProject.industry" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm">
|
| 267 |
+
<option value="餐饮/食品">餐饮/食品</option>
|
| 268 |
+
<option value="医疗/健康">医疗/健康</option>
|
| 269 |
+
<option value="教育/培训">教育/培训</option>
|
| 270 |
+
<option value="物流/运输">物流/运输</option>
|
| 271 |
+
<option value="互联网/科技">互联网/科技</option>
|
| 272 |
+
<option value="其他">其他</option>
|
| 273 |
+
</select>
|
| 274 |
+
</div>
|
| 275 |
+
<div>
|
| 276 |
+
<label class="block text-sm font-medium text-gray-700">详细描述</label>
|
| 277 |
+
<textarea v-model="newProject.description" rows="3" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-primary focus:border-primary sm:text-sm" placeholder="请详细描述您的业务内容、规��、特殊设备等..."></textarea>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</div>
|
| 283 |
+
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
| 284 |
+
<button type="button" @click="createProject" :disabled="creating" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary text-base font-medium text-white hover:bg-blue-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50">
|
| 285 |
+
<span v-if="creating"><i class="fas fa-spinner fa-spin mr-2"></i> 分析中...</span>
|
| 286 |
+
<span v-else>开始智能分析</span>
|
| 287 |
+
</button>
|
| 288 |
+
<button type="button" @click="showNewProjectModal = false" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
| 289 |
+
取消
|
| 290 |
+
</button>
|
| 291 |
+
</div>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
<!-- Asset View Modal -->
|
| 297 |
+
<div v-if="viewingAsset" class="fixed z-30 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
| 298 |
+
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
| 299 |
+
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true" @click="viewingAsset = null"></div>
|
| 300 |
+
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
| 301 |
+
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl w-full">
|
| 302 |
+
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
| 303 |
+
<div class="flex justify-between items-center mb-4">
|
| 304 |
+
<h3 class="text-xl font-bold text-gray-900">{{ viewingAsset.name }}</h3>
|
| 305 |
+
<div class="space-x-2">
|
| 306 |
+
<button @click="generateAssetContent" class="text-sm bg-indigo-100 text-indigo-700 px-3 py-1 rounded hover:bg-indigo-200">
|
| 307 |
+
<i class="fas fa-magic mr-1"></i> AI 生成/优化
|
| 308 |
+
</button>
|
| 309 |
+
<button @click="viewingAsset = null" class="text-gray-400 hover:text-gray-600">
|
| 310 |
+
<i class="fas fa-times"></i>
|
| 311 |
+
</button>
|
| 312 |
+
</div>
|
| 313 |
+
</div>
|
| 314 |
+
<div class="bg-gray-50 rounded-lg p-6 max-h-[60vh] overflow-y-auto">
|
| 315 |
+
<div v-if="assetLoading" class="flex justify-center py-10">
|
| 316 |
+
<i class="fas fa-spinner fa-spin text-3xl text-gray-400"></i>
|
| 317 |
+
</div>
|
| 318 |
+
<div v-else-if="viewingAsset.content" class="markdown-body" v-html="renderMarkdown(viewingAsset.content)"></div>
|
| 319 |
+
<div v-else class="text-center text-gray-500 py-10">
|
| 320 |
+
<p>暂无内容</p>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<script>
|
| 331 |
+
const { createApp, ref, onMounted } = Vue;
|
| 332 |
+
|
| 333 |
+
createApp({
|
| 334 |
+
setup() {
|
| 335 |
+
const currentView = ref('list');
|
| 336 |
+
const projects = ref([]);
|
| 337 |
+
const activeProject = ref(null);
|
| 338 |
+
const loading = ref(false);
|
| 339 |
+
const showNewProjectModal = ref(false);
|
| 340 |
+
const creating = ref(false);
|
| 341 |
+
const viewingAsset = ref(null);
|
| 342 |
+
const assetLoading = ref(false);
|
| 343 |
+
|
| 344 |
+
const newProject = ref({
|
| 345 |
+
name: '',
|
| 346 |
+
location: '',
|
| 347 |
+
industry: '餐饮/食品',
|
| 348 |
+
description: ''
|
| 349 |
+
});
|
| 350 |
+
|
| 351 |
+
const permitStatusMap = {
|
| 352 |
+
'pending': '待办理',
|
| 353 |
+
'preparing': '准备中',
|
| 354 |
+
'submitted': '已提交',
|
| 355 |
+
'approved': '已获批'
|
| 356 |
+
};
|
| 357 |
+
|
| 358 |
+
const fetchProjects = async () => {
|
| 359 |
+
loading.value = true;
|
| 360 |
+
try {
|
| 361 |
+
const res = await fetch('/api/projects');
|
| 362 |
+
projects.value = await res.json();
|
| 363 |
+
} catch (e) {
|
| 364 |
+
console.error(e);
|
| 365 |
+
} finally {
|
| 366 |
+
loading.value = false;
|
| 367 |
+
}
|
| 368 |
+
};
|
| 369 |
+
|
| 370 |
+
const createProject = async () => {
|
| 371 |
+
if (!newProject.value.name || !newProject.value.description) return;
|
| 372 |
+
creating.value = true;
|
| 373 |
+
try {
|
| 374 |
+
const res = await fetch('/api/projects', {
|
| 375 |
+
method: 'POST',
|
| 376 |
+
headers: { 'Content-Type': 'application/json' },
|
| 377 |
+
body: JSON.stringify(newProject.value)
|
| 378 |
+
});
|
| 379 |
+
const data = await res.json();
|
| 380 |
+
if (data.success) {
|
| 381 |
+
showNewProjectModal.value = false;
|
| 382 |
+
newProject.value = { name: '', location: '', industry: '餐饮/食品', description: '' };
|
| 383 |
+
await fetchProjects();
|
| 384 |
+
openProject(data.project_id);
|
| 385 |
+
}
|
| 386 |
+
} catch (e) {
|
| 387 |
+
console.error(e);
|
| 388 |
+
} finally {
|
| 389 |
+
creating.value = false;
|
| 390 |
+
}
|
| 391 |
+
};
|
| 392 |
+
|
| 393 |
+
const openProject = async (id) => {
|
| 394 |
+
loading.value = true;
|
| 395 |
+
try {
|
| 396 |
+
const res = await fetch(`/api/projects/${id}`);
|
| 397 |
+
activeProject.value = await res.json();
|
| 398 |
+
currentView.value = 'detail';
|
| 399 |
+
} catch (e) {
|
| 400 |
+
console.error(e);
|
| 401 |
+
} finally {
|
| 402 |
+
loading.value = false;
|
| 403 |
+
}
|
| 404 |
+
};
|
| 405 |
+
|
| 406 |
+
const updatePermitStatus = async (id, status) => {
|
| 407 |
+
try {
|
| 408 |
+
await fetch(`/api/permits/${id}/status`, {
|
| 409 |
+
method: 'POST',
|
| 410 |
+
headers: { 'Content-Type': 'application/json' },
|
| 411 |
+
body: JSON.stringify({ status })
|
| 412 |
+
});
|
| 413 |
+
// Update local state
|
| 414 |
+
const p = activeProject.value.permits.find(p => p.id === id);
|
| 415 |
+
if (p) p.status = status;
|
| 416 |
+
} catch (e) {
|
| 417 |
+
console.error(e);
|
| 418 |
+
}
|
| 419 |
+
};
|
| 420 |
+
|
| 421 |
+
const viewAsset = (asset) => {
|
| 422 |
+
viewingAsset.value = asset;
|
| 423 |
+
// If content is just a placeholder, maybe trigger generation?
|
| 424 |
+
// For now just show what we have.
|
| 425 |
+
};
|
| 426 |
+
|
| 427 |
+
const generateAssetContent = async () => {
|
| 428 |
+
if (!viewingAsset.value) return;
|
| 429 |
+
assetLoading.value = true;
|
| 430 |
+
try {
|
| 431 |
+
const res = await fetch('/api/assets/generate', {
|
| 432 |
+
method: 'POST',
|
| 433 |
+
headers: { 'Content-Type': 'application/json' },
|
| 434 |
+
body: JSON.stringify({
|
| 435 |
+
asset_id: viewingAsset.value.id,
|
| 436 |
+
context: JSON.stringify(activeProject.value.project)
|
| 437 |
+
})
|
| 438 |
+
});
|
| 439 |
+
const data = await res.json();
|
| 440 |
+
if (data.success) {
|
| 441 |
+
viewingAsset.value.content = data.content;
|
| 442 |
+
}
|
| 443 |
+
} catch (e) {
|
| 444 |
+
console.error(e);
|
| 445 |
+
} finally {
|
| 446 |
+
assetLoading.value = false;
|
| 447 |
+
}
|
| 448 |
+
};
|
| 449 |
+
|
| 450 |
+
const parseRequirements = (reqStr) => {
|
| 451 |
+
try {
|
| 452 |
+
return JSON.parse(reqStr);
|
| 453 |
+
} catch (e) {
|
| 454 |
+
return [];
|
| 455 |
+
}
|
| 456 |
+
};
|
| 457 |
+
|
| 458 |
+
const renderMarkdown = (content) => {
|
| 459 |
+
if (!content) return '';
|
| 460 |
+
return marked.parse(content);
|
| 461 |
+
};
|
| 462 |
+
|
| 463 |
+
const formatDate = (dateStr) => {
|
| 464 |
+
return new Date(dateStr).toLocaleDateString('zh-CN');
|
| 465 |
+
};
|
| 466 |
+
|
| 467 |
+
onMounted(() => {
|
| 468 |
+
fetchProjects();
|
| 469 |
+
});
|
| 470 |
+
|
| 471 |
+
return {
|
| 472 |
+
currentView,
|
| 473 |
+
projects,
|
| 474 |
+
activeProject,
|
| 475 |
+
loading,
|
| 476 |
+
showNewProjectModal,
|
| 477 |
+
newProject,
|
| 478 |
+
creating,
|
| 479 |
+
createProject,
|
| 480 |
+
openProject,
|
| 481 |
+
permitStatusMap,
|
| 482 |
+
updatePermitStatus,
|
| 483 |
+
parseRequirements,
|
| 484 |
+
viewingAsset,
|
| 485 |
+
viewAsset,
|
| 486 |
+
renderMarkdown,
|
| 487 |
+
formatDate,
|
| 488 |
+
generateAssetContent,
|
| 489 |
+
assetLoading
|
| 490 |
+
};
|
| 491 |
+
}
|
| 492 |
+
}).mount('#app');
|
| 493 |
+
</script>
|
| 494 |
+
</body>
|
| 495 |
+
</html>
|