fix+feat: 修复工作台接口不存在 + 3D 区域分配制重建
- 补齐公告后端: Announcement 模型 + /api/announcements + 种子公告 (修复工作台/布局页调用不存在接口报'接口不存在') - 修复 seed init_db 前置模型导入(announcements 表未建) - WindoorThreed 区域分配制: 每个重放区域唯一归属 (扇认领重叠最大区域, 其余静态玻璃), 杜绝扇面板与玻璃重叠混乱 - 全区域薄框线还原真实窗格, 扇面板含玻璃+四边扇框
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
- [2026-09-21 11:20] FIX+FEAT: v1.8 修复工作台"接口不存在"+3D 区域分配制重建。①补齐公告后端: Announcement 模型+/api/announcements 接口+种子 4 条自撰公告(修复工作台/布局页调用不存在的接口报"接口不存在"); 修复 seed init_db 前置模型导入(announcements 表未建); ②WindoorThreed 重建改为区域分配制: 每个重放区域唯一归属(扇认领重叠最大区域/其余静态玻璃), 杜绝扇面板与玻璃重叠混乱; 全区域绘制薄框线还原真实窗格; 扇面板含玻璃+四边扇框 (Files: src/huamenchuang/models/announcement.py, src/huamenchuang/api/platform.py, src/huamenchuang/scripts/seed.py, frontend/src/components/WindoorThreed.vue)
|
||||||
- [2026-09-21 11:00] FIX: v1.7 修复 3D NaN 崩溃/样品图不刷新/画布点选失效——①THREE NaN: drawing_data 畸形数据进入 3D 网格, replayShape 段过滤补 Y 轴有限值校验、inferSashGlyphs bdRect 有限值校验、WindoorThreed 逐网格有限值防护+maxDim 守卫、CombineCanvas shapePts 过滤; ②样品图不刷新: 组合画布原地变异 shapes 数组, combinedJson computed 感知不到深层变化 → 引入 rev 版本号强制重算驱动 3D 重建; ③点选失效: 单元包围盒被非有限值污染致命中测试失效, 随①修复 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorThreed.vue, frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue)
|
- [2026-09-21 11:00] FIX: v1.7 修复 3D NaN 崩溃/样品图不刷新/画布点选失效——①THREE NaN: drawing_data 畸形数据进入 3D 网格, replayShape 段过滤补 Y 轴有限值校验、inferSashGlyphs bdRect 有限值校验、WindoorThreed 逐网格有限值防护+maxDim 守卫、CombineCanvas shapePts 过滤; ②样品图不刷新: 组合画布原地变异 shapes 数组, combinedJson computed 感知不到深层变化 → 引入 rev 版本号强制重算驱动 3D 重建; ③点选失效: 单元包围盒被非有限值污染致命中测试失效, 随①修复 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorThreed.vue, frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue)
|
||||||
- [2026-09-21 10:50] FIX: 修复 spa_fallback 的 NameError(JSONResponse 未导入导致未匹配 /api 路径 500)与 JWT 密钥长度警告(默认密钥补足 32 字节以上) (Files: src/huamenchuang/main.py, src/huamenchuang/core/config.py)
|
- [2026-09-21 10:50] FIX: 修复 spa_fallback 的 NameError(JSONResponse 未导入导致未匹配 /api 路径 500)与 JWT 密钥长度警告(默认密钥补足 32 字节以上) (Files: src/huamenchuang/main.py, src/huamenchuang/core/config.py)
|
||||||
- [2026-09-21 10:55] FEAT: v1.6 组合画布编辑能力增强——①选中单元可视化: 蓝色虚线框+四角手柄+实时尺寸标注(W×H mm); ②精确尺寸编辑: 工具栏 W/H 输入框(选中单元几何按比例缩放, 锚定左下角, 中梃点位同步); ③方向键微调: 1mm/Shift 10mm; ④右侧单元清单点击选中联动画布; selSize 双向同步(画布拖拽/输入框改动均实时刷新) (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue)
|
- [2026-09-21 10:55] FEAT: v1.6 组合画布编辑能力增强——①选中单元可视化: 蓝色虚线框+四角手柄+实时尺寸标注(W×H mm); ②精确尺寸编辑: 工具栏 W/H 输入框(选中单元几何按比例缩放, 锚定左下角, 中梃点位同步); ③方向键微调: 1mm/Shift 10mm; ④右侧单元清单点击选中联动画布; selSize 双向同步(画布拖拽/输入框改动均实时刷新) (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue)
|
||||||
|
|||||||
@@ -17,17 +17,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 3D 窗型查看器 + 门窗开合动画(自研 Three.js 实现)
|
* 3D 窗型查看器 + 门窗开合动画(自研 Three.js 实现)
|
||||||
* - 玻璃区域/中梃/外框静态重建(分割树重放)
|
* 构建原则(区域分配制): 分割树重放后的每个区域唯一归属——
|
||||||
* - 扇面板独立成组: 平开扇绕铰链旋转(门开角更大), 推拉扇沿轨道平移, 双向门两向开
|
* 被扇引用的区域 → 生成可开合扇面板(平开绕铰链旋转/推拉沿轨道平移)
|
||||||
* - 开合度滑杆 / 自动演示; 组合画布的多形状直接支持
|
* 其余区域 → 静态玻璃
|
||||||
|
* 全部区域绘制薄框线还原真实窗格;组合画布的多形状直接支持。
|
||||||
*/
|
*/
|
||||||
import { onMounted, onBeforeUnmount, ref, watch, computed } from 'vue'
|
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import {
|
import {
|
||||||
bboxOf, inferSashGlyphs, parseDrawing, regionPoints, replayShape,
|
bboxOf, inferSashGlyphs, parseDrawing, regionPoints, replayShape,
|
||||||
type DrawingData, type Pt,
|
type DrawingData, type Pt,
|
||||||
} from '../types/drawing'
|
} from '../types/drawing'
|
||||||
|
|
||||||
|
interface BBox { x0: number; y0: number; x1: number; y1: number }
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ drawingData: string | object; controls?: boolean }>(), { controls: true })
|
const props = withDefaults(defineProps<{ drawingData: string | object; controls?: boolean }>(), { controls: true })
|
||||||
const cvRef = ref<HTMLCanvasElement>()
|
const cvRef = ref<HTMLCanvasElement>()
|
||||||
const wrapRef = ref<HTMLDivElement>()
|
const wrapRef = ref<HTMLDivElement>()
|
||||||
@@ -66,6 +69,12 @@ function dispose() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function overlapArea(a: BBox, b: BBox): number {
|
||||||
|
const w = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0)
|
||||||
|
const h = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0)
|
||||||
|
return w > 0 && h > 0 ? w * h : 0
|
||||||
|
}
|
||||||
|
|
||||||
function build(drawing: DrawingData) {
|
function build(drawing: DrawingData) {
|
||||||
dispose()
|
dispose()
|
||||||
scene = new THREE.Scene()
|
scene = new THREE.Scene()
|
||||||
@@ -95,7 +104,8 @@ function build(drawing: DrawingData) {
|
|||||||
const DEPTH = Math.max(0.04, (drawing.s?.cs ?? 60) * S * 1.2)
|
const DEPTH = Math.max(0.04, (drawing.s?.cs ?? 60) * S * 1.2)
|
||||||
|
|
||||||
const frameMat = new THREE.MeshStandardMaterial({ color: 0x636568, roughness: 0.55, metalness: 0.35 })
|
const frameMat = new THREE.MeshStandardMaterial({ color: 0x636568, roughness: 0.55, metalness: 0.35 })
|
||||||
const sashMat = new THREE.MeshStandardMaterial({ color: 0x777c82, roughness: 0.5, metalness: 0.35 })
|
const mullionMat = new THREE.MeshStandardMaterial({ color: 0x777c82, roughness: 0.5, metalness: 0.35 })
|
||||||
|
const sashMat = new THREE.MeshStandardMaterial({ color: 0x83888f, roughness: 0.5, metalness: 0.35 })
|
||||||
const glassMat = new THREE.MeshPhysicalMaterial({
|
const glassMat = new THREE.MeshPhysicalMaterial({
|
||||||
color: 0xdfeef5, transparent: true, opacity: 0.42, roughness: 0.08,
|
color: 0xdfeef5, transparent: true, opacity: 0.42, roughness: 0.08,
|
||||||
metalness: 0.05, side: THREE.DoubleSide,
|
metalness: 0.05, side: THREE.DoubleSide,
|
||||||
@@ -110,6 +120,13 @@ function build(drawing: DrawingData) {
|
|||||||
const ew = shape.fm?.ew ?? [60, 60, 60, 60]
|
const ew = shape.fm?.ew ?? [60, 60, 60, 60]
|
||||||
return Math.max(0.02, (ew.reduce((a: number, b: number) => a + b, 0) / ew.length) * S)
|
return Math.max(0.02, (ew.reduce((a: number, b: number) => a + b, 0) / ew.length) * S)
|
||||||
}
|
}
|
||||||
|
const bar = (parent: THREE.Group, mat: THREE.Material, bw: number, bh: number,
|
||||||
|
ox: number, oy: number, oz: number, depth: number) => {
|
||||||
|
const m = new THREE.Mesh(new THREE.BoxGeometry(bw, bh, depth), mat)
|
||||||
|
m.position.set(ox, oy, oz)
|
||||||
|
parent.add(m)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
for (const shape of drawing.sm) {
|
for (const shape of drawing.sm) {
|
||||||
const { regions, sashes } = inferSashGlyphs(shape)
|
const { regions, sashes } = inferSashGlyphs(shape)
|
||||||
@@ -126,87 +143,89 @@ function build(drawing: DrawingData) {
|
|||||||
: glassMat
|
: glassMat
|
||||||
const fw = fwOf(shape)
|
const fw = fwOf(shape)
|
||||||
|
|
||||||
// 判断每个区域是否属于某扇(区域中心在扇矩形内 → 由扇面板渲染)
|
const regionBBs: BBox[] = regions.map(r => bboxOf(regionPoints(r)))
|
||||||
const sashRects = sashes.map(s => ({ ...s, cx: (s.x0 + s.x1) / 2, cy: (s.y0 + s.y1) / 2 }))
|
const regionMids = regionBBs.map(b => ({ x: (b.x0 + b.x1) / 2, y: (b.y0 + b.y1) / 2 }))
|
||||||
|
|
||||||
for (const region of regions) {
|
// ---- 区域分配: 每个扇认领重叠最大的未占用区域 ----
|
||||||
const rpts = regionPoints(region)
|
const used = new Set<number>()
|
||||||
if (rpts.length < 3) continue
|
const panels: { rect: BBox; sash: (typeof sashes)[number] }[] = []
|
||||||
const bb = bboxOf(rpts)
|
for (const s of sashes) {
|
||||||
const w = (bb.x1 - bb.x0) * S, h = (bb.y1 - bb.y0) * S
|
const sb: BBox = { x0: s.x0, y0: s.y0, x1: s.x1, y1: s.y1 }
|
||||||
const mx = ((bb.x0 + bb.x1) / 2 - cx) * S
|
let best = -1, bestA = 0
|
||||||
const my = ((bb.y0 + bb.y1) / 2 - cy) * S
|
regionBBs.forEach((rb, ri) => {
|
||||||
if (![w, h, mx, my].every(Number.isFinite) || w <= 0 || h <= 0) continue
|
if (used.has(ri)) return
|
||||||
const inSash = sashRects.some(r => {
|
const a = overlapArea(sb, rb)
|
||||||
const cxr = (r.x0 + r.x1) / 2, cyr = (r.y0 + r.y1) / 2
|
if (a > bestA) { bestA = a; best = ri }
|
||||||
return Math.abs(cxr - (bb.x0 + bb.x1) / 2) < w / 2 && Math.abs(cyr - (bb.y0 + bb.y1) / 2) < h / 2
|
|
||||||
})
|
})
|
||||||
if (inSash) continue
|
if (best >= 0 && bestA > 0) { used.add(best); panels.push({ rect: regionBBs[best], sash: s }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 静态玻璃(未被扇认领的区域)----
|
||||||
|
regionBBs.forEach((bb, ri) => {
|
||||||
|
if (used.has(ri)) return
|
||||||
|
const w = (bb.x1 - bb.x0) * S, h = (bb.y1 - bb.y0) * S
|
||||||
|
const mx = (regionMids[ri].x - cx) * S, my = (regionMids[ri].y - cy) * S
|
||||||
|
if (![w, h, mx, my].every(Number.isFinite) || w <= 0 || h <= 0) return
|
||||||
const glass = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.008), gm)
|
const glass = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.008), gm)
|
||||||
glass.position.set(mx, my, 0)
|
glass.position.set(mx, my, 0)
|
||||||
frameGroup.add(glass)
|
frameGroup.add(glass)
|
||||||
}
|
// 区域薄框线(窗格还原)
|
||||||
|
const t = Math.max(0.008, fw * 0.35)
|
||||||
|
bar(frameGroup, mullionMat, w, t, mx, my + h / 2 - t / 2, 0, DEPTH * 0.5)
|
||||||
|
bar(frameGroup, mullionMat, w, t, mx, my - h / 2 + t / 2, 0, DEPTH * 0.5)
|
||||||
|
bar(frameGroup, mullionMat, t, h, mx - w / 2 + t / 2, my, 0, DEPTH * 0.5)
|
||||||
|
bar(frameGroup, mullionMat, t, h, mx + w / 2 - t / 2, my, 0, DEPTH * 0.5)
|
||||||
|
})
|
||||||
|
|
||||||
// 扇面板(可开合):玻璃 + 四周扇框,装入铰链/滑动组
|
// ---- 扇面板(可开合)----
|
||||||
sashes.forEach((s, k) => {
|
panels.forEach(({ rect, s }, k) => {
|
||||||
const w = (s.x1 - s.x0) * S, h = (s.y1 - s.y0) * S
|
const w = (rect.x1 - rect.x0) * S, h = (rect.y1 - rect.y0) * S
|
||||||
const cxr = ((s.x0 + s.x1) / 2 - cx) * S
|
const cxr = ((rect.x0 + rect.x1) / 2 - cx) * S
|
||||||
const cyr = ((s.y0 + s.y1) / 2 - cy) * S
|
const cyr = ((rect.y0 + rect.y1) / 2 - cy) * S
|
||||||
if (![w, h, cxr, cyr].every(Number.isFinite) || w < 0.02 || h < 0.02) return
|
if (![w, h, cxr, cyr].every(Number.isFinite) || w < 0.02 || h < 0.02) return
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group()
|
||||||
const panel = new THREE.Group()
|
|
||||||
const glass = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.006), gm)
|
const glass = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.006), gm)
|
||||||
panel.add(glass)
|
group.add(glass)
|
||||||
const t = Math.max(0.018, fw)
|
const t = Math.max(0.016, fw)
|
||||||
const mkBar = (bw: number, bh: number, ox: number, oy: number) => {
|
bar(group, sashMat, w, t, 0, h / 2 - t / 2, 0, DEPTH * 0.7)
|
||||||
const bar = new THREE.Mesh(new THREE.BoxGeometry(bw, bh, DEPTH * 0.8), sashMat)
|
bar(group, sashMat, w, t, 0, -h / 2 + t / 2, 0, DEPTH * 0.7)
|
||||||
bar.position.set(ox, oy, 0)
|
bar(group, sashMat, t, h, -w / 2 + t / 2, 0, DEPTH * 0.7)
|
||||||
panel.add(bar)
|
bar(group, sashMat, t, h, w / 2 - t / 2, 0, DEPTH * 0.7)
|
||||||
}
|
group.position.set(cxr, cyr, 0)
|
||||||
mkBar(w, t, 0, h / 2 - t / 2)
|
|
||||||
mkBar(w, t, 0, -h / 2 + t / 2)
|
|
||||||
mkBar(t, h, -w / 2 + t / 2, 0)
|
|
||||||
mkBar(t, h, w / 2 - t / 2, 0)
|
|
||||||
group.add(panel)
|
|
||||||
sashRoot.add(group)
|
sashRoot.add(group)
|
||||||
|
|
||||||
if (s.kind === 'sliding') {
|
if (s.kind === 'sliding') {
|
||||||
// 推拉: 双扇前后轨交替, 沿 X 平移
|
group.position.z = k % 2 === 0 ? 0.012 : -0.012
|
||||||
group.position.set(cxr, cyr, k % 2 === 0 ? 0.012 : -0.012)
|
sashAnims.push({ group, kind: 'sliding', baseX: cxr, slideDir: k % 2 === 0 ? 1 : -1,
|
||||||
sashAnims.push({ group, kind: 'sliding', baseX: cxr, slideDir: k % 2 === 0 ? 1 : -1, hingeSign: 1, maxAng: 0, w })
|
hingeSign: 1, maxAng: 0, w })
|
||||||
} else {
|
} else {
|
||||||
// 平开/门: 铰链在 od 指向的对侧(箭头指向开启方向), 绕 Y 轴旋转
|
|
||||||
const odRad = ((s.od ?? 90) * Math.PI) / 180
|
const odRad = ((s.od ?? 90) * Math.PI) / 180
|
||||||
const hingeLeft = Math.cos(odRad) >= 0 ? false : true // 箭头偏右→铰链在左
|
const hingeLeft = Math.cos(odRad) < 0
|
||||||
const hingeX = hingeLeft ? cxr - w / 2 : cxr + w / 2
|
const hingeX = hingeLeft ? cxr - w / 2 : cxr + w / 2
|
||||||
group.position.set(hingeX, cyr, 0)
|
group.position.set(hingeX, cyr, 0)
|
||||||
panel.position.set(cxr - hingeX, 0, 0)
|
group.children.forEach(child => { child.position.x -= (cxr - hingeX) })
|
||||||
const maxAng = (s.isDoor ? 75 : s.wtf ? 40 : 32) * Math.PI / 180
|
const maxAng = (s.isDoor ? 75 : s.wtf ? 40 : 32) * Math.PI / 180
|
||||||
const dirSign = s.openToward === 'inward' ? 1 : -1
|
const dirSign = s.openToward === 'inward' ? 1 : -1
|
||||||
sashAnims.push({ group, kind: 'casement', baseX: hingeX, slideDir: 0, hingeSign: dirSign, maxAng, w })
|
sashAnims.push({ group, kind: 'casement', baseX: hingeX, slideDir: 0,
|
||||||
|
hingeSign: dirSign, maxAng, w })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 静态外框(整体)
|
// ---- 整体外框 ----
|
||||||
if (regions.length) {
|
const all = regions.map(r => regionPoints(r)).flat()
|
||||||
const all = regions.map(r => regionPoints(r).length ? r : r).flat()
|
if (all.length) {
|
||||||
const bb = bboxOf(all.flatMap(seg => [seg.ps, seg.pe]))
|
const bb = bboxOf(all)
|
||||||
const x0 = ((bb.x0) - cx) * S, x1 = ((bb.x1) - cx) * S
|
const x0 = (bb.x0 - cx) * S, x1 = (bb.x1 - cx) * S
|
||||||
const y0 = ((bb.y0) - cy) * S, y1 = ((bb.y1) - cy) * S
|
const y0 = (bb.y0 - cy) * S, y1 = (bb.y1 - cy) * S
|
||||||
const w = x1 - x0, h = y1 - y0
|
const w = x1 - x0, h = y1 - y0
|
||||||
const mk = (bw: number, bh: number, ox: number, oy: number) => {
|
bar(frameGroup, frameMat, w + fw * 2, fw, 0, y1 + fw / 2, 0, DEPTH)
|
||||||
const bar = new THREE.Mesh(new THREE.BoxGeometry(bw, bh, DEPTH), frameMat)
|
bar(frameGroup, frameMat, w + fw * 2, fw, 0, y0 - fw / 2, 0, DEPTH)
|
||||||
bar.position.set((x0 + x1) / 2 + ox, (y0 + y1) / 2 + oy, 0)
|
bar(frameGroup, frameMat, fw, h, x0 - fw / 2, 0, DEPTH)
|
||||||
frameGroup.add(bar)
|
bar(frameGroup, frameMat, fw, h, x1 + fw / 2, 0, DEPTH)
|
||||||
}
|
|
||||||
mk(w + fw * 2, fw, 0, y1 + fw / 2)
|
|
||||||
mk(w + fw * 2, fw, 0, y0 - fw / 2)
|
|
||||||
mk(fw, h, x0 - fw / 2, 0)
|
|
||||||
mk(fw, h, x1 + fw / 2, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasOpenable.value = sashAnims.some(a => a.kind === 'sliding' || a.maxAng > 0)
|
hasOpenable.value = sashAnims.length > 0
|
||||||
const grid = new THREE.GridHelper(2, 10, 0xc8cdd2, 0xdfe3e8)
|
const grid = new THREE.GridHelper(2, 10, 0xc8cdd2, 0xdfe3e8)
|
||||||
grid.position.y = -gh * S / 2 - DEPTH
|
grid.position.y = -gh * S / 2 - DEPTH
|
||||||
scene.add(grid)
|
scene.add(grid)
|
||||||
@@ -233,7 +252,6 @@ function applyOpen(t: number) {
|
|||||||
function loop() {
|
function loop() {
|
||||||
raf = requestAnimationFrame(loop)
|
raf = requestAnimationFrame(loop)
|
||||||
if (!renderer || !scene || !camera) return
|
if (!renderer || !scene || !camera) return
|
||||||
// 自动演示: 开合度按正弦摆动
|
|
||||||
const target = (autoPlay.value ? 50 + 50 * Math.sin(Date.now() / 1200) : openPct.value) / 100
|
const target = (autoPlay.value ? 50 + 50 * Math.sin(Date.now() / 1200) : openPct.value) / 100
|
||||||
applyOpen(curT + (target - curT) * 0.12)
|
applyOpen(curT + (target - curT) * 0.12)
|
||||||
camera.position.set(
|
camera.position.set(
|
||||||
|
|||||||
@@ -210,3 +210,17 @@ async def list_articles(db: DbSession, category: str | None = None):
|
|||||||
items = list((await db.execute(stmt)).scalars())
|
items = list((await db.execute(stmt)).scalars())
|
||||||
return ok(data=[{"id": a.id, "title": a.title, "category": a.category, "content": a.content}
|
return ok(data=[{"id": a.id, "title": a.title, "category": a.category, "content": a.content}
|
||||||
for a in items])
|
for a in items])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 平台公告 ----------------
|
||||||
|
|
||||||
|
@router.get("/announcements")
|
||||||
|
async def list_announcements(db: DbSession):
|
||||||
|
"""平台公告(工作台轮播 + 顶栏气泡)。"""
|
||||||
|
from huamenchuang.models.announcement import Announcement
|
||||||
|
stmt = select(Announcement).order_by(Announcement.sort, Announcement.id.desc())
|
||||||
|
items = list((await db.execute(stmt)).scalars())
|
||||||
|
return ok(data=[{
|
||||||
|
"id": a.id, "name": a.name, "desc": a.desc, "content": a.content,
|
||||||
|
"pic_url": a.pic_url, "created_at": str(a.created_at),
|
||||||
|
} for a in items])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""ORM 模型统一导出。"""
|
"""ORM 模型统一导出。"""
|
||||||
|
from huamenchuang.models.announcement import Announcement
|
||||||
from huamenchuang.models.business import (
|
from huamenchuang.models.business import (
|
||||||
AfterSale,
|
AfterSale,
|
||||||
MeasureOrder,
|
MeasureOrder,
|
||||||
@@ -19,6 +20,7 @@ from huamenchuang.models.windoor_template import WindoorCatelog, WindoorTemplate
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AfterSale",
|
"AfterSale",
|
||||||
|
"Announcement",
|
||||||
"Contract",
|
"Contract",
|
||||||
"Customer",
|
"Customer",
|
||||||
"DEFAULT_PROCESSES",
|
"DEFAULT_PROCESSES",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"""平台公告模型。"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from huamenchuang.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Announcement(Base):
|
||||||
|
__tablename__ = "announcements"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(128))
|
||||||
|
desc: Mapped[str] = mapped_column(String(255), default="")
|
||||||
|
content: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
pic_url: Mapped[str] = mapped_column(String(255), default="")
|
||||||
|
sort: Mapped[int] = mapped_column(default=0)
|
||||||
|
created_at: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime.utcnow)
|
||||||
@@ -11,6 +11,7 @@ DATA = Path(__file__).resolve().parents[3] / "data"
|
|||||||
|
|
||||||
|
|
||||||
async def seed() -> None:
|
async def seed() -> None:
|
||||||
|
import huamenchuang.models # noqa: F401 确保全部模型注册到 metadata 后再建表
|
||||||
from huamenchuang.core.database import SessionLocal, init_db
|
from huamenchuang.core.database import SessionLocal, init_db
|
||||||
from huamenchuang.models.user import User
|
from huamenchuang.models.user import User
|
||||||
from huamenchuang.models.windoor_template import WindoorCatelog, WindoorTemplate
|
from huamenchuang.models.windoor_template import WindoorCatelog, WindoorTemplate
|
||||||
@@ -82,6 +83,22 @@ async def seed() -> None:
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
print("帮助文章就绪")
|
print("帮助文章就绪")
|
||||||
|
|
||||||
|
# 5) 平台公告(幂等:按名称去重)
|
||||||
|
async with SessionLocal() as db:
|
||||||
|
from huamenchuang.models.announcement import Announcement
|
||||||
|
ANNOUNCEMENTS = [
|
||||||
|
("AI 炫图功能上线", "选择样板一键生成多风格效果图,支持日景/黄昏/夜景与四种装修风格", 1),
|
||||||
|
("样板组合工场发布", "拖拽组合多个窗型样板,自由摆放、等高对齐、边缘吸附,支持 3D 预览", 2),
|
||||||
|
("3D 门窗开合演示", "3D 展示支持平开扇旋转、推拉扇滑动动画,可调节开合度", 3),
|
||||||
|
("优化下料引擎", "合同级汇总算料需求,一维优化切割提升型材利用率", 4),
|
||||||
|
]
|
||||||
|
existing = {n for (n,) in (await db.execute(select(Announcement.name))).all()}
|
||||||
|
for name, desc, sort in ANNOUNCEMENTS:
|
||||||
|
if name not in existing:
|
||||||
|
db.add(Announcement(name=name, desc=desc, sort=sort))
|
||||||
|
await db.commit()
|
||||||
|
print("平台公告就绪")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(seed())
|
asyncio.run(seed())
|
||||||
|
|||||||
Reference in New Issue
Block a user