Browse Source

```

feat(env): 添加百度统计开关配置项

在 .env、.env.production 和 .env.static 文件中新增 `VITE_APP_BAIDU_ENABLE` 配置项,
用于控制是否启用百度统计功能,默认设置为 false。

feat(package): 替换构建脚本中的 tsx 命令为 esno

将 build 相关命令中的 `tsx` 替换为 `pnpm exec esno`,以统一执行方式并提高兼容性。

fix(drawer): 优化 detail 模式下的容器挂载逻辑

当内容区容器不存在或高度为 0 时,回退到 body 容器并避免添加 __detail 类,
防止因 absolute 定位导致抽屉不可见的问题。

feat(icon): 改进图标缺失时的展示样式

引入 `.iconify-missing` 样式类,在图标加载失败时显示带问号的圆形占位符,
提升用户体验与界面可读性。

feat(router): 新增模型创建页面路由

添加 `/model/create` 路由及其子路由,支持从模型列表跳转至新建模型页面,
隐藏菜单和面包屑导航。

feat(tongji): 增强百度统计初始化逻辑

增加对 `VITE_APP_BAIDU_ENABLE` 环境变量的支持,并确保 script 标签不会重复插入,
增强百度统计的安全性和可控性。

refactor(model): 移除 CreateModel 抽屉组件引用

移除 ModelCard 中对 CreateModel 抽屉组件的依赖,改为通过路由跳转实现创建功能,
简化组件结构并降低耦合度。
```
pull/85/head
chenjiale 3 weeks ago
parent
commit
2c653e72af
  1. 1
      .env
  2. 1
      .env.production
  3. 3
      .env.static
  4. 6
      package.json
  5. 21
      src/components/Drawer/src/BasicDrawer.vue
  6. 22
      src/components/Icon/src/Icon.vue
  7. 22
      src/router/routes/index.ts
  8. 52
      src/utils/tongji.ts
  9. 177
      src/views/model/create/index.vue
  10. 15
      src/views/model/list/ModelCard.vue

1
.env

@ -17,4 +17,5 @@ VITE_GLOB_APP_CAPTCHA_ENABLE = true
VITE_APP_DOCALERT_ENABLE=false
# 百度统计
VITE_APP_BAIDU_ENABLE = false
VITE_APP_BAIDU_CODE = eb21166668bf766b9d059a6fd1c10777

1
.env.production

@ -24,4 +24,5 @@ VITE_GLOB_API_URL_PREFIX =
VITE_USE_PWA = false
# 百度统计
VITE_APP_BAIDU_ENABLE = false
VITE_APP_BAIDU_CODE = eb21166668bf766b9d059a6fd1c10777

3
.env.static

@ -24,7 +24,8 @@ VITE_GLOB_API_URL_PREFIX =
VITE_USE_PWA = false
# 百度统计
VITE_APP_BAIDU_ENABLE = false
VITE_APP_BAIDU_CODE = eb21166668bf766b9d059a6fd1c10777
# 验证码的开关
VITE_GLOB_APP_CAPTCHA_ENABLE = false
VITE_GLOB_APP_CAPTCHA_ENABLE = false

6
package.json

@ -25,9 +25,9 @@
"serve": "pnpm dev",
"dev": "vite",
"front": "vite --mode front",
"build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 vite build && tsx ./build/script/postBuild.ts",
"build:test": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode test && tsx ./build/script/postBuild.ts",
"build:static": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode static && tsx ./build/script/postBuild.ts",
"build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 vite build && pnpm exec esno ./build/script/postBuild.ts",
"build:test": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode test && pnpm exec esno ./build/script/postBuild.ts",
"build:static": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode static && pnpm exec esno ./build/script/postBuild.ts",
"build:no-cache": "pnpm store prune && pnpm build",
"report": "cross-env REPORT=true pnpm build",
"type:check": "vue-tsc --noEmit --skipLibCheck",

21
src/components/Drawer/src/BasicDrawer.vue

@ -52,11 +52,26 @@ const getProps = computed((): DrawerProps => {
if (!width)
opt.width = '100%'
// detail /
// 0 Drawer `position: absolute`
const detailCls = `${prefixCls}__detail`
opt.rootClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls
if (!getContainer)
opt.getContainer = `.${prefixVar}-layout-content`
const selector = `.${prefixVar}-layout-content`
const layoutEl = typeof document === 'undefined'
? null
: (document.querySelector(selector) as HTMLElement | null)
const canUseLayoutContainer = !!layoutEl && layoutEl.getBoundingClientRect().height > 0
if (canUseLayoutContainer) {
opt.rootClassName = wrapClassName ? `${wrapClassName} ${detailCls}` : detailCls
if (!getContainer)
opt.getContainer = layoutEl
}
else {
// 退body `__detail` absolute
opt.rootClassName = wrapClassName
}
}
return opt as DrawerProps
})

