国际化国家

This commit is contained in:
2026-02-10 20:55:56 +08:00
parent c8a11a6d78
commit 4ef5815890
29 changed files with 774 additions and 405 deletions

View File

@@ -4,8 +4,8 @@ NODE_ENV=development
VITE_DEV=true VITE_DEV=true
# 请求路径 # 请求路径
# VITE_BASE_URL='http://192.168.2.21:48080' VITE_BASE_URL='http://192.168.2.22:48080'
VITE_BASE_URL='https://backstageapi.yolozs.com' # VITE_BASE_URL='https://backstageapi.yolozs.com'
# VITE_BASE_URL='https://testapi.tknb.net' # VITE_BASE_URL='https://testapi.tknb.net'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务 # 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务
@@ -30,7 +30,11 @@ VITE_BASE_PATH=/
VITE_MALL_H5_DOMAIN='http://localhost:3000' VITE_MALL_H5_DOMAIN='http://localhost:3000'
# 验证码的开关 # 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false VITE_APP_CAPTCHA_ENABLE=true
# Turnstile 站点密钥(生产 Key需要在 Cloudflare 添加 localhost 到域名白名单)
VITE_APP_TURNSTILE_SITE_KEY=0x4AAAAAACYSAf0bQMQ347Pz
# VITE_APP_TURNSTILE_SITE_KEY=1x00000000000000000000AA
# GoView域名 # GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000' VITE_GOVIEW_URL='http://127.0.0.1:3000'

233
CLAUDE.md Normal file
View File

@@ -0,0 +1,233 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is **yudao-ui-admin-vue3**, a Vue 3 admin dashboard built with Vite, TypeScript, and Element Plus. It's part of the 芋道 (Yudao) open-source admin system that supports multi-tenant SaaS scenarios and integrates with Spring Boot/Spring Cloud backends.
## Development Commands
### Setup
```bash
pnpm install # Install dependencies (pnpm is required, version >=8.6.0)
```
### Development
```bash
pnpm dev # Start dev server with env.local environment
pnpm dev-server # Start dev server with dev environment
pnpm ts:check # Run TypeScript type checking
```
### Build
```bash
pnpm build:local # Build for local environment
pnpm build:dev # Build for dev environment
pnpm build:test # Build for test environment
pnpm build:stage # Build for staging environment
pnpm build:prod # Build for production environment
```
### Preview
```bash
pnpm serve:dev # Preview dev build
pnpm serve:prod # Preview production build
pnpm preview # Build local and preview
```
### Linting & Formatting
```bash
pnpm lint:eslint # Fix ESLint issues in .js, .ts, .vue files
pnpm lint:format # Format code with Prettier
pnpm lint:style # Fix Stylelint issues in styles
```
### Cleanup
```bash
pnpm clean # Remove node_modules
pnpm clean:cache # Clear node_modules cache
```
## Architecture
### Technology Stack
- **Framework**: Vue 3.5.12 (Composition API)
- **Build Tool**: Vite 5.1.4
- **UI Library**: Element Plus 2.9.1
- **Language**: TypeScript 5.3.3
- **State Management**: Pinia 2.1.7 with persistence (pinia-plugin-persistedstate)
- **Router**: Vue Router 4.4.5
- **I18n**: Vue I18n 9.10.2
- **HTTP Client**: Axios 1.9.0
- **CSS Framework**: UnoCSS 0.58.5
- **Icons**: Iconify 3.1.1
### Directory Structure
```
src/
├── api/ # API service layer organized by business modules
│ ├── login/ # Authentication APIs
│ ├── system/ # System management (users, roles, menus, etc.)
│ ├── bpm/ # Business Process Management (workflow)
│ ├── infra/ # Infrastructure (code gen, files, jobs, etc.)
│ ├── pay/ # Payment system
│ ├── mall/ # E-commerce/Mall system
│ ├── crm/ # Customer Relationship Management
│ ├── erp/ # Enterprise Resource Planning
│ ├── ai/ # AI/LLM features
│ └── mp/ # WeChat Official Account
├── assets/ # Static assets (images, svgs)
├── components/ # Global reusable components
├── config/ # Configuration files
├── directives/ # Custom Vue directives
├── hooks/ # Composable functions
├── layout/ # Layout components
├── locales/ # I18n translation files
├── plugins/ # Plugin setup (Element Plus, UnoCSS, etc.)
├── router/ # Vue Router configuration
├── store/ # Pinia stores
│ └── modules/ # Store modules (user, permission, dict, etc.)
├── styles/ # Global styles (SCSS)
├── types/ # TypeScript type definitions
├── utils/ # Utility functions
├── views/ # Page components organized by features
├── App.vue # Root component
├── main.ts # Application entry point
└── permission.ts # Route permission guard
```
### Application Bootstrap (src/main.ts)
The application initializes in this order:
1. **I18n** setup (async - loads translations)
2. **Pinia** store setup
3. **Global components** registration
4. **Element Plus** setup
5. **Form Create** (form builder) setup
6. **Router** setup
7. **Directives** (auth, mounted-focus)
8. **VueDOMPurifyHTML** (XSS protection for v-html)
### Permission & Route System (src/permission.ts)
- **Dynamic Route Loading**: Routes are fetched from backend based on user permissions
- **Route Guards**:
- Checks authentication token before each route
- Loads user info and permissions on first access
- Loads dictionary data for the entire app
- Dynamically adds authorized routes using `router.addRoute()`
- **White List**: `/login`, `/social-login`, `/auth-redirect`, `/bind`, `/register`, `/oauthLogin/gitee`
### State Management Pattern
Stores are located in `src/store/modules/` and use Pinia:
- `user.ts` - User information and authentication
- `permission.ts` - User permissions and dynamic routes
- `dict.ts` - System dictionaries (cached)
- Store modules use the "WithOut" pattern for access outside setup: `useUserStoreWithOut()`
### API Layer Pattern
- APIs are organized by business domain in `src/api/`
- Each module has an `index.ts` (API functions) and `types.ts` (TypeScript interfaces)
- Axios is configured in `src/config/axios/` with interceptors for auth and error handling
- API calls should use the centralized request service, not raw axios
### Auto-Import System
The project uses unplugin-auto-import and unplugin-vue-components:
- Vue APIs (ref, computed, etc.) are auto-imported
- Element Plus components are auto-imported
- Custom composables from hooks are auto-imported
- Generated types are in `src/types/auto-imports.d.ts` and `src/types/auto-components.d.ts`
### Form Builder Integration
- Uses `@form-create/element-ui` and `@form-create/designer` for dynamic form building
- BPMN workflow designer uses `bpmn-js` and `bpmn-js-properties-panel`
### Environment Configuration
- Environment files: `.env`, `.env.local`, `.env.dev`, `.env.test`, `.env.stage`, `.env.prod`
- Key variables:
- `VITE_BASE_PATH` - Base URL path
- `VITE_BASE_URL` - API base URL
- `VITE_PORT` - Dev server port
- `VITE_OUT_DIR` - Build output directory
- `VITE_DROP_CONSOLE` - Remove console.log in production
### Build Configuration
- **Code Splitting**:
- `echarts` is split into separate chunk
- `@form-create/element-ui` is split separately
- `@form-create/designer` is split separately
- **Minification**: Uses Terser
- **Memory**: Build uses `--max_old_space_size=4096` for large builds
### Multi-Tenant (SaaS) Support
This system has built-in multi-tenant functionality:
- Tenant management in system modules
- Tenant packages with customizable menu/permission configurations
- APIs should be tenant-aware
## Development Guidelines
### Path Aliases
Use `@/` for imports, which resolves to `src/`:
```typescript
import { useUserStore } from '@/store/modules/user'
```
### TypeScript
- `noImplicitAny` is disabled - type annotations are not strictly required
- Use interfaces from `src/api/*/types.ts` for API request/response types
### I18n
- Translation files are in `src/locales/`
- Use the `useI18n()` composable (auto-imported)
- Support for Chinese and English
### Component Development
- Global components in `src/components/` are auto-registered
- Use Element Plus components (auto-imported, no manual import needed)
- DiyEditor mobile components are excluded from auto-import
### Icons
- SVG icons are in `src/assets/svgs/`
- Use Iconify for icon sets
- SVG icons are registered with `icon-[dir]-[name]` pattern
### Styling
- SCSS is the preprocessor
- Global variables are in `src/styles/variables.scss` (auto-injected)
- UnoCSS is used for atomic/utility CSS
### Security
- XSS protection via `vue-dompurify-html` for v-html directives
- Auth tokens managed in `src/utils/auth`
- Permission directives for button-level access control
## Important Notes
- **Node.js**: Requires >= 16.0.0
- **pnpm**: Must use pnpm >= 8.6.0 (not npm or yarn)
- **Backend**: Designed to work with Spring Boot (single) or Spring Cloud (microservices) backend
- **Route Debugging**: If routes aren't appearing, check the permission store and backend API responses
- **Dictionary Loading**: System dictionaries are loaded once on login and cached in the dict store
## Related Documentation
- Backend API docs (Swagger): Available at `/admin-api/swagger-ui.html` on backend server
- Official docs: https://doc.iocoder.cn
- Demo: http://dashboard-vue3.yudao.iocoder.cn

