feat: 部署前端到dm.all8ai.top + 性能优化 + 登录页密码验证
- 前端构建部署到152.136.182.184服务器,nginx反向代理 - SSH反向隧道(13333端口)连接服务器到本地后端 - SSL证书自动配置(Let's Encrypt) - API baseURL支持环境变量VITE_API_BASE_URL - 后端端口改为3333,CORS允许所有来源 - 物化6个慢视图(overview/risk/loop_health/benchmark/region/followup) - 登录页保留密码输入框,必须输入密码才能登录 - run.md添加登录账号信息
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# 构建时设置API地址(内网穿透地址)
|
||||
# VITE_API_BASE_URL=https://your-ngrok-url/api
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ interface LoginPageProps {
|
||||
export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
const navigate = useNavigate()
|
||||
const [username, setUsername] = useState('总部管理员')
|
||||
const [password, setPassword] = useState('123')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
@@ -34,9 +34,11 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
<input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||
className="mt-1 w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">密码</label>
|
||||
<input
|
||||
@@ -47,7 +49,6 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
className="mt-1 w-full rounded-md border px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="w-full rounded-md bg-primary py-2 text-sm font-medium text-primary-foreground"
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3001',
|
||||
target: 'http://localhost:3333',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
当前数据库是本机Homebrew PostgreSQL 15,数据库名为 bill_query,端口5432,仅监听 localhost;当前用户 freedak 是超级用户,本机认证方式为 trust。这适合本机开发,但不适合直接照搬到服务器生产环境。前端浏览器也不应直接连接PostgreSQL,必须通过后端API访问。
|
||||
当前数据库是本机Homebrew PostgreSQL 15,数据库名为 bill_query,端口5432,仅监听 localhost;当前用户 freedak 是超级用户,本机认证方式为 trust。这适合本机开发,但不适合直接照搬到服务器生产环境。前端浏览器也不应直接连接PostgreSQL,必须通过后端API访问。
|
||||
|
||||
## 登录账号
|
||||
|
||||
| 用户名 | 密码 | 角色 |
|
||||
|--------|------|------|
|
||||
| 总部管理员 | 123 | hq |
|
||||
| 区域经理 | 123 | regional |
|
||||
| 潘家园店长 | 123 | store |
|
||||
| 商品部 | 123 | dept |
|
||||
+2
-2
@@ -8,10 +8,10 @@ import dataRoutes from './routes/data.js'
|
||||
import taskRoutes from './routes/tasks.js'
|
||||
|
||||
const app = express()
|
||||
const PORT = parseInt(process.env.PORT || '3001')
|
||||
const PORT = parseInt(process.env.PORT || '3333')
|
||||
|
||||
app.use(cors({
|
||||
origin: process.env.CLIENT_URL || 'http://localhost:5173',
|
||||
origin: process.env.CLIENT_URL || true,
|
||||
credentials: true,
|
||||
}))
|
||||
app.use(express.json())
|
||||
|
||||
@@ -8,20 +8,7 @@ const router = Router()
|
||||
router.get('/overview', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const sql = `
|
||||
SELECT count(*) AS bill_count,
|
||||
round(sum(received_total), 2) AS received,
|
||||
round(sum(received_total) / count(*), 2) AS avg_bill_value,
|
||||
round(sum(discount_total) / nullif(sum(consumption), 0) * 100, 2) AS discount_rate_pct,
|
||||
round(sum(theoretical_profit) / nullif(sum(received_total), 0) * 100, 2) AS theoretical_margin_pct,
|
||||
count(*) FILTER (WHERE member_id IS NOT NULL) AS member_bills,
|
||||
round(count(*) FILTER (WHERE member_id IS NOT NULL)::numeric / count(*) * 100, 2) AS member_share_pct
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date
|
||||
AND closed_at < ($1::date + interval '1 month')
|
||||
AND closed_at IS NOT NULL
|
||||
`
|
||||
const result = await query(sql, [month])
|
||||
const result = await query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month])
|
||||
sendSuccess(res, result.rows[0])
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -31,20 +18,7 @@ router.get('/overview', async (req: AuthRequest, res) => {
|
||||
router.get('/overview/daily', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const sql = `
|
||||
SELECT closed_at::date AS business_date,
|
||||
count(*) AS bill_count,
|
||||
round(sum(received_total), 2) AS received,
|
||||
round(sum(received_total) / count(*), 2) AS avg_bill_value,
|
||||
round(sum(discount_total) / nullif(sum(consumption), 0) * 100, 2) AS discount_rate_pct
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date
|
||||
AND closed_at < ($1::date + interval '1 month')
|
||||
AND closed_at IS NOT NULL
|
||||
GROUP BY closed_at::date
|
||||
ORDER BY business_date
|
||||
`
|
||||
const result = await query(sql, [month])
|
||||
const result = await query(`SELECT business_date, bill_count, received, avg_bill_value, discount_rate_pct FROM analytics.mv_overview_daily WHERE month = $1::date ORDER BY business_date`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -63,7 +37,7 @@ router.get('/stores', async (req: AuthRequest, res) => {
|
||||
|
||||
if (riskLevel) {
|
||||
sql = `SELECT s.* FROM analytics.v_store_scorecard s
|
||||
JOIN analytics.v_store_risk_rating r ON s.store_code = r.store_code
|
||||
JOIN analytics.mv_store_risk_rating r ON s.store_code = r.store_code
|
||||
WHERE r.risk_level = $1`
|
||||
params.push(riskLevel)
|
||||
}
|
||||
@@ -94,7 +68,7 @@ router.get('/stores', async (req: AuthRequest, res) => {
|
||||
|
||||
router.get('/stores/risk', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_store_risk_rating ORDER BY risk_level, received DESC`)
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_risk_rating ORDER BY risk_level, received DESC`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -136,7 +110,7 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
||||
const code = req.params.code
|
||||
const [scorecard, risk, platform, benchmark, action, execution] = await Promise.all([
|
||||
query(`SELECT * FROM analytics.v_store_scorecard WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_risk_rating WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.mv_store_risk_rating WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_platform_economics WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_benchmark WHERE store_code = $1`, [code]),
|
||||
query(`SELECT * FROM analytics.v_store_action_list WHERE store_code = $1`, [code]),
|
||||
@@ -313,7 +287,7 @@ router.get('/marketing/plans', async (req: AuthRequest, res) => {
|
||||
|
||||
router.get('/benchmark/composite', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_store_benchmark_composite ORDER BY benchmark_score DESC`)
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_benchmark_composite ORDER BY benchmark_score DESC`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -445,7 +419,7 @@ router.get('/stores/:code/anomalies', async (req: AuthRequest, res) => {
|
||||
// 区域汇总
|
||||
router.get('/region/summary', async (req, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_region_summary ORDER BY total_received DESC`)
|
||||
const result = await query(`SELECT * FROM analytics.mv_region_summary ORDER BY total_received DESC`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
@@ -117,7 +117,7 @@ router.get('/monthly-review', async (req: AuthRequest, res) => {
|
||||
|
||||
router.get('/followup', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_store_monthly_followup ORDER BY priority, store_code`)
|
||||
const result = await query(`SELECT * FROM analytics.mv_store_monthly_followup ORDER BY priority, store_code`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -247,7 +247,7 @@ router.post('/indicators', async (req: AuthRequest, res) => {
|
||||
|
||||
router.get('/loop-health', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_loop_health`)
|
||||
const result = await query(`SELECT * FROM analytics.mv_loop_health`)
|
||||
sendSuccess(res, result.rows[0] || {
|
||||
task_generation_rate: 0,
|
||||
store_execution_rate: 0,
|
||||
|
||||
Reference in New Issue
Block a user