22
src/components/Icon/src/Icon.vue

@ -50,8 +50,8 @@ async function update() {
}
else {
const span = document.createElement('span')
span.className = 'iconify'
span.dataset.icon = icon
span.className = 'iconify-missing'
span.title = icon
el.textContent = ''
el.appendChild(span)
}
@ -99,4 +99,22 @@ span.iconify {
background-color: @iconify-bg-color;
border-radius: 100%;
}
span.iconify-missing {
display: flex;
align-items: center;
justify-content: center;
min-width: 1em;
min-height: 1em;
background-color: @iconify-bg-color;
border-radius: 100%;
font-size: 0.75em;
color: currentcolor;
&::after {
content: '?';
line-height: 1;
opacity: 0.65;
}
}
</style>

22
src/router/routes/index.ts

@ -119,6 +119,28 @@ export const basicRoutes = [
RootRoute,
ProfileRoute,
CodegenRoute,
{
path: '/model/create',
name: 'ModelCreateBasic',
component: LAYOUT,
meta: {
title: '新建模型',
hideMenu: true,
hideBreadcrumb: true,
},
children: [
{
path: '',
name: 'ModelCreate',
component: () => import('@/views/model/create/index.vue'),
meta: {
title: '新建模型',
hideMenu: true,
currentActiveMenu: '/model/list',
},
},
],
},
{
path: '/model/assess-report/:id?',
name: 'AssessReportBasic',

52
src/utils/tongji.ts

@ -1,23 +1,35 @@
import { router } from '@/router'
// 用于 router push
window._hmt = window._hmt || []
// HM_ID
declare global {
interface Window {
_hmt?: any[]
}
}
// HM_ID(构建时注入)
const HM_ID = import.meta.env.VITE_APP_BAIDU_CODE
;(function () {
// 有值的时候,才开启
if (!HM_ID)
return
const hm = document.createElement('script')
hm.src = `https://hm.baidu.com/hm.js?${HM_ID}`
const s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore(hm, s)
})()
router.afterEach((to) => {
if (!HM_ID)
return
_hmt.push(['_trackPageview', to.fullPath])
})
// 统计开关:默认关闭,离线/内网部署建议保持关闭
const BAIDU_TONGJI_ENABLED = import.meta.env.VITE_APP_BAIDU_ENABLE === 'true'
const shouldEnable = BAIDU_TONGJI_ENABLED && !!HM_ID
if (shouldEnable) {
window._hmt = window._hmt || []
;(function () {
const existing = document.querySelector<HTMLScriptElement>(`script[data-hm-id="${HM_ID}"]`)
if (existing)
return
const hm = document.createElement('script')
hm.setAttribute('data-hm-id', HM_ID)
hm.src = `https://hm.baidu.com/hm.js?${HM_ID}`
const s = document.getElementsByTagName('script')[0]
s?.parentNode?.insertBefore(hm, s)
})()
router.afterEach((to) => {
window._hmt?.push(['_trackPageview', to.fullPath])
})
}

177
src/views/model/create/index.vue

@ -0,0 +1,177 @@
<script lang="ts" setup>
import { computed, reactive, ref, toRefs } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Alert, Button, Card, Space, Steps } from 'ant-design-vue'
import Step1 from '@/views/model/list/step/Step1.vue'
import Step2 from '@/views/model/list/step/Step2.vue'
import Step3 from '@/views/model/list/step/Step3.vue'
import Step4 from '@/views/model/list/step/Step4.vue'
import { PageWrapper } from '@/components/Page'
defineOptions({ name: 'ModelCreatePage' })
const route = useRoute()
const router = useRouter()
const systemId = computed(() => {
const v = route.query.systemId
return v === undefined || v === null || v === '' ? undefined : Number(v)
})
const unitId = computed(() => {
const v = route.query.unitId
return v === undefined || v === null || v === '' ? undefined : Number(v)
})
const current = ref(0)
const step1Data = ref<any>(null)
const step2Data = ref<any>(null)
const step3Data = ref<any>(null)
const state = reactive({
initStep2: false,
initStep3: false,
initStep4: false,
initStep5: false,
})
const { initStep2, initStep3, initStep4, initStep5 } = toRefs(state)
const canShowContextWarning = computed(() => {
return systemId.value === undefined || unitId.value === undefined
})
function handleStep1Next(step1Values: any) {
current.value++
step1Data.value = step1Values
state.initStep2 = true
}
function handleStepPrev() {
current.value--
}
function handleStep2Next(step2Values: any) {
current.value++
step2Data.value = step2Values
step2Values.systemId = systemId.value
step2Values.unitId = unitId.value
state.initStep3 = true
}
function handleStep3Next(step3Values: any) {
current.value++
step3Data.value = step3Values
state.initStep4 = true
}
function handleStep4Next(step4Values: any) {
current.value++
state.initStep5 = true
}
function handleRedo() {
current.value = 0
state.initStep2 = false
state.initStep3 = false
state.initStep4 = false
state.initStep5 = false
}
function goBack() {
router.push('/model/list')
}
</script>
<template>
<PageWrapper title="新建模型" content-background content-class="p-4">
<div class="model-create">
<div class="model-create__top">
<Space>
<Button @click="goBack">
返回模型列表
</Button>
<Button v-if="current > 0" @click="handleRedo">
重新开始
</Button>
</Space>
</div>
<Alert
v-if="canShowContextWarning"
class="model-create__hint"
type="warning"
show-icon
message="提示:当前未携带系统/机组信息"
description="你仍然可以完成建模;但如果需要绑定到具体机组,请从模型列表页选择系统/机组后再进入“新建模型”。"
/>
<div class="model-create__steps">
<a-steps :current="current">
<a-step title="填写基本信息" />
<a-step title="算法参数配置" />
<a-step title="数据源选取" />
<a-step title="完成" />
</a-steps>
</div>
<Card class="model-create__content" :bordered="false">
<Step1 v-show="current === 0" @next="handleStep1Next" />
<Step2
v-show="current === 1"
v-if="initStep2"
:before-data="step1Data"
@prev="handleStepPrev"
@next="handleStep2Next"
/>
<Step3
v-show="current === 2"
v-if="initStep3"
:before-data="step2Data"
:system-id="systemId"
:unit-id="unitId"
@prev="handleStepPrev"
@next="handleStep3Next"
/>
<Step4
v-show="current === 3"
v-if="initStep4"
:model-id="step3Data"
@redo="handleRedo"
/>
</Card>
</div>
</PageWrapper>
</template>
<style lang="less" scoped>
.model-create {
max-width: 1200px;
margin: 0 auto;
&__top {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
&__hint {
margin-bottom: 12px;
}
&__steps {
position: sticky;
top: 0;
z-index: 3;
padding: 12px 16px;
margin-bottom: 16px;
background: var(--component-background);
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.06));
border-radius: 8px;
}
&__content {
border-radius: 10px;
box-shadow: 0 8px 24px rgb(15 23 42 / 6%);
}
}
</style>

