Files
cms-SiteForge/integrations/art-design-pro/src/views/siteforge/WebsiteFactory.vue
T
浅念 601451f22b upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:31:03 +08:00

83 lines
9.3 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="sf-art-page">
<div class="sf-art-intro">
<div><div class="sf-overline">SITEFORGE / WEBSITE FACTORY</div><h1>网站生产工厂</h1><p>多站点管理 · 旧站迁移 · 主题装配 · 可视化页面设计 · 静态发布</p></div>
<el-button type="primary" size="large" @click="createVisible=true"><el-icon><Plus /></el-icon> 创建新网站</el-button>
</div>
<el-row :gutter="18" class="metric-line">
<el-col :span="6"><el-card shadow="never"><div class="metric-label">网站数量</div><strong>{{ sites.length }}</strong></el-card></el-col>
<el-col :span="6"><el-card shadow="never"><div class="metric-label">站点内容</div><strong>{{ sites.reduce((sum, s)=>sum+s.entries_count,0) }}</strong></el-card></el-col>
<el-col :span="6"><el-card shadow="never"><div class="metric-label">构建版本</div><strong>{{ sites.reduce((sum, s)=>sum+s.build_count,0) }}</strong></el-card></el-col>
<el-col :span="6"><el-card shadow="never"><div class="metric-label">行业主题</div><strong>{{ themes.length }}</strong></el-card></el-col>
</el-row>
<el-card shadow="never" class="listing-card">
<template #header><div class="table-head"><div><h3>我的网站</h3><p>可选择站点继续编辑、批量套版或进行静态构建</p></div><el-input v-model="filter" clearable placeholder="搜索网站名称" style="width:240px" /></div></template>
<el-table :data="visibleSites" v-loading="loading" size="large" stripe>
<el-table-column label="网站项目" min-width="220"><template #default="{ row }"><div class="site-name"><span class="site-mark">{{ row.name[0] }}</span><div><b>{{ row.name }}</b><small>{{ row.domain || '尚未绑定域名' }}</small></div></div></template></el-table-column>
<el-table-column label="主题" width="135"><template #default="{ row }"><el-tag effect="plain">{{ themeTitle(row.theme) }}</el-tag></template></el-table-column>
<el-table-column prop="entries_count" label="内容" width="85" />
<el-table-column prop="build_count" label="版本" width="85" />
<el-table-column prop="updated_at" label="更新日期" min-width="180" />
<el-table-column label="操作" width="350" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="editSite(row)">管理站点</el-button><el-button link @click="openStudio(row)">设计首页</el-button><el-button link @click="applyPreset(row)">内容套版</el-button><el-button link type="success" @click="build(row)">构建</el-button></template></el-table-column>
</el-table>
</el-card>
<el-dialog v-model="createVisible" title="创建网站项目" width="540px" destroy-on-close>
<el-form :model="draft" label-position="top">
<el-form-item label="网站名称"><el-input v-model="draft.name" placeholder="例如:HESU Global Auto" /></el-form-item>
<el-form-item label="网站地址"><el-input v-model="draft.domain" placeholder="https://example.com" /></el-form-item>
<el-form-item label="描述"><el-input v-model="draft.tagline" placeholder="品牌服务介绍" /></el-form-item>
<el-form-item label="行业主题"><el-select v-model="draft.theme" style="width:100%"><el-option v-for="t in themes" :key="t.id" :label="t.name" :value="t.id" /></el-select></el-form-item>
<el-form-item label="建站模式"><el-radio-group v-model="draft.mode"><el-radio value="new">新建网站</el-radio><el-radio value="migration">旧站迁移</el-radio><el-radio value="hybrid">混合建站</el-radio></el-radio-group></el-form-item>
</el-form>
<template #footer><el-button @click="createVisible=false">取消</el-button><el-button type="primary" :loading="loading" @click="create">创建网站</el-button></template>
</el-dialog>
<el-dialog v-model="siteVisible" :title="active?.name || '网站管理'" width="680px">
<el-tabs v-model="siteTab"><el-tab-pane label="基本信息" name="info"><p>主题:{{ themeTitle(active?.theme) }} · {{ active?.domain }}</p><p>{{ active?.tagline }}</p></el-tab-pane><el-tab-pane label="内容管理" name="content"><el-button @click="nativeConsole(active)">打开内容编辑工作台</el-button><p class="hint">支持页面、产品、文章、SEO及 WordPress/CSV/XLSX 数据导入。</p></el-tab-pane><el-tab-pane label="主题与编辑" name="design"><el-button type="primary" @click="openStudio(active)">进入 SiteForge Visual Studio</el-button></el-tab-pane><el-tab-pane label="静态发布" name="build"><el-button type="success" @click="build(active)">生成静态 HTML</el-button><p class="hint">构建后可以在 SiteForge Web 控制台预览、下载 ZIP 或发布到持久化目录。</p></el-tab-pane></el-tabs>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {computed,onMounted,ref} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
import {preferredSite,rememberSite,sfApi} from './_shared'
import {Plus} from '@element-plus/icons-vue'
interface Site {id:string;name:string;domain:string;tagline:string;theme:string;entries_count:number;build_count:number;updated_at:string}
interface Theme {id:string;name:string}
const sites=ref<Site[]>([]),themes=ref<Theme[]>([]),filter=ref(''),loading=ref(false),createVisible=ref(false),siteVisible=ref(false)
const active=ref<Site|null>(null),siteTab=ref('info'),csrf=ref('')
const draft=ref({name:'',domain:'',tagline:'',theme:'corporate',mode:'new'})
const visibleSites=computed(()=>sites.value.filter(s=>s.name.toLowerCase().includes(filter.value.trim().toLowerCase())))
const themeTitle=(key?:string)=>themes.value.find(t=>t.id===key)?.name||key||'—'
async function api<T>(path:string,init:RequestInit={}){return sfApi<T>(path,init)}
const post=(data:unknown):RequestInit=>({method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})
async function reload(){loading.value=true;try{const [list,ts]=await Promise.all([api<Site[]>('/api/sites'),api<Theme[]>('/api/themes')]);sites.value=list;themes.value=ts}catch(e:any){ElMessage.error(e.message)}finally{loading.value=false}}
onMounted(async()=>{try{csrf.value=(await api<{csrf:string}>('/api/auth/me')).csrf;await reload()}catch(e:any){ElMessage.error(e.message)}})
async function create(){if(!draft.value.name.trim())return ElMessage.warning('请输入网站名称');loading.value=true;try{await api('/api/sites',post(draft.value));createVisible.value=false;await reload();ElMessage.success('网站创建成功')}catch(e:any){ElMessage.error(e.message)}finally{loading.value=false}}
function editSite(s:Site){active.value=s;siteTab.value='info';siteVisible.value=true}
function nativeConsole(s:Site|null){if(s)window.open('/','_blank','noopener')}
async function openStudio(s:Site|null){if(!s)return;try{const entries=await api<Array<{id:string;route:string}>>(`/api/sites/${s.id}/entries`);const home=entries.find(x=>x.route==='/')||entries[0];if(!home)return ElMessage.warning('网站暂无可编辑的页面');window.open(`/editor/${s.id}/${home.id}`,'_blank','noopener')}catch(e:any){ElMessage.error(e.message)}}
async function applyPreset(s:Site){try{await ElMessageBox.confirm('自动给没有布局的页面装配新主题,原 URL、正文和 SEO 不变。继续吗?','批量内容套版');const r=await api<{updated:number}>(`/api/sites/${s.id}/apply-theme`,post({mode:'missing'}));ElMessage.success(`已完成 ${r.updated} 页布局装配`)}catch(e:any){if(e!=='cancel')ElMessage.error(e.message)}}
async function build(s:Site|null){
if(!s)return
try{
const task=await api<{id:string;state:string}>(`/api/sites/${s.id}/builds`,post({mirror_images:false}))
ElMessage.info('构建任务已加入 Worker 队列,正在执行')
for(let attempts=0;attempts<240;attempts++){
await new Promise(resolve=>setTimeout(resolve,1500))
const job=await api<{state:string;error:string;result_json:string}>(`/api/enterprise/sites/${s.id}/jobs/${task.id}`)
if(job.state==='done'){
const result=JSON.parse(job.result_json||'{}')
ElMessage.success(`构建成功:${result.pages||0} 个静态页面`)
if(result.version)window.open(`/preview/${s.id}/${result.version}/`,'_blank','noopener')
await reload();return
}
if(['failed','cancelled'].includes(job.state))throw new Error(job.error||job.state)
}
ElMessage.warning('任务仍在运行,可以在构建中心查询进度')
}catch(e:any){ElMessage.error(e.message)}
}
</script>
<style scoped>
.sf-art-page{padding:4px 4px 30px}.sf-art-intro{display:flex;justify-content:space-between;align-items:center;gap:18px;margin-bottom:27px}.sf-art-intro h1{font-size:30px;color:var(--el-text-color-primary);margin:5px 0 7px}.sf-art-intro p,.table-head p{color:var(--el-text-color-secondary);font-size:13px;margin:0}.sf-overline{font-size:10px;letter-spacing:2px;font-weight:800;color:#5884d4}.metric-line{margin-bottom:22px}.metric-label{font-size:12px;color:var(--el-text-color-secondary);margin-bottom:10px}.metric-line strong{font-size:32px;line-height:1.2;display:block}.table-head{display:flex;justify-content:space-between;align-items:center}.table-head h3{margin:0 0 5px}.site-name{display:flex;align-items:center;gap:11px}.site-mark{background:#eaf1ff;color:#4b73ca;width:38px;height:38px;border-radius:10px;display:grid;place-items:center;font-size:16px;font-weight:800}.site-name b{display:block;font-size:13px}.site-name small{display:block;font-size:11px;color:#98a4b6;margin-top:3px}.hint{font-size:12px;color:#8997a8;line-height:1.8}
</style>