View File

@@ -146,6 +146,7 @@
</div> </div>
</div> </div>
</div> </div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,5 @@
import request from '@/config/axios' import request from '@/config/axios'
import type { RegisterVO, UserLoginVO } from './types' import type { RegisterVO, TenantRegisterVO, UserLoginVO } from './types'
export interface SmsCodeVO { export interface SmsCodeVO {
mobile: string mobile: string
@@ -16,11 +16,14 @@ export const login = (data: UserLoginVO) => {
return request.post({ url: '/system/auth/login', data }) return request.post({ url: '/system/auth/login', data })
} }
// 注册 // 用户注册
export const register = (data: RegisterVO) => { export const register = (data: RegisterVO) => {
return request.post({ url: '/system/auth/register', data }) return request.post({ url: '/system/auth/register', data })
} }
// 租户注册
export const tenantRegister = (data: TenantRegisterVO) => {
return request.post({ url: '/system/tenant/register', data })
}
// 使用租户名,获得租户编号 // 使用租户名,获得租户编号
export const getTenantIdByName = (name: string) => { export const getTenantIdByName = (name: string) => {
return request.get({ url: '/system/tenant/get-id-by-name?name=' + name }) return request.get({ url: '/system/tenant/get-id-by-name?name=' + name })

View File

@@ -36,3 +36,12 @@ export type RegisterVO = {
password: string password: string
captchaVerification: string captchaVerification: string
} }
export type TenantRegisterVO = {
name: string
contactName: string
contactMobile: string
username: string
password: string
captchaVerification: string
}

View File

@@ -32,11 +32,7 @@
<XTextButton title="预览JSON" @click="previewProcessJson" /> <XTextButton title="预览JSON" @click="previewProcessJson" />
</template> </template>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip v-if="props.simulation" effect="light" :content="simulationStatus ? '退出模拟' : '开启模拟'">
v-if="props.simulation"
effect="light"
:content="simulationStatus ? '退出模拟' : '开启模拟'"
>
<XButton preIcon="ep:cpu" title="模拟" @click="processSimulation" /> <XButton preIcon="ep:cpu" title="模拟" @click="processSimulation" />
</el-tooltip> </el-tooltip>
</ElButtonGroup> </ElButtonGroup>
@@ -47,11 +43,7 @@
icon="el-icon-s-data" icon="el-icon-s-data"
@click="elementsAlign('left')" @click="elementsAlign('left')"
/> --> /> -->
<XButton <XButton preIcon="fa:align-left" class="align align-bottom" @click="elementsAlign('left')" />
preIcon="fa:align-left"
class="align align-bottom"
@click="elementsAlign('left')"
/>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="向右对齐"> <el-tooltip effect="light" content="向右对齐">
<!-- <el-button <!-- <el-button
@@ -59,11 +51,7 @@
icon="el-icon-s-data" icon="el-icon-s-data"
@click="elementsAlign('right')" @click="elementsAlign('right')"
/> --> /> -->
<XButton <XButton preIcon="fa:align-left" class="align align-top" @click="elementsAlign('right')" />
preIcon="fa:align-left"
class="align align-top"
@click="elementsAlign('right')"
/>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="向上对齐"> <el-tooltip effect="light" content="向上对齐">
<!-- <el-button <!-- <el-button
@@ -71,11 +59,7 @@
icon="el-icon-s-data" icon="el-icon-s-data"
@click="elementsAlign('top')" @click="elementsAlign('top')"
/> --> /> -->
<XButton <XButton preIcon="fa:align-left" class="align align-left" @click="elementsAlign('top')" />
preIcon="fa:align-left"
class="align align-left"
@click="elementsAlign('top')"
/>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="向下对齐"> <el-tooltip effect="light" content="向下对齐">
<!-- <el-button <!-- <el-button
@@ -83,11 +67,7 @@
icon="el-icon-s-data" icon="el-icon-s-data"
@click="elementsAlign('bottom')" @click="elementsAlign('bottom')"
/> --> /> -->
<XButton <XButton preIcon="fa:align-left" class="align align-right" @click="elementsAlign('bottom')" />
preIcon="fa:align-left"
class="align align-right"
@click="elementsAlign('bottom')"
/>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="水平居中"> <el-tooltip effect="light" content="水平居中">
<!-- <el-button <!-- <el-button
@@ -96,11 +76,7 @@
@click="elementsAlign('center')" @click="elementsAlign('center')"
/> --> /> -->
<!-- class="align align-center" --> <!-- class="align align-center" -->
<XButton <XButton preIcon="fa:align-left" class="align align-center" @click="elementsAlign('center')" />
preIcon="fa:align-left"
class="align align-center"
@click="elementsAlign('center')"
/>
</el-tooltip> </el-tooltip>
<el-tooltip effect="light" content="垂直居中"> <el-tooltip effect="light" content="垂直居中">
<!-- <el-button <!-- <el-button
@@ -108,11 +84,7 @@
icon="el-icon-s-data" icon="el-icon-s-data"
@click="elementsAlign('middle')" @click="elementsAlign('middle')"
/> --> /> -->
<XButton <XButton preIcon="fa:align-left" class="align align-middle" @click="elementsAlign('middle')" />
preIcon="fa:align-left"
class="align align-middle"
@click="elementsAlign('middle')"
/>
</el-tooltip> </el-tooltip>
</ElButtonGroup> </ElButtonGroup>
<ElButtonGroup key="scale-control"> <ElButtonGroup key="scale-control">
@@ -122,11 +94,7 @@
icon="el-icon-zoom-out" icon="el-icon-zoom-out"
@click="processZoomOut()" @click="processZoomOut()"
/> --> /> -->
<XButton <XButton preIcon="ep:zoom-out" @click="processZoomOut()" :disabled="defaultZoom < 0.2" />
preIcon="ep:zoom-out"
@click="processZoomOut()"
:disabled="defaultZoom < 0.2"
/>
</el-tooltip> </el-tooltip>
<el-button>{{ Math.floor(defaultZoom * 10 * 10) + '%' }}</el-button> <el-button>{{ Math.floor(defaultZoom * 10 * 10) + '%' }}</el-button>
<el-tooltip effect="light" content="放大视图"> <el-tooltip effect="light" content="放大视图">
@@ -162,32 +130,16 @@
</ElButtonGroup> </ElButtonGroup>
</template> </template>
<!-- 用于打开本地文件--> <!-- 用于打开本地文件-->
<input <input type="file" id="files" ref="refFile" style="display: none" accept=".xml, .bpmn"
type="file" @change="importLocalFile" />
id="files"
ref="refFile"
style="display: none"
accept=".xml, .bpmn"
@change="importLocalFile"
/>
</div> </div>
<div class="my-process-designer__container"> <div class="my-process-designer__container">
<div <div class="my-process-designer__canvas" ref="bpmnCanvas" id="bpmnCanvas" style="width: 1680px; height: 800px">
class="my-process-designer__canvas" </div>
ref="bpmnCanvas"
id="bpmnCanvas"
style="width: 1680px; height: 800px"
></div>
<!-- <div id="js-properties-panel" class="panel"></div> --> <!-- <div id="js-properties-panel" class="panel"></div> -->
<!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> --> <!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> -->
</div> </div>
<Dialog <Dialog title="预览" v-model="previewModelVisible" width="80%" :scroll="true" max-height="600px">
title="预览"
v-model="previewModelVisible"
width="80%"
:scroll="true"
max-height="600px"
>
<div> <div>
<pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre> <pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre>
</div> </div>
@@ -261,13 +213,13 @@ const props = defineProps({
translations: { translations: {
// 自定义的翻译文件 // 自定义的翻译文件
type: Object, type: Object,
default: () => {} default: () => { }
}, },
additionalModel: [Object, Array], // 自定义model additionalModel: [Object, Array], // 自定义model
moddleExtension: { moddleExtension: {
// 自定义moddle // 自定义moddle
type: Object, type: Object,
default: () => {} default: () => { }
}, },
onlyCustomizeAddi: { onlyCustomizeAddi: {
type: Boolean, type: Boolean,
@@ -542,8 +494,7 @@ const setEncoded = (type, data) => {
const encodedData = encodeURIComponent(data) const encodedData = encodeURIComponent(data)
return { return {
filename: `${filename}.${type}`, filename: `${filename}.${type}`,
href: `data:application/${ href: `data:application/${type === 'svg' ? 'text/xml' : 'bpmn20-xml'
type === 'svg' ? 'text/xml' : 'bpmn20-xml'
};charset=UTF-8,${encodedData}`, };charset=UTF-8,${encodedData}`,
data: data data: data
} }
@@ -642,7 +593,7 @@ const previewProcessJson = () => {
}) })
} }
/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */ /* ------------------------------------------------ YOLO methods ------------------------------------------------------ */
onMounted(() => { onMounted(() => {
initBpmnModeler() initBpmnModeler()
createNewDiagram(props.value) createNewDiagram(props.value)

View File

@@ -1,5 +1,5 @@
/** /**
* Created by 芋道源码 * Created by YOLO
* *
* 枚举类 * 枚举类
*/ */

View File

@@ -49,6 +49,8 @@
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 忘记密码 --> <!-- 忘记密码 -->
<ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" /> <ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 租户注册 -->
<TenantRegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
</div> </div>
</Transition> </Transition>
</div> </div>
@@ -63,7 +65,7 @@ import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch' import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown' import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components' import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm, TenantRegisterForm } from './components'
defineOptions({ name: 'Login' }) defineOptions({ name: 'Login' })

View File

@@ -50,46 +50,35 @@
</el-col> </el-col>
<Verify v-if="loginData.captchaEnable === 'true'" ref="verify" :captchaType="captchaType" <Verify v-if="loginData.captchaEnable === 'true'" ref="verify" :captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }" mode="pop" @success="handleLogin" /> :imgSize="{ width: '400px', height: '200px' }" mode="pop" @success="handleLogin" />
<!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item> <el-form-item>
<el-row :gutter="5" justify="space-between" style="width: 100%"> <el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="8"> <!-- <el-col :span="8">
<XButton <XButton :title="t('login.btnMobile')" class="w-[100%]" @click="setLoginState(LoginStateEnum.MOBILE)" />
:title="t('login.btnMobile')"
class="w-[100%]"
@click="setLoginState(LoginStateEnum.MOBILE)"
/>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<XButton <XButton :title="t('login.btnQRCode')" class="w-[100%]" @click="setLoginState(LoginStateEnum.QR_CODE)" />
:title="t('login.btnQRCode')"
class="w-[100%]"
@click="setLoginState(LoginStateEnum.QR_CODE)"
/>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="24">
<XButton <XButton :title="t('login.btnRegister')" class="w-[100%]"
:title="t('login.btnRegister')" @click="setLoginState(LoginStateEnum.REGISTER)" />
class="w-[100%]" </el-col> -->
@click="setLoginState(LoginStateEnum.REGISTER)"
/>
<!-- 租户注册 -->
<el-col :span="24" style="margin-top: 10px;">
<XButton title="租户注册" class="w-[100%]" @click="setLoginState(LoginStateEnum.TENANT_REGISTER)" />
</el-col> </el-col>
</el-row> </el-row>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider> <!-- <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item> <el-form-item>
<div class="w-[100%] flex justify-between"> <div class="w-[100%] flex justify-between">
<Icon <Icon v-for="(item, key) in socialList" :key="key" :icon="item.icon" :size="30"
v-for="(item, key) in socialList" class="anticon cursor-pointer" color="#999" @click="doSocialLogin(item.type)" />
:key="key"
:icon="item.icon"
:size="30"
class="anticon cursor-pointer"
color="#999"
@click="doSocialLogin(item.type)"
/>
</div> </div>
</el-form-item> </el-form-item>
</el-col> </el-col>

View File

@@ -1,14 +1,6 @@
<template> <template>
<el-form <el-form v-show="getShow" ref="formSmsLogin" :model="loginData.loginForm" :rules="rules" class="login-form"
v-show="getShow" label-position="top" label-width="120px" size="large">
ref="formSmsLogin"
:model="loginData.loginForm"
:rules="rules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<el-row style="margin-right: -10px; margin-left: -10px"> <el-row style="margin-right: -10px; margin-left: -10px">
<!-- 租户名 --> <!-- 租户名 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
@@ -18,23 +10,15 @@
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName"> <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input <el-input v-model="loginData.loginForm.tenantName" :placeholder="t('login.tenantNamePlaceholder')"
v-model="loginData.loginForm.tenantName" :prefix-icon="iconHouse" type="primary" link />
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
type="primary"
link
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 手机号 --> <!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="mobileNumber"> <el-form-item prop="mobileNumber">
<el-input <el-input v-model="loginData.loginForm.mobileNumber" :placeholder="t('login.mobileNumberPlaceholder')"
v-model="loginData.loginForm.mobileNumber" :prefix-icon="iconCellphone" />
:placeholder="t('login.mobileNumberPlaceholder')"
:prefix-icon="iconCellphone"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 验证码 --> <!-- 验证码 -->
@@ -42,19 +26,11 @@
<el-form-item prop="code"> <el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%"> <el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24"> <el-col :span="24">
<el-input <el-input v-model="loginData.loginForm.code" :placeholder="t('login.codePlaceholder')"
v-model="loginData.loginForm.code" :prefix-icon="iconCircleCheck">
:placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck"
>
<!-- <el-button class="w-[100%]"> --> <!-- <el-button class="w-[100%]"> -->
<template #append> <template #append>
<span <span v-if="mobileCodeTimer <= 0" class="getMobileCode" style="cursor: pointer" @click="getSmsCode">
v-if="mobileCodeTimer <= 0"
class="getMobileCode"
style="cursor: pointer"
@click="getSmsCode"
>
{{ t('login.getSmsCode') }} {{ t('login.getSmsCode') }}
</span> </span>
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer"> <span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
@@ -70,23 +46,13 @@
<!-- 登录按钮 / 返回按钮 --> <!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item> <el-form-item>
<XButton <XButton :loading="loginLoading" :title="t('login.login')" class="w-[100%]" type="primary"
:loading="loginLoading" @click="signIn()" />
:title="t('login.login')"
class="w-[100%]"
type="primary"
@click="signIn()"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item> <el-form-item>
<XButton <XButton :loading="loginLoading" :title="t('login.backLogin')" class="w-[100%]" @click="handleBackLogin()" />
:loading="loginLoading"
:title="t('login.backLogin')"
class="w-[100%]"
@click="handleBackLogin()"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -133,7 +99,7 @@ const loginData = reactive({
}, },
loginForm: { loginForm: {
uuid: '', uuid: '',
tenantName: '芋道源码', tenantName: 'YOLO',
mobileNumber: '', mobileNumber: '',
code: '' code: ''
} }
@@ -202,7 +168,7 @@ const signIn = async () => {
} }
push({ path: redirect.value || permissionStore.addRouters[0].path }) push({ path: redirect.value || permissionStore.addRouters[0].path })
}) })
.catch(() => {}) .catch(() => { })
.finally(() => { .finally(() => {
loginLoading.value = false loginLoading.value = false
setTimeout(() => { setTimeout(() => {

View File

@@ -1,14 +1,6 @@
<template> <template>
<el-form <el-form v-show="getShow" ref="formLogin" :model="registerData.registerForm" :rules="registerRules" class="login-form"
v-show="getShow" label-position="top" label-width="120px" size="large">
ref="formLogin"
:model="registerData.registerForm"
:rules="registerRules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<el-row style="margin-right: -10px; margin-left: -10px"> <el-row style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item> <el-form-item>
@@ -17,81 +9,42 @@
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName"> <el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
<el-input <el-input v-model="registerData.registerForm.tenantName" :placeholder="t('login.tenantname')"
v-model="registerData.registerForm.tenantName" :prefix-icon="iconHouse" link type="primary" size="large" />
:placeholder="t('login.tenantname')"
:prefix-icon="iconHouse"
link
type="primary"
size="large"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input v-model="registerData.registerForm.username" :placeholder="t('login.username')" size="large"
v-model="registerData.registerForm.username" :prefix-icon="iconAvatar" />
:placeholder="t('login.username')"
size="large"
:prefix-icon="iconAvatar"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input v-model="registerData.registerForm.nickname" placeholder="昵称" size="large"
v-model="registerData.registerForm.nickname" :prefix-icon="iconAvatar" />
placeholder="昵称"
size="large"
:prefix-icon="iconAvatar"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input v-model="registerData.registerForm.password" type="password" auto-complete="off"
v-model="registerData.registerForm.password" :placeholder="t('login.password')" size="large" :prefix-icon="iconLock" show-password />
type="password"
auto-complete="off"
:placeholder="t('login.password')"
size="large"
:prefix-icon="iconLock"
show-password
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="confirmPassword"> <el-form-item prop="confirmPassword">
<el-input <el-input v-model="registerData.registerForm.confirmPassword" type="password" size="large" auto-complete="off"
v-model="registerData.registerForm.confirmPassword" :placeholder="t('login.checkPassword')" :prefix-icon="iconLock" show-password />
type="password"
size="large"
auto-complete="off"
:placeholder="t('login.checkPassword')"
:prefix-icon="iconLock"
show-password
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px"> <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item> <el-form-item>
<XButton <XButton :loading="loginLoading" :title="t('login.register')" class="w-[100%]" type="primary"
:loading="loginLoading" @click="getCode()" />
:title="t('login.register')"
class="w-[100%]"
type="primary"
@click="getCode()"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<Verify <Verify v-if="registerData.captchaEnable === 'true'" ref="verify" :captchaType="captchaType"
v-if="registerData.captchaEnable === 'true'" :imgSize="{ width: '400px', height: '200px' }" mode="pop" @success="handleRegister" />
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
mode="pop"
@success="handleRegister"
/>
</el-row> </el-row>
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" /> <XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
</el-form> </el-form>

View File

@@ -0,0 +1,323 @@
<template>
<el-form v-show="getShow" ref="formLogin" :model="registerData.registerForm" :rules="registerRules" class="login-form"
label-position="top" label-width="120px" size="large">
<el-row style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="name">
<el-input v-model="registerData.registerForm.name" placeholder="租户名称" :prefix-icon="iconHouse" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="contactName">
<el-input v-model="registerData.registerForm.contactName" placeholder="联系人" :prefix-icon="iconAvatar" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="contactMobile">
<el-input v-model="registerData.registerForm.contactMobile" placeholder="联系手机" :prefix-icon="iconMobile" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username">
<el-input v-model="registerData.registerForm.username" :placeholder="t('login.username')"
:prefix-icon="iconAvatar" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<el-input v-model="registerData.registerForm.password" type="password" auto-complete="off"
:placeholder="t('login.password')" :prefix-icon="iconLock" show-password />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="confirmPassword">
<el-input v-model="registerData.registerForm.confirmPassword" type="password" auto-complete="off"
:placeholder="t('login.checkPassword')" :prefix-icon="iconLock" show-password />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton :loading="loginLoading" :title="t('login.register')" class="w-[100%]" type="primary"
@click="getCode()" />
</el-form-item>
</el-col>
<el-col v-if="registerData.captchaEnable === 'true'" :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<div id="turnstile-container" class="w-[100%] flex justify-center"></div>
</el-form-item>
</el-col>
</el-row>
<XButton :title="t('login.hasUser')" class="w-[100%]" @click="handleBackLogin()" />
</el-form>
</template>
<script lang="ts" setup>
import { ElLoading } from 'element-plus'
import LoginFormTitle from './LoginFormTitle.vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
import * as authUtil from '@/utils/auth'
import { usePermissionStore } from '@/store/modules/permission'
import * as LoginApi from '@/api/login'
import { LoginStateEnum, useLoginState } from './useLogin'
declare global {
interface Window {
turnstile: any
}
}
defineOptions({ name: 'TenantRegisterForm' })
const { t } = useI18n()
const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const iconMobile = useIcon({ icon: 'ep:iphone' })
const formLogin = ref()
const { handleBackLogin, getLoginState } = useLoginState()
const { currentRoute, push } = useRouter()
const permissionStore = usePermissionStore()
const redirect = ref<string>('')
const loginLoading = ref(false)
// const verify = ref()
// const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.TENANT_REGISTER)
const equalToPassword = (_rule, value, callback) => {
if (registerData.registerForm.password !== value) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const registerRules = {
name: [
{ required: true, trigger: 'blur', message: '请输入租户名称' },
{ min: 2, max: 20, message: '租户名称长度必须介于 2 和 20 之间', trigger: 'blur' }
],
contactName: [
{ required: true, trigger: 'blur', message: '请输入联系人' }
],
contactMobile: [
{ required: true, trigger: 'blur', message: '请输入联系手机' },
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
username: [
{ required: true, trigger: 'blur', message: '请输入您的账号' },
{ min: 4, max: 30, message: '用户账号长度必须介于 4 和 30 之间', trigger: 'blur' }
],
password: [
{ required: true, trigger: 'blur', message: '请输入您的密码' },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\\ |', trigger: 'blur' }
],
confirmPassword: [
{ required: true, trigger: 'blur', message: '请再次输入您的密码' },
{ required: true, validator: equalToPassword, trigger: 'blur' }
]
}
const registerData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
registerForm: {
name: '',
contactName: '',
contactMobile: '',
username: '',
password: '',
confirmPassword: '',
turnstileToken: ''
}
})
const turnstileToken = ref('')
const turnstileWidgetId = ref('')
const initTurnstile = () => {
const waitForTurnstile = (retries = 0) => {
if (retries > 50) {
console.error('Turnstile SDK 加载超时')
return
}
if (window.turnstile && !turnstileWidgetId.value) {
const container = document.getElementById('turnstile-container')
if (!container) {
console.error('未找到 Turnstile 容器')
return
}
try {
const siteKey = import.meta.env.VITE_APP_TURNSTILE_SITE_KEY || '0x4AAAAAAAK0rP_gCjP2oJDa'
console.log('使用 Site Key:', siteKey)
turnstileWidgetId.value = window.turnstile.render('#turnstile-container', {
sitekey: siteKey,
theme: 'light',
callback: function (token) {
console.log('Turnstile 验证成功token:', token)
turnstileToken.value = token
},
'error-callback': function (error) {
console.error('Turnstile 验证失败:', error)
// 测试环境下,如果验证失败,使用测试 token
if (siteKey === '1x00000000000000000000AA') {
console.log('测试模式:使用模拟 token')
turnstileToken.value = 'XXXX.DUMMY.TOKEN.XXXX'
} else {
turnstileToken.value = ''
}
},
'expired-callback': function () {
console.warn('Turnstile token 已过期')
turnstileToken.value = ''
}
})
console.log('Turnstile 组件已渲染Widget ID:', turnstileWidgetId.value)
} catch (error) {
console.error('Turnstile 渲染错误:', error)
}
} else if (!window.turnstile) {
// SDK 还未加载100ms 后重试
setTimeout(() => waitForTurnstile(retries + 1), 100)
}
}
waitForTurnstile()
}
// 重置 Turnstile 验证码
const resetTurnstile = () => {
if (window.turnstile && turnstileWidgetId.value) {
window.turnstile.reset(turnstileWidgetId.value)
turnstileToken.value = ''
console.log('Turnstile 已重置')
}
}
// 提交注册
const handleRegister = async () => {
loginLoading.value = true
try {
const form = unref(formLogin)
if (!form) return
const valid = await form.validate()
if (!valid) return
if (registerData.captchaEnable === 'true' && !turnstileToken.value) {
ElMessage.warning('请完成人机验证')
return
}
if (registerData.captchaEnable) {
registerData.registerForm.turnstileToken = turnstileToken.value
}
console.log('提交注册turnstileToken:', turnstileToken.value)
const res = await LoginApi.tenantRegister(registerData.registerForm)
if (!res) {
// 注册失败,重置验证码
resetTurnstile()
return
}
console.log(res, '注册成功返回')
// 注册成功,保存账号和租户名到缓存,用于自动填充登录表单
authUtil.setLoginForm({
tenantName: registerData.registerForm.name,
username: registerData.registerForm.username,
password: '',
rememberMe: true
})
// 提示注册成功
ElMessage.success('注册成功,请登录')
// 跳转回登录页
handleBackLogin()
} catch (error) {
// 请求异常时也要重置验证码
console.error('注册失败:', error)
resetTurnstile()
} finally {
loginLoading.value = false
}
}
// 获取验证码 / 提交
const getCode = async () => {
// Just call handleRegister directly now, as Turnstile is on-page
await handleRegister()
}
const loading = ref() // ElLoading.service 返回的实例
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
watch(
() => getShow.value,
(isShow) => {
console.log('getShow 变化:', isShow)
console.log('captchaEnable:', registerData.captchaEnable)
console.log('环境变量 CAPTCHA_ENABLE:', import.meta.env.VITE_APP_CAPTCHA_ENABLE)
console.log('环境变量 SITE_KEY:', import.meta.env.VITE_APP_TURNSTILE_SITE_KEY)
if (isShow && registerData.captchaEnable === 'true') {
console.log('准备初始化 Turnstile')
nextTick(() => {
console.log('nextTick 中初始化 Turnstile')
initTurnstile()
})
}
}
)
onMounted(() => {
console.log('组件已挂载')
console.log('getShow:', getShow.value)
console.log('captchaEnable:', registerData.captchaEnable)
console.log('window.turnstile:', window.turnstile)
// Check if already visible on mount (unlikely but possible)
if (getShow.value && registerData.captchaEnable === 'true') {
console.log('挂载时初始化 Turnstile')
initTurnstile()
}
})
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.login-code {
float: right;
width: 100%;
height: 38px;
img {
width: 100%;
height: auto;
max-width: 100px;
vertical-align: middle;
cursor: pointer;
}
}
</style>

View File

@@ -5,5 +5,6 @@ import RegisterForm from './RegisterForm.vue'
import QrCodeForm from './QrCodeForm.vue' import QrCodeForm from './QrCodeForm.vue'
import SSOLoginVue from './SSOLogin.vue' import SSOLoginVue from './SSOLogin.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue' import ForgetPasswordForm from './ForgetPasswordForm.vue'
import TenantRegisterForm from './TenantRegisterForm.vue'
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm } export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm, TenantRegisterForm }

View File

@@ -3,6 +3,7 @@ import { Ref } from 'vue'
export enum LoginStateEnum { export enum LoginStateEnum {
LOGIN, LOGIN,
REGISTER, REGISTER,
TENANT_REGISTER,
RESET_PASSWORD, RESET_PASSWORD,
MOBILE, MOBILE,
QR_CODE, QR_CODE,

View File

@@ -1,5 +1,5 @@
/** /**
* Created by 芋道源码 * Created by YOLO
* *
* AI 枚举类 * AI 枚举类
* *

View File

@@ -1,5 +1,5 @@
/** /**
* Created by 芋道源码 * Created by YOLO
* *
* AI 枚举类 * AI 枚举类
* *

View File

@@ -1,7 +1,7 @@
<!-- <!--
- Copyright (C) 2018-2019 - Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com - All rights reserved, Designed By www.joolun.com
芋道源码 YOLO
移除 avue 组件使用 ElementUI 原生组件 移除 avue 组件使用 ElementUI 原生组件
--> -->
<template> <template>
@@ -21,12 +21,8 @@
</div> </div>
</div> </div>
<!-- 分页组件 --> <!-- 分页组件 -->
<Pagination <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
:total="total" @pagination="getMaterialPageFun" />
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
</div> </div>
<!-- 类型voice --> <!-- 类型voice -->
<div v-else-if="props.type === 'voice'"> <div v-else-if="props.type === 'voice'">
@@ -39,29 +35,18 @@
<WxVoicePlayer :url="scope.row.url" /> <WxVoicePlayer :url="scope.row.url" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="上传时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
label="上传时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center" fixed="right"> <el-table-column label="操作" align="center" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button type="primary" link @click="selectMaterialFun(scope.row)" <el-button type="primary" link @click="selectMaterialFun(scope.row)">选择
>选择
<Icon icon="ep:plus" /> <Icon icon="ep:plus" />
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页组件 --> <!-- 分页组件 -->
<Pagination <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
:total="total" @pagination="getPage" />
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getPage"
/>
</div> </div>
<!-- 类型video --> <!-- 类型video -->
<div v-else-if="props.type === 'video'"> <div v-else-if="props.type === 'video'">
@@ -76,34 +61,18 @@
<WxVideoPlayer :url="scope.row.url" /> <WxVideoPlayer :url="scope.row.url" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="上传时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
label="上传时间" <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
>
<template #default="scope"> <template #default="scope">
<el-button type="primary" link @click="selectMaterialFun(scope.row)" <el-button type="primary" link @click="selectMaterialFun(scope.row)">选择
>选择
<Icon icon="akar-icons:circle-plus" /> <Icon icon="akar-icons:circle-plus" />
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页组件 --> <!-- 分页组件 -->
<Pagination <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
:total="total" @pagination="getMaterialPageFun" />
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
</div> </div>
<!-- 类型news --> <!-- 类型news -->
<div v-else-if="props.type === 'news'"> <div v-else-if="props.type === 'news'">
@@ -121,12 +90,8 @@
</div> </div>
</div> </div>
<!-- 分页组件 --> <!-- 分页组件 -->
<Pagination <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
:total="total" @pagination="getMaterialPageFun" />
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -229,7 +194,7 @@ onMounted(async () => {
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@media (width >= 992px) and (width <= 1300px) { @media (width >=992px) and (width <=1300px) {
.waterfall { .waterfall {
column-count: 3; column-count: 3;
} }
@@ -239,7 +204,7 @@ onMounted(async () => {
} }
} }
@media (width >= 768px) and (width <= 991px) { @media (width >=768px) and (width <=991px) {
.waterfall { .waterfall {
column-count: 2; column-count: 2;
} }
@@ -249,7 +214,7 @@ onMounted(async () => {
} }
} }
@media (width <= 767px) { @media (width <=767px) {
.waterfall { .waterfall {
column-count: 1; column-count: 1;
} }

View File

@@ -1,7 +1,7 @@
<!-- <!--
- Copyright (C) 2018-2019 - Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com - All rights reserved, Designed By www.joolun.com
芋道源码 YOLO
移除暂时用不到的 websocket 移除暂时用不到的 websocket
代码优化补充注释提升阅读性 代码优化补充注释提升阅读性
--> -->
@@ -11,12 +11,9 @@
<!-- 加载更多 --> <!-- 加载更多 -->
<div v-loading="loading"></div> <div v-loading="loading"></div>
<div v-if="!loading"> <div v-if="!loading">
<div class="el-table__empty-block" v-if="hasMore" @click="loadMore" <div class="el-table__empty-block" v-if="hasMore" @click="loadMore"><span
><span class="el-table__empty-text">点击加载更多</span></div class="el-table__empty-text">点击加载更多</span></div>
> <div class="el-table__empty-block" v-if="!hasMore"><span class="el-table__empty-text">没有更多了</span></div>
<div class="el-table__empty-block" v-if="!hasMore"
><span class="el-table__empty-text">没有更多了</span></div
>
</div> </div>
<!-- 消息列表 --> <!-- 消息列表 -->

View File

@@ -2,7 +2,7 @@
- Copyright (C) 2018-2019 - Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com - All rights reserved, Designed By www.joolun.com
微信消息 - 图文 微信消息 - 图文
芋道源码 YOLO
代码优化补充注释提升阅读性 代码优化补充注释提升阅读性
--> -->
<template> <template>
@@ -12,11 +12,7 @@
<a v-if="index === 0" :href="article.url" target="_blank"> <a v-if="index === 0" :href="article.url" target="_blank">
<div class="news-main"> <div class="news-main">
<div class="news-content"> <div class="news-content">
<el-image <el-image :src="article.picUrl" class="material-img" style="width: 100%; height: 120px" />
:src="article.picUrl"
class="material-img"
style="width: 100%; height: 120px"
/>
<div class="news-content-title"> <div class="news-content-title">
<span>{{ article.title }}</span> <span>{{ article.title }}</span>
</div> </div>

View File

@@ -1,7 +1,7 @@
<!-- <!--
- Copyright (C) 2018-2019 - Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com - All rights reserved, Designed By www.joolun.com
芋道源码 YOLO
移除多余的 rep 为前缀的变量 message 消息更简单 移除多余的 rep 为前缀的变量 message 消息更简单
代码优化补充注释提升阅读性 代码优化补充注释提升阅读性
优化消息的临时缓存策略发送消息时只清理被发送消息的 tab不会强制切回到 text 输入 优化消息的临时缓存策略发送消息时只清理被发送消息的 tab不会强制切回到 text 输入
@@ -12,7 +12,9 @@
<!-- 类型 1文本 --> <!-- 类型 1文本 -->
<el-tab-pane :name="ReplyType.Text"> <el-tab-pane :name="ReplyType.Text">
<template #label> <template #label>
<el-row align="middle"><Icon icon="ep:document" /> 文本</el-row> <el-row align="middle">
<Icon icon="ep:document" /> 文本
</el-row>
</template> </template>
<TabText v-model="reply.content" /> <TabText v-model="reply.content" />
</el-tab-pane> </el-tab-pane>
@@ -20,7 +22,9 @@
<!-- 类型 2图片 --> <!-- 类型 2图片 -->
<el-tab-pane :name="ReplyType.Image"> <el-tab-pane :name="ReplyType.Image">
<template #label> <template #label>
<el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row> <el-row align="middle">
<Icon icon="ep:picture" class="mr-5px" /> 图片
</el-row>
</template> </template>
<TabImage v-model="reply" /> <TabImage v-model="reply" />
</el-tab-pane> </el-tab-pane>
@@ -28,7 +32,9 @@
<!-- 类型 3语音 --> <!-- 类型 3语音 -->
<el-tab-pane :name="ReplyType.Voice"> <el-tab-pane :name="ReplyType.Voice">
<template #label> <template #label>
<el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row> <el-row align="middle">
<Icon icon="ep:phone" /> 语音
</el-row>
</template> </template>
<TabVoice v-model="reply" /> <TabVoice v-model="reply" />
</el-tab-pane> </el-tab-pane>
@@ -36,7 +42,9 @@
<!-- 类型 4视频 --> <!-- 类型 4视频 -->
<el-tab-pane :name="ReplyType.Video"> <el-tab-pane :name="ReplyType.Video">
<template #label> <template #label>
<el-row align="middle"><Icon icon="ep:share" /> 视频</el-row> <el-row align="middle">
<Icon icon="ep:share" /> 视频
</el-row>
</template> </template>
<TabVideo v-model="reply" /> <TabVideo v-model="reply" />
</el-tab-pane> </el-tab-pane>
@@ -44,7 +52,9 @@
<!-- 类型 5图文 --> <!-- 类型 5图文 -->
<el-tab-pane :name="ReplyType.News"> <el-tab-pane :name="ReplyType.News">
<template #label> <template #label>
<el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row> <el-row align="middle">
<Icon icon="ep:reading" /> 图文
</el-row>
</template> </template>
<TabNews v-model="reply" :news-type="newsType" /> <TabNews v-model="reply" :news-type="newsType" />
</el-tab-pane> </el-tab-pane>
@@ -52,7 +62,9 @@
<!-- 类型 6音乐 --> <!-- 类型 6音乐 -->
<el-tab-pane :name="ReplyType.Music"> <el-tab-pane :name="ReplyType.Music">
<template #label> <template #label>
<el-row align="middle"><Icon icon="ep:service" />音乐</el-row> <el-row align="middle">
<Icon icon="ep:service" />音乐
</el-row>
</template> </template>
<TabMusic v-model="reply" /> <TabMusic v-model="reply" />
</el-tab-pane> </el-tab-pane>

View File

@@ -2,7 +2,7 @@
- Copyright (C) 2018-2019 - Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com - All rights reserved, Designed By www.joolun.com
微信消息 - 视频 微信消息 - 视频
芋道源码 YOLO
bug 修复 bug 修复
1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容 1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容
存在的问题mediaId 有效期是 3 超过时间后无法播放 存在的问题mediaId 有效期是 3 超过时间后无法播放
@@ -20,18 +20,9 @@
<!-- 弹窗播放 --> <!-- 弹窗播放 -->
<el-dialog v-model="dialogVideo" title="视频播放" append-to-body> <el-dialog v-model="dialogVideo" title="视频播放" append-to-body>
<video-player <video-player v-if="dialogVideo" class="video-player vjs-big-play-centered" :src="props.url" poster=""
v-if="dialogVideo" crossorigin="anonymous" controls playsinline :volume="0.6" :width="800"
class="video-player vjs-big-play-centered" :playback-rates="[0.7, 1.0, 1.5, 2.0]" />
:src="props.url"
poster=""
crossorigin="anonymous"
controls
playsinline
:volume="0.6"
:width="800"
:playback-rates="[0.7, 1.0, 1.5, 2.0]"
/>
<!-- 事件暫時沒用 <!-- 事件暫時沒用
@mounted="handleMounted"--> @mounted="handleMounted"-->
<!-- @ready="handleEvent($event)"--> <!-- @ready="handleEvent($event)"-->

View File

@@ -2,7 +2,7 @@
- Copyright (C) 2018-2019 - Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com - All rights reserved, Designed By www.joolun.com
微信消息 - 语音 微信消息 - 语音
芋道源码 YOLO
bug 修复 bug 修复
1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容 1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容
存在的问题mediaId 有效期是 3 超过时间后无法播放 存在的问题mediaId 有效期是 3 超过时间后无法播放

View File

@@ -58,7 +58,7 @@ const emit = defineEmits<{
column-count: 5; column-count: 5;
margin-top: 10px; margin-top: 10px;
/* 芋道源码:增加 10px避免顶着上面 */ /* YOLO:增加 10px避免顶着上面 */
} }
.waterfall-item { .waterfall-item {

View File

@@ -55,8 +55,9 @@
<el-form-item :label="t('newHosts.hostsCountryinfo')" prop="country"> <el-form-item :label="t('newHosts.hostsCountryinfo')" prop="country">
<el-select v-model="queryParams.country" :placeholder="t('newHosts.placeHostsCountry')" clearable <el-select v-model="queryParams.country" :placeholder="t('newHosts.placeHostsCountry')" clearable
class="!w-240px"> class="!w-240px">
<el-option v-for="dict in countryinfoList" :key="dict.id" :label="dict.countryName" <el-option v-for="dict in countryinfoList" :key="dict.id"
:value="dict.countryName" /> :label="t('newHosts.min') == '最小值' ? dict.countryName : dict.countryNameEnglish"
:value="t('newHosts.min') == '最小值' ? dict.countryName : dict.countryNameEnglish" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -313,11 +314,11 @@
</div> </div>
<MobilePagination v-if="isMobile" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" <MobilePagination v-if="isMobile" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
:total="total" :page-sizes="[10, 20, 30, 50]" @size-change="getList()" @load="getList()" @load-pre="getList()" /> :total="total" :page-sizes="[10, 20, 30, 50]" @size-change="getList()" @load="getList()" @load-pre="getList()" />
<div v-if="isMobile" class="mobile-total"> <div v-if="isMobile" class="mobile-total">
<span class="mobile-total__label"></span> <span class="mobile-total__label"></span>
<span class="mobile-total__value">{{ total }}</span> <span class="mobile-total__value">{{ total }}</span>
<span class="mobile-total__suffix"></span> <span class="mobile-total__suffix"></span>
</div> </div>
<!-- PC 显示分页移动端隐藏 --> <!-- PC 显示分页移动端隐藏 -->
<Pagination v-if="!isMobile" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize" <Pagination v-if="!isMobile" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" /> @pagination="getList" />

View File

@@ -53,8 +53,9 @@
<el-form-item :label="t('newHosts.hostsCountryinfo')" prop="country"> <el-form-item :label="t('newHosts.hostsCountryinfo')" prop="country">
<el-select v-model="queryParams.country" :placeholder="t('newHosts.placeHostsCountry')" clearable <el-select v-model="queryParams.country" :placeholder="t('newHosts.placeHostsCountry')" clearable
class="!w-240px"> class="!w-240px">
<el-option v-for="dict in countryinfoList" :key="dict.id" :label="dict.countryName" <el-option v-for="dict in countryinfoList" :key="dict.id"
:value="dict.countryName" /> :label="t('newHosts.min') == '最小值' ? dict.countryName : dict.countryNameEnglish"
:value="t('newHosts.min') == '最小值' ? dict.countryName : dict.countryNameEnglish" />
</el-select> </el-select>
</el-form-item> </el-form-item>

View File

@@ -85,8 +85,9 @@
<el-form-item :label="t('newHosts.hostsCountryinfo')" prop="country"> <el-form-item :label="t('newHosts.hostsCountryinfo')" prop="country">
<el-select v-model="queryParams.country" :placeholder="t('newHosts.placeHostsCountry')" clearable <el-select v-model="queryParams.country" :placeholder="t('newHosts.placeHostsCountry')" clearable
class="!w-240px"> class="!w-240px">
<el-option v-for="dict in countryinfoList" :key="dict.id" :label="dict.countryName" <el-option v-for="dict in countryinfoList" :key="dict.id"
:value="dict.countryName" /> :label="t('newHosts.min') == '最小值' ? dict.countryName : dict.countryNameEnglish"
:value="t('newHosts.min') == '最小值' ? dict.countryName : dict.countryNameEnglish" />
</el-select> </el-select>
</el-form-item> </el-form-item>

View File

@@ -1,12 +1,6 @@
<template> <template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="800"> <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
<el-form <el-form ref="formRef" v-loading="formLoading" :model="formData" :rules="formRules" label-width="80px">
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="80px"
>
<el-form-item label="公告标题" prop="title"> <el-form-item label="公告标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入公告标题" /> <el-input v-model="formData.title" placeholder="请输入公告标题" />
</el-form-item> </el-form-item>
@@ -15,22 +9,21 @@
</el-form-item> </el-form-item>
<el-form-item label="公告类型" prop="type"> <el-form-item label="公告类型" prop="type">
<el-select v-model="formData.type" clearable placeholder="请选择公告类型"> <el-select v-model="formData.type" clearable placeholder="请选择公告类型">
<el-option <el-option v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE)" :key="parseInt(dict.value as any)"
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE)" :label="dict.label" :value="parseInt(dict.value as any)" />
:key="parseInt(dict.value as any)" </el-select>
:label="dict.label" </el-form-item>
:value="parseInt(dict.value as any)" <el-form-item label="公告级别" prop="category">
/> <el-select v-model="formData.category" clearable placeholder="请选择公告级别">
<el-option label="通知" value="info" />
<el-option label="警告" value="warning" />
<el-option label="紧急" value="danger" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="formData.status" clearable placeholder="请选择状态"> <el-select v-model="formData.status" clearable placeholder="请选择状态">
<el-option <el-option v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="parseInt(dict.value as any)"
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :label="dict.label" :value="parseInt(dict.value as any)" />
:key="parseInt(dict.value as any)"
:label="dict.label"
:value="parseInt(dict.value as any)"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remark"> <el-form-item label="备注" prop="remark">
@@ -60,6 +53,7 @@ const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({ const formData = ref({
id: undefined, id: undefined,
title: '', title: '',
category: 'info',
type: undefined, type: undefined,
content: '', content: '',
status: CommonStatusEnum.ENABLE, status: CommonStatusEnum.ENABLE,
@@ -68,6 +62,7 @@ const formData = ref({
const formRules = reactive({ const formRules = reactive({
title: [{ required: true, message: '公告标题不能为空', trigger: 'blur' }], title: [{ required: true, message: '公告标题不能为空', trigger: 'blur' }],
type: [{ required: true, message: '公告类型不能为空', trigger: 'change' }], type: [{ required: true, message: '公告类型不能为空', trigger: 'change' }],
category: [{ required: true, message: '公告级别不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }], status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
content: [{ required: true, message: '公告内容不能为空', trigger: 'blur' }] content: [{ required: true, message: '公告内容不能为空', trigger: 'blur' }]
}) })

View File

@@ -1,55 +1,29 @@
<template> <template>
<ContentWrap> <ContentWrap>
<!-- 搜索工作栏 --> <!-- 搜索工作栏 -->
<el-form <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公告标题" prop="title"> <el-form-item label="公告标题" prop="title">
<el-input <el-input v-model="queryParams.title" placeholder="请输入公告标题" clearable @keyup.enter="handleQuery"
v-model="queryParams.title" class="!w-240px" />
placeholder="请输入公告标题"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item> </el-form-item>
<el-form-item label="公告状态" prop="status"> <el-form-item label="公告状态" prop="status">
<el-select <el-select v-model="queryParams.status" placeholder="请选择公告状态" clearable class="!w-240px">
v-model="queryParams.status" <el-option v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :label="dict.label"
placeholder="请选择公告状态" :value="dict.value" />
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> <el-button @click="handleQuery">
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> <Icon icon="ep:search" class="mr-5px" /> 搜索
<el-button </el-button>
type="primary" <el-button @click="resetQuery">
plain <Icon icon="ep:refresh" class="mr-5px" /> 重置
@click="openForm('create')" </el-button>
v-hasPermi="['system:notice:create']" <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['system:notice:create']">
>
<Icon icon="ep:plus" class="mr-5px" /> 新增 <Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> </el-button>
<el-button <el-button type="danger" plain :disabled="checkedIds.length === 0" @click="handleDeleteBatch"
type="danger" v-hasPermi="['system:notice:delete']">
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['system:notice:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除 <Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button> </el-button>
</el-form-item> </el-form-item>
@@ -62,6 +36,15 @@
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column label="公告编号" align="center" prop="id" /> <el-table-column label="公告编号" align="center" prop="id" />
<el-table-column label="公告标题" align="center" prop="title" /> <el-table-column label="公告标题" align="center" prop="title" />
<el-table-column label="公告级别" align="center" prop="category">
<template #default="scope">
<!-- 根据公告级别显示不同的标签 -->
<el-tag v-if="scope.row.category === 'info'" type="success" disable-transitions>通知</el-tag>
<el-tag v-else-if="scope.row.category === 'warning'" type="warning" disable-transitions>警告</el-tag>
<el-tag v-else-if="scope.row.category === 'danger'" type="danger" disable-transitions>紧急</el-tag>
</template>
</el-table-column>
<el-table-column label="公告类型" align="center" prop="type"> <el-table-column label="公告类型" align="center" prop="type">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="scope.row.type" /> <dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="scope.row.type" />
@@ -72,29 +55,14 @@
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column label="创建时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
label="创建时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="操作" align="center"> <el-table-column label="操作" align="center">
<template #default="scope"> <template #default="scope">
<el-button <el-button link type="primary" @click="openForm('update', scope.row.id)"
link v-hasPermi="['system:notice:update']">
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['system:notice:update']"
>
编辑 编辑
</el-button> </el-button>
<el-button <el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['system:notice:delete']">
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['system:notice:delete']"
>
删除 删除
</el-button> </el-button>
<el-button link @click="handlePush(scope.row.id)" v-hasPermi="['system:notice:update']"> <el-button link @click="handlePush(scope.row.id)" v-hasPermi="['system:notice:update']">
@@ -104,12 +72,8 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页 --> <!-- 分页 -->
<Pagination <Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
:total="total" @pagination="getList" />
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap> </ContentWrap>
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
@@ -178,7 +142,7 @@ const handleDelete = async (id: number) => {
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
// 刷新列表 // 刷新列表
await getList() await getList()
} catch {} } catch { }
} }
/** 批量删除按钮操作 */ /** 批量删除按钮操作 */
@@ -196,7 +160,7 @@ const handleDeleteBatch = async () => {
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
// 刷新列表 // 刷新列表
await getList() await getList()
} catch {} } catch { }
} }
/** 推送按钮操作 */ /** 推送按钮操作 */
@@ -207,7 +171,7 @@ const handlePush = async (id: number) => {
// 发起推送 // 发起推送
await NoticeApi.pushNotice(id) await NoticeApi.pushNotice(id)
message.success(t('推送成功')) message.success(t('推送成功'))
} catch {} } catch { }
} }
/** 初始化 **/ /** 初始化 **/

View File

@@ -43,14 +43,23 @@
<el-date-picker v-model="formData.brotherExpireTime" clearable placeholder="请选择爬大哥过期时间" type="date" <el-date-picker v-model="formData.brotherExpireTime" clearable placeholder="请选择爬大哥过期时间" type="date"
value-format="x" /> value-format="x" />
</el-form-item> </el-form-item>
<el-form-item v-if="tenantLevel == 0" label="爬虫过期时间" prop="crawlExpireTime">
<el-date-picker v-model="formData.crawlExpireTime" clearable placeholder="请选择爬虫过期时间" type="date"
value-format="x" />
</el-form-item>
<el-form-item v-if="tenantLevel == 0" label="爬虫后台过期时间" prop="expireTime">
<el-form-item v-if="tenantLevel == 0" label="后台过期时间" prop="expireTime">
<el-date-picker v-model="formData.expireTime" clearable placeholder="请选择过期时间" type="date" value-format="x" /> <el-date-picker v-model="formData.expireTime" clearable placeholder="请选择过期时间" type="date" value-format="x" />
</el-form-item> </el-form-item>
<el-form-item v-else label="爬虫后台过期时间" prop="expireTime">
<el-form-item v-else label="后台过期时间" prop="expireTime">
<el-date-picker v-model="formData.expireTime" clearable placeholder="请选择过期时间" disabled type="date" <el-date-picker v-model="formData.expireTime" clearable placeholder="请选择过期时间" disabled type="date"
value-format="x" /> value-format="x" />
</el-form-item> </el-form-item>
<el-form-item label="绑定域名" prop="website"> <el-form-item label="绑定域名" prop="website">
<el-input v-model="formData.website" placeholder="请输入绑定域名" /> <el-input v-model="formData.website" placeholder="请输入绑定域名" />
</el-form-item> </el-form-item>
@@ -208,6 +217,7 @@ const formData = ref({
remark: undefined as string | undefined, remark: undefined as string | undefined,
aiExpireTime: undefined as number | undefined, aiExpireTime: undefined as number | undefined,
brotherExpireTime: undefined as number | undefined, brotherExpireTime: undefined as number | undefined,
crawlExpireTime: undefined as number | undefined,
tenantType: undefined as number | undefined, tenantType: undefined as number | undefined,
parentId: undefined as number | undefined, parentId: undefined as number | undefined,