15
src/views/model/list/ModelCard.vue

@ -3,12 +3,10 @@ import { Card, Dropdown, Menu, Popconfirm } from 'ant-design-vue'
import type { PropType } from 'vue'
import { ref, watch } from 'vue'
import type { ModelItem } from './data'
import CreateModel from './CreateModel.vue'
import Icon from '@/components/Icon/index'
import { modelCardListApi, modelDeleteApi } from '@/api/alert/model/models'
import type { ModelQueryParams } from '@/api/alert/model/model/models'
import { useGo } from '@/hooks/web/usePage'
import { useDrawer } from '@/components/Drawer'
import { useMessage } from '@/hooks/web/useMessage'
const props = defineProps({
@ -26,7 +24,6 @@ const props = defineProps({
},
})
const [registerDraw, { openDrawer }] = useDrawer()
const { createMessage } = useMessage()
//
@ -97,6 +94,15 @@ async function confirmDelete(id: number | string) {
createMessage.success('删除成功')
await loadModelList(props.selectData)
}
function goCreateModel() {
const query: string[] = []
if (props.systemId !== undefined && props.systemId !== null)
query.push(`systemId=${encodeURIComponent(String(props.systemId))}`)
if (props.unitId !== undefined && props.unitId !== null)
query.push(`unitId=${encodeURIComponent(String(props.unitId))}`)
go(`/model/create${query.length ? `?${query.join('&')}` : ''}`)
}
</script>
<template>
@ -158,11 +164,10 @@ async function confirmDelete(id: number | string) {
</template>
</Dropdown>
</template>
<Card v-show="systemId != null" size="small" class="icon-card" :hoverable="true" @click="openDrawer(true)">
<Card v-show="systemId != null" size="small" class="icon-card" :hoverable="true" @click="goCreateModel">
<Icon icon="ic:sharp-add" :size="80" color="#a7a9a7" />
</Card>
</div>
<CreateModel :system-id="systemId" :unit-id="unitId" @register="registerDraw" />
</div>
</template>

Loading…
Cancel
Save