diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..dde215c --- /dev/null +++ b/.env.production @@ -0,0 +1,6 @@ +# 后端接口地址(后端服务端口由 server/.env 的 PORT 决定,当前为 3000) +NEXT_PUBLIC_API_URL=https://web-api.linyikj.com.cn/api +# 图片资源访问地址 +NEXT_PUBLIC_UPLOAD_URL=https://web-api.linyikj.com.cn/uploads +# JWT本地存储Key +NEXT_PUBLIC_TOKEN_KEY=admin_token diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..dc55cc0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "extends": ["next/core-web-vitals"], + "rules": { + "@typescript-eslint/no-explicit-any": "error", + "react/no-unescaped-entities": "off" + } +} diff --git a/.gitea/workflows/main.yml b/.gitea/workflows/main.yml new file mode 100644 index 0000000..e5df906 --- /dev/null +++ b/.gitea/workflows/main.yml @@ -0,0 +1,88 @@ +name: main + +on: + push: + branches: ["main"] + +jobs: + build-and-push: + name: Build and push to Aliyun ACR + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: https://gitee.com/zsqai/checkout@v4 + + - name: Set up Docker Buildx + uses: https://gitee.com/zsqai/setup-buildx-action@v3 + + - name: Login to Aliyun Container Registry + uses: https://gitee.com/zsqai/login-action@v3 + with: + registry: ${{ vars.ALIYUN_REGISTRY }} + username: ${{ vars.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: Build and push Docker image + uses: https://gitee.com/zsqai/build-push-action@v5 + with: + context: . + push: true + # 禁用所有缓存,确保每次都是全新构建 + no-cache: true + build-args: | + BUILD_VERSION=${{ github.sha }} + BUILD_TIME=${{ github.run_number }} + CACHE_BUST=${{ github.run_id }} + tags: | + ${{ vars.ALIYUN_REGISTRY }}/${{ vars.ALIYUN_NAMESPACE }}/${{ vars.ALIYUN_REPO }}:latest + ${{ vars.ALIYUN_REGISTRY }}/${{ vars.ALIYUN_NAMESPACE }}/${{ vars.ALIYUN_REPO }}:${{ github.sha }} + + deploy: + name: Deploy to server + runs-on: ubuntu-latest + needs: build-and-push + steps: + - name: Deploy via SSH + uses: https://gitee.com/zsqai/ssh-action@v1.0.3 + with: + host: ${{ vars.HOST }} + username: ${{ vars.USERNAME }} + key: ${{ secrets.SSH_KEY }} + port: 22 + script: | + # 登录阿里云镜像仓库 + docker login --username=${{ vars.ALIYUN_USERNAME }} --password=${{ secrets.ALIYUN_PASSWORD }} ${{ vars.ALIYUN_REGISTRY }} + + # 停止并删除旧容器 + docker stop ai-personage-web 2>/dev/null || true + docker rm ai-personage-web 2>/dev/null || true + + # 删除旧镜像(强制重新拉取) + docker rmi ${{ vars.ALIYUN_REGISTRY }}/${{ vars.ALIYUN_NAMESPACE }}/${{ vars.ALIYUN_REPO }}:latest 2>/dev/null || true + + # 强制拉取最新镜像 + docker pull ${{ vars.ALIYUN_REGISTRY }}/${{ vars.ALIYUN_NAMESPACE }}/${{ vars.ALIYUN_REPO }}:latest + + # 运行新容器 + docker run -d \ + --name ai-personage-web \ + --restart always \ + -p 8085:3003 \ + -e NODE_OPTIONS="--max-old-space-size=4096" \ + -e NODE_ENV="production" \ + ${{ vars.ALIYUN_REGISTRY }}/${{ vars.ALIYUN_NAMESPACE }}/${{ vars.ALIYUN_REPO }}:latest + + # 等待启动 + sleep 3 + + # 查看 BUILD_ID 确认更新 + echo "=== Build ID ===" + docker exec -it ai-personage-web cat .next/BUILD_ID 2>/dev/null || echo "Cannot read BUILD_ID" + + # 查看日志 + echo "" + echo "=== Container Logs ===" + docker logs ai-personage-web --tail 20 + + # 清理无用镜像 + docker image prune -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87bc58a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.next/ +.env +.env.local +*.log diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8b0e83d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "printWidth": 100, + "tabWidth": 2 +} diff --git a/app/(front)/about/page.tsx b/app/(front)/about/page.tsx new file mode 100644 index 0000000..755e3bc --- /dev/null +++ b/app/(front)/about/page.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; +import { publicApi } from '@/lib/services'; + +export const metadata: Metadata = { + title: '关于我们', + description: '了解企业的发展历程、团队与文化。', +}; + +export const revalidate = 60; + +export default async function AboutPage() { + const config = await publicApi.getSiteConfig().catch(() => null); + return ( +
+ 我们期待您的来访与咨询 +
+ +共 {newsRes.total} 条
+ +{product.desc}
+ )} ++ 共 {productsRes.total} 款产品 +
+ + {/* 分类筛选 */} ++ 由经验丰富的专业人士组成 +
+{m.position}
+ {m.desc && ( +{m.desc}
+ )} +懂物业,更懂“省”心
+ +登录智管物业管理后台
++ 初始账号:admin / 密码:123456 +
+{description}
+ )} +{hint}
+ {error &&{error}
} ++ 共 {total} 条,当前 {start}-{end} +
++ {nickname} +
+@{username}
++ 懂物业,更懂“省”心。一站式物业管理平台,覆盖缴费、报修、公告、巡检全流程。 +
+{error}
} +
+ {children}
+
+ ),
+ code: ({ className: cls, children }) => {
+ // 行内 code(无 language- 前缀)→ 灰底圆角;块级 code 由 包裹
+ const isBlock = typeof cls === 'string' && cls.includes('language-');
+ if (isBlock) {
+ return {children};
+ }
+ return (
+
+ {children}
+
+ );
+ },
+ table: ({ children }) => (
+ + 懂物业,更懂“省”心。立即预约演示,体验智慧物业管理的便捷与高效。 +
++ 或拨打咨询电话 {tel} +
+ )} ++ 六大核心模块覆盖物业管理全流程,让每一个环节都简单、高效、可控 +
++ {f.desc} +
+ + {/* 标签 */} ++ 懂物业,更懂“省”心 +
++ 一站式物业管理平台,覆盖缴费、报修、公告、巡检全流程,助力物业公司降本增效。 +
+ +本文档暂无内容
+ )} + + {/* 上一篇 / 下一篇 */} + {(prev || next) && ( +暂无可显示的文档,请从左侧菜单选择。
++ 若有权限,可在后台「使用手册」中新增文档。 +
++ {news.intro || '点击查看详情'} +
++ {formatDate(news.createdAt)} +
++ {product.desc} +
+ )} + {product.category?.name && ( + + {product.category.name} + + )} +{subtitle}
+ )} ++ 三端协同,覆盖业主、员工、管理层全角色场景 +
++ {s.subtitle} +
+ ++ {s.desc} +
+ 等块级标签
+ * - 纯 Markdown 文本不含 HTML 块级标签(行内 `` 等极少见,且即便匹配也按 HTML 处理仍可被 dangerouslySetInnerHTML 渲染)
+ */
+
+const HTML_BLOCK_RE =
+ /<\/?(p|div|span|h[1-6]|ul|ol|li|table|tbody|thead|tr|td|th|br|img|a|strong|em|b|i|blockquote|pre|code|hr|figure|figcaption|section|article|header|footer|html|body)\b/i;
+
+export function isHTMLContent(s: string | null | undefined): boolean {
+ if (!s) return false;
+ return HTML_BLOCK_RE.test(s);
+}
diff --git a/lib/services.ts b/lib/services.ts
new file mode 100644
index 0000000..b360194
--- /dev/null
+++ b/lib/services.ts
@@ -0,0 +1,48 @@
+/**
+ * 前台公开 API(无需 token)
+ */
+import { http } from './api';
+import type {
+ Banner,
+ Manual,
+ ManualTreeNode,
+ News,
+ NewsCategory,
+ Paginated,
+ Product,
+ ProductCategory,
+ SiteConfig,
+ Team,
+} from './types';
+
+export const publicApi = {
+ getSiteConfig: () =>
+ http.get