Ver código fonte

feat: 集成 Vue I18n 国际化支持,新增多语言配置和切换功能。更新了相关组件以支持中文、英文和日文,优化了用户界面中的文本显示,确保用户体验更佳。

master
lizhuang 2 dias atrás
pai
commit
e2b4f23a4e

+ 315
- 0
docs/i18n-guide.md Ver arquivo

@@ -0,0 +1,315 @@
# 多语言配置使用指南

## 概述

本项目已集成 Vue I18n 国际化支持,支持中文和英文两种语言。系统会根据用户浏览器语言自动选择默认语言,用户也可以手动切换语言。

## 主要特性

- ✅ 支持中文(zh)、英文(en)和日文(ja)
- ✅ 自动检测浏览器语言
- ✅ Cookie 持久化语言选择
- ✅ Element UI 组件多语言支持
- ✅ Vuex 状态管理
- ✅ 语言切换组件

## 目录结构

```
src/
├── i18n/
│ ├── index.js # i18n 主配置文件
│ └── lang/
│ ├── zh.js # 中文语言包
│ ├── en.js # 英文语言包
│ └── ja.js # 日文语言包
├── components/
│ └── LangSelect/ # 语言切换组件
│ └── index.vue
└── store/
└── modules/
└── language.js # 语言状态管理
```

## 基础使用

### 1. 模板中使用

```vue
<template>
<div>
<!-- 基础翻译 -->
<h1>{{ $t('system.title') }}</h1>
<!-- 按钮文本 -->
<el-button>{{ $t('button.save') }}</el-button>
<!-- 属性中使用 -->
<el-input :placeholder="$t('form.username')"></el-input>
<!-- 带参数的翻译 -->
<span>{{ $t('pagination.total', { total: 100 }) }}</span>
</div>
</template>
```

### 2. JavaScript 中使用

```javascript
export default {
methods: {
showMessage() {
// 显示国际化消息
this.$message.success(this.$t('message.success'))
},
// 表单验证规则中使用
getValidationRules() {
return {
username: [
{
required: true,
message: this.$t('message.inputRequired'),
trigger: 'blur'
}
]
}
}
}
}
```

### 3. 语言切换组件

```vue
<template>
<div>
<!-- 在导航栏或任何位置添加语言切换器 -->
<lang-select></lang-select>
</div>
</template>
```

## 语言包结构

### 中文语言包 (src/i18n/lang/zh.js)

```javascript
export default {
// 系统通用
system: {
title: '数字化办公自动化系统',
name: 'DOAS',
welcome: '欢迎使用'
},
// 通用按钮
button: {
search: '搜索',
reset: '重置',
add: '新增',
save: '保存'
},
// 表单标签
form: {
username: '用户名',
password: '密码',
email: '邮箱'
}
}
```

### 英文语言包 (src/i18n/lang/en.js)

```javascript
export default {
// System common
system: {
title: 'Digital Office Automation System',
name: 'DOAS',
welcome: 'Welcome to'
},
// Common buttons
button: {
search: 'Search',
reset: 'Reset',
add: 'Add',
save: 'Save'
},
// Form labels
form: {
username: 'Username',
password: 'Password',
email: 'Email'
}
}
```

### 日文语言包 (src/i18n/lang/ja.js)

```javascript
export default {
// システム共通
system: {
title: 'デジタルオフィス自動化システム',
name: 'DOAS',
welcome: 'ようこそ'
},
// 共通ボタン
button: {
search: '検索',
reset: 'リセット',
add: '追加',
save: '保存'
},
// フォームラベル
form: {
username: 'ユーザー名',
password: 'パスワード',
email: 'メールアドレス'
}
}
```

## 高级用法

### 1. 动态语言切换

```javascript
// 在组件中切换语言
this.$i18n.locale = 'ja' // 或 'zh', 'en'
this.$store.dispatch('language/setLanguage', 'ja')
```

### 2. 条件翻译

```vue
<template>
<div>
<span v-if="$i18n.locale === 'zh'">中文特有内容</span>
<span v-else>English specific content</span>
</div>
</template>
```

### 3. 复数形式处理

```javascript
// 语言包中定义
{
message: {
apple: 'no apples | one apple | {count} apples'
}
}

// 使用
{{ $tc('message.apple', count) }}
```

## 开发指南

### 1. 添加新的翻译

1. 在 `src/i18n/lang/zh.js` 中添加中文翻译
2. 在 `src/i18n/lang/en.js` 中添加对应的英文翻译
3. 确保两个语言包的 key 保持一致

### 2. 最佳实践

#### 命名规范
- 使用有意义的嵌套结构
- 按功能模块组织翻译 key
- 避免过深的嵌套层级(建议不超过 3 层)

```javascript
// ✅ 推荐
{
user: {
profile: {
name: '姓名',
email: '邮箱'
}
}
}

// ❌ 不推荐
{
userProfileFormFieldLabelName: '姓名'
}
```

#### 参数化翻译
```javascript
// 语言包
{
welcome: '欢迎 {name},您有 {count} 条新消息'
}

// 使用
{{ $t('welcome', { name: userName, count: messageCount }) }}
```

### 3. 测试多语言

建议在开发过程中:
1. 经常切换语言测试界面显示
2. 检查所有文本是否正确翻译
3. 确保不同语言下的布局不会破坏

## 扩展支持新语言

系统目前支持中文、英文、日文三种语言。如需添加其他新语言(如韩语),按以下步骤:

1. 创建语言包文件 `src/i18n/lang/ko.js`
2. 在 `src/i18n/index.js` 中导入并配置
3. 更新语言选择组件支持新语言
4. 导入对应的 Element UI 语言包

```javascript
// src/i18n/index.js
import koLocale from './lang/ko'
import elementKoLocale from 'element-ui/lib/locale/lang/ko'

const messages = {
// ...existing languages
ko: {
...koLocale,
...elementKoLocale
}
}
```

## 注意事项

1. **性能考虑**:大型应用建议按需加载语言包
2. **SEO 优化**:考虑使用不同的路由前缀区分语言版本
3. **日期时间**:使用 moment.js 或 dayjs 处理不同语言的日期格式
4. **数字格式**:注意不同地区的数字、货币格式差异

## 常见问题

### Q: 切换语言后页面没有立即更新?
A: 确保使用了响应式的 `$t()` 函数,避免在 `data()` 中缓存翻译结果。

### Q: Element UI 组件的文本没有翻译?
A: 检查是否正确配置了 Element UI 的 i18n 选项。

### Q: 如何处理长文本翻译?
A: 建议将长文本放在单独的文件中,或者使用富文本编辑器。

## 参与贡献

如需添加新的翻译或改进现有翻译,请:
1. Fork 项目仓库
2. 创建功能分支
3. 添加或修改翻译文件
4. 提交 Pull Request

---

更多信息请参考 [Vue I18n 官方文档](https://vue-i18n.intlify.dev/)

+ 1
- 0
package.json Ver arquivo

@@ -51,6 +51,7 @@
"vue-codemirror": "^4.0.6",
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-i18n": "^8.28.2",
"vue-meta": "2.4.0",
"vue-quill-editor": "^3.0.6",
"vue-router": "3.4.9",

+ 1
- 0
src/assets/icons/svg/i18n.svg Ver arquivo

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1753319916186" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4445" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M522.695111 21.333333c3.811556 0 7.623111 0.170667 11.377778 0.512a490.666667 490.666667 0 0 1 0 980.309334 133.575111 133.575111 0 0 1-11.377778 0.512H512A490.666667 490.666667 0 0 1 21.333333 512 490.666667 490.666667 0 0 1 512 21.333333l7.224889 0.056889 3.413333-0.056889z m116.622222 533.333334H406.016l0.341333 10.865777 0.796445 17.408 0.455111 8.590223 1.080889 17.066666 1.251555 16.725334 1.536 16.384 1.706667 16.156444 0.910222 7.964445 0.967111 7.850666 2.048 15.473778 2.275556 15.132444 2.389333 14.734223 2.616889 14.336 2.730667 13.937777 2.901333 13.482667 1.479111 6.656 1.592889 6.428445 3.185778 12.629333 3.413333 12.117333 3.527111 11.662222 2.730667 8.419556 2.844444 8.078222 3.811556 10.353778c18.773333 48.469333 41.927111 77.937778 67.015111 80.099556h1.024l5.973333-0.170667c23.722667-2.616889 45.624889-29.696 63.772445-74.069333l2.332444-5.859556 3.868445-10.353778 2.844444-8.078222 2.673778-8.419556 3.527111-11.662222 3.413333-12.117333 3.185778-12.629333 1.536-6.485334 1.536-6.599111 2.901334-13.482667 2.787555-13.937777 2.56-14.336 2.446222-14.791111 2.730667-18.887112 2.503111-19.512888 0.910222-7.964445 1.706667-16.099555 1.536-16.497778 1.251556-16.725334 1.137777-17.009777 0.455111-8.590223 0.739556-17.408 0.341333-10.865777z m-318.805333 0h-211.626667a405.788444 405.788444 0 0 0 275.512889 342.186666 492.202667 492.202667 0 0 1-33.28-92.728889l-2.901333-11.719111-3.299556-14.108444-1.536-7.168-1.479111-7.281778-2.844444-14.791111-1.365334-7.566222-2.56-15.303111-2.389333-15.587556-1.080889-7.964444-1.991111-16.042667-0.967111-8.135111-1.763556-16.497778-1.536-16.725333-1.365333-17.066667-0.568889-8.533333-1.308444-21.788445-0.967111-22.186666-0.682667-20.992z m594.602667 0h-190.293334l-0.853333 25.486222-1.024 22.016-0.512 8.760889-1.137778 17.237333-1.365333 17.066667-1.536 16.725333-0.853333 8.305778-2.332445 20.366222-2.673778 19.911111-2.275555 15.644445-1.308445 7.68-2.616889 15.132444-1.422222 7.452445-2.958222 14.677333-1.536 7.168-3.299556 14.108444a530.090667 530.090667 0 0 1-31.630222 94.890667 405.674667 405.674667 0 0 0 249.628445-332.629333zM384.398222 127.146667l-3.128889 1.080889A405.731556 405.731556 0 0 0 108.885333 469.333333h211.626667l0.853333-25.486222 1.024-22.016 0.512-8.760889 1.137778-17.237333 1.365333-17.066667 1.536-16.725333 0.853334-8.305778 2.332444-20.366222 2.673778-19.911111 2.275556-15.644445 1.308444-7.68 2.616889-15.132444 1.422222-7.452445 2.958222-14.677333 1.536-7.168 3.299556-14.108444a506.538667 506.538667 0 0 1 36.181333-104.448z m136.305778-20.366223h-1.024c-24.120889 2.104889-46.421333 29.354667-64.853333 74.353778l-2.275556 5.745778-3.811555 10.353778-2.844445 8.078222-2.673778 8.419556-3.527111 11.662222-3.413333 12.117333-3.185778 12.629333-1.536 6.485334-1.536 6.599111-2.901333 13.482667-2.787556 13.937777-2.56 14.336-2.446222 14.791111-2.730667 18.887112-2.503111 19.512888-0.910222 7.964445-1.706667 16.099555-1.536 16.497778-1.251555 16.725334-1.137778 17.009777-0.398222 8.590223-0.796445 17.408-0.341333 10.865777h233.301333l-0.341333-10.865777-0.739556-17.408-0.512-8.590223-1.080888-17.066666-1.251556-16.725334-1.536-16.384-1.706667-16.156444-0.910222-7.964445-0.967111-7.850666-2.048-15.473778-2.275556-15.132444-2.389333-14.734223-2.616889-14.336-2.730666-13.937777-2.901334-13.482667-1.536-6.599111-1.536-6.485334-3.185778-12.629333-3.413333-12.117333-3.527111-11.662222-2.730667-8.419556-2.844444-8.078222-3.811556-10.353778c-18.545778-47.900444-41.358222-77.198222-66.104889-79.985778l-5.916444-0.113778z m144.725333 29.923556l0.341334 0.568889c11.093333 24.462222 20.536889 52.224 28.444444 82.716444l2.901333 11.605334 3.299556 14.108444 1.536 7.168 1.479111 7.281778 2.844445 14.791111 1.422222 7.566222 2.56 15.303111 2.275555 15.587556 1.137778 7.964444 2.048 16.042667 0.910222 8.135111 1.763556 16.497778 1.536 16.782222 1.365333 17.009778 0.568889 8.533333 1.308445 21.788445 0.967111 22.186666 0.682666 21.048889h190.293334a405.674667 405.674667 0 0 0-249.685334-332.686222z" p-id="4446"></path></svg>

+ 1
- 0
src/assets/icons/svg/icon-one.svg Ver arquivo

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1753321605617" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6418" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M395.2 531.2c57.6 64 113.6 112 115.2 112h3.2c3.2 0 211.2-177.6 238.4-323.2 3.2-19.2 3.2-30.4 3.2-33.6 0-65.6-25.6-126.4-72-171.2C636.8 70.4 576 46.4 512 46.4c-64 0-126.4 25.6-171.2 68.8-48 46.4-73.6 107.2-73.6 172.8 0 3.2 0 14.4 3.2 32 4.8 54.4 46.4 126.4 124.8 211.2z m-57.6-236.8c-1.6-44.8 16-88 48-121.6 33.6-33.6 80-54.4 126.4-54.4 97.6 0 177.6 78.4 177.6 174.4 0 0 0 8-1.6 22.4-9.6 54.4-46.4 99.2-83.2 144-9.6 11.2-19.2 24-28.8 35.2l-1.6 1.6c-25.6 32-52.8 54.4-60.8 60.8-8-6.4-35.2-30.4-60.8-62.4l-1.6 1.6c-9.6-11.2-19.2-22.4-27.2-33.6-36.8-44.8-75.2-89.6-84.8-144-1.6-16-1.6-24-1.6-24z" p-id="6419"></path><path d="M512 417.6c62.4 0 112-48 112-108.8s-51.2-108.8-112-108.8-112 48-112 108.8 49.6 108.8 112 108.8z m-52.8-108.8c0-28.8 24-51.2 52.8-51.2 28.8 0 52.8 24 52.8 51.2 0 28.8-24 51.2-52.8 51.2-28.8 0-52.8-24-52.8-51.2zM291.2 806.4c32 16 67.2 32 107.2 38.4h1.6l1.6-1.6c0-4.8 1.6-11.2 1.6-16 1.6-8 1.6-17.6 3.2-24l3.2-20.8v-1.6l-1.6-1.6c-8-1.6-17.6-3.2-27.2-6.4-3.2-1.6-6.4-1.6-9.6-3.2-27.2-9.6-56-25.6-83.2-46.4h-3.2c-1.6 0-1.6 0-1.6 1.6-6.4 9.6-12.8 17.6-17.6 25.6-1.6 3.2-4.8 6.4-6.4 9.6l-11.2 16c-1.6 1.6 0 3.2 0 4.8 12.8 9.6 28.8 17.6 43.2 25.6z" p-id="6420"></path><path d="M977.6 339.2c-1.6-41.6-33.6-73.6-73.6-73.6H784c-1.6 0-3.2 1.6-3.2 3.2v60.8c0 1.6 1.6 3.2 3.2 3.2h65.6c54.4 0 57.6 22.4 57.6 57.6V496h-6.4c-32 0-91.2-1.6-129.6 17.6-1.6 0-1.6 1.6-1.6 3.2l4.8 14.4 16 43.2c0 1.6 1.6 3.2 3.2 1.6 24-8 46.4-12.8 60.8-16l52.8 3.2v289.6c1.6 24-1.6 41.6-12.8 51.2-12.8 11.2-32 8-40 8H171.2c-24 1.6-40-1.6-49.6-11.2-11.2-12.8-9.6-32-8-41.6V688c4.8 1.6 9.6 4.8 20.8 9.6 20.8 9.6 49.6 24 60.8 33.6 1.6 1.6 3.2 1.6 4.8 0 8-6.4 16-16 22.4-24l4.8-4.8c4.8-4.8 9.6-12.8 12.8-14.4 1.6-1.6 0-3.2 0-4.8-24-20.8-60.8-35.2-86.4-46.4-16-6.4-30.4-12.8-35.2-17.6V392 384c0-14.4 0-27.2 8-36.8 8-8 24-12.8 49.6-12.8h60.8c1.6 0 3.2-1.6 3.2-3.2v-60.8c0-1.6-1.6-3.2-3.2-3.2H118.4c-40 0-73.6 33.6-73.6 75.2v560c0 41.6 33.6 75.2 73.6 75.2h787.2c40 0 73.6-33.6 73.6-75.2l-1.6-563.2z" p-id="6421"></path><path d="M659.2 710.4c1.6 0 3.2-1.6 3.2-1.6 20.8-43.2 43.2-68.8 60.8-86.4 11.2-9.6 20.8-16 30.4-19.2 1.6 0 1.6-1.6 1.6-1.6v-3.2c-6.4-6.4-9.6-14.4-14.4-20.8-8-11.2-16-25.6-20.8-33.6-1.6-1.6-3.2-1.6-4.8-1.6-27.2 16-56 49.6-73.6 75.2-9.6 14.4-20.8 30.4-32 52.8 0 1.6 0 3.2 1.6 4.8l48 36.8c-1.6-1.6 0-1.6 0-1.6zM572.8 710.4c-1.6 0-1.6 0-1.6 1.6-4.8 4.8-11.2 9.6-19.2 16-3.2 1.6-4.8 3.2-8 4.8-20.8 12.8-48 28.8-89.6 41.6-1.6 0-1.6 1.6-1.6 1.6v3.2c3.2 8 4.8 14.4 6.4 20.8 1.6 6.4 3.2 12.8 6.4 17.6 1.6 8 3.2 12.8 4.8 19.2 0 1.6 0 1.6 1.6 1.6h3.2c22.4-6.4 38.4-12.8 54.4-19.2 41.6-17.6 72-36.8 89.6-57.6 1.6-1.6 1.6-3.2 0-4.8L576 710.4h-3.2z" p-id="6422"></path></svg>

+ 70
- 0
src/components/LangSelect/index.vue Ver arquivo

@@ -0,0 +1,70 @@
<template>
<el-dropdown trigger="click" class="lang-select" @command="handleSetLanguage">
<div>
<svg-icon class="lang-icon" icon-class="i18n" />
<span class="lang-text">{{ $t("language." + language) }}</span>
<i class="el-icon-arrow-down" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :disabled="language === 'zh'" command="zh">
{{ $t("language.zh") }}
</el-dropdown-item>
<el-dropdown-item :disabled="language === 'en'" command="en">
{{ $t("language.en") }}
</el-dropdown-item>
<el-dropdown-item :disabled="language === 'ja'" command="ja">
{{ $t("language.ja") }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>

<script>
import { mapGetters } from "vuex";

export default {
name: "LangSelect",
computed: {
...mapGetters(["language"]),
},
methods: {
/**
* 处理语言切换
* @param {string} language - 语言代码
*/
handleSetLanguage(language) {
this.$i18n.locale = language;
this.$store.dispatch("language/setLanguage", language);
this.$message({
message: this.$t("message.success"),
type: "success",
});
},
},
};
</script>

<style lang="scss" scoped>
.lang-select {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 10px;
color: #606266;
font-size: 14px;

&:hover {
color: #409eff;
}

.lang-icon {
margin-right: 6px;
font-size: 18px;
}

.lang-text {
margin-right: 4px;
font-size: 14px;
}
}
</style>

+ 58
- 0
src/i18n/index.js Ver arquivo

@@ -0,0 +1,58 @@
import Vue from "vue";
import VueI18n from "vue-i18n";
import Cookies from "js-cookie";
import elementEnLocale from "element-ui/lib/locale/lang/en"; // element-ui英文语言包
import elementZhLocale from "element-ui/lib/locale/lang/zh-CN"; // element-ui中文语言包
import elementJaLocale from "element-ui/lib/locale/lang/ja"; // element-ui日文语言包
import enLocale from "./lang/en";
import zhLocale from "./lang/zh";
import jaLocale from "./lang/ja";

Vue.use(VueI18n);

const messages = {
en: {
...enLocale,
...elementEnLocale,
},
zh: {
...zhLocale,
...elementZhLocale,
},
ja: {
...jaLocale,
...elementJaLocale,
},
};

/**
* 获取浏览器语言
* @returns {string} 语言代码
*/
export function getLanguage() {
const chooseLanguage = Cookies.get("language");
if (chooseLanguage) return chooseLanguage;

// 如果没有Cookie,则判断浏览器语言
const language = (
navigator.language || navigator.browserLanguage
).toLowerCase();
const locales = Object.keys(messages);
for (const locale of locales) {
if (language.indexOf(locale) > -1) {
return locale;
}
}
return "zh"; // 默认中文
}

const i18n = new VueI18n({
// 设置语言环境
locale: getLanguage(),
// 设置备用语言环境
fallbackLocale: "zh",
// 设置语言环境信息
messages,
});

export default i18n;

+ 249
- 0
src/i18n/lang/en.js Ver arquivo

@@ -0,0 +1,249 @@
export default {
// System common
system: {
title: "Digital Office Automation System",
name: "DOAS",
welcome: "Welcome to",
logout: "Logout",
login: "Login",
register: "Register",
home: "Home",
dashboard: "Dashboard",
profile: "Profile",
settings: "Settings",
about: "About Us",
},

// Navigation menu
navbar: {
dashboard: "Dashboard",
system: "System Management",
user: "User Management",
role: "Role Management",
dept: "Department Management",
menu: "Menu Management",
dict: "Dictionary Management",
config: "Config Management",
monitor: "System Monitor",
workflow: "Workflow",
oa: "Office Management",
tool: "System Tools",
},

// Common buttons
button: {
search: "Search",
reset: "Reset",
add: "Add",
edit: "Edit",
delete: "Delete",
save: "Save",
cancel: "Cancel",
confirm: "Confirm",
submit: "Submit",
export: "Export",
import: "Import",
refresh: "Refresh",
close: "Close",
view: "View",
detail: "Detail",
back: "Back",
next: "Next",
prev: "Previous",
finish: "Finish",
},

// Form labels
form: {
username: "Username",
password: "Password",
confirmPassword: "Confirm Password",
name: "Name",
email: "Email",
phone: "Phone",
status: "Status",
remark: "Remark",
createTime: "Create Time",
updateTime: "Update Time",
operation: "Operation",
required: "Required",
optional: "Optional",
},

menu: {
checkin: "Check in",
dashboard: "Dashboard",
profile: "Profile",
},

// Status text
status: {
enabled: "Enabled",
disabled: "Disabled",
normal: "Normal",
locked: "Locked",
deleted: "Deleted",
pending: "Pending",
processing: "Processing",
completed: "Completed",
cancelled: "Cancelled",
},

// Messages
message: {
success: "Operation successful",
error: "Operation failed",
warning: "Warning",
info: "Info",
confirm: "Confirm operation",
deleteConfirm: "Are you sure to delete?",
saveSuccess: "Save successful",
deleteSuccess: "Delete successful",
updateSuccess: "Update successful",
createSuccess: "Create successful",
loading: "Loading...",
noData: "No data",
selectData: "Please select data",
inputRequired: "Please input required field",
selectRequired: "Please select required field",
},

// Pagination
pagination: {
total: "Total {total} items",
page: "Page {page}",
pageSize: "{size} items per page",
goto: "Go to",
prev: "Previous",
next: "Next",
},

// Language selection
language: {
zh: "中文",
en: "English",
ja: "日本語",
switch: "Switch Language",
},

// Error pages
error: {
401: {
title: "401",
message: "Sorry, you do not have access to this page",
back: "Back to Home",
},
404: {
title: "404",
message: "Sorry, the page you visited does not exist",
back: "Back to Home",
},
500: {
title: "500",
message: "Sorry, internal server error",
back: "Back to Home",
},
},

login: {
title: "Digital Office Automation System",
username: "Username",
password: "Password",
rememberMe: "Remember Me",
forgetPassword: "Forget Password",
login: "Login",
register: "Register",
code: "Verification Code",
placeholder: {
username: "Please enter your username",
password: "Please enter your password",
code: "Please enter the verification code",
},
},

home: {
checkin: {
title: "Check in",
},
dept: {
noSet: "Not set department",
},
joinedDate: "Joined Date",
birthday: "{name}, {birthday} is your birthday, happy birthday!",
birthdayMonth: "Month",
birthdayDay: "Day",
},

checkin: {
workStartTime: "Start",
workEndTime: "End",
loading: "Loading attendance information...",
button: {
notChecked: "Check In",
checkedIn: "Check Out",
checkedOut: "Checked Out",
confirmCheck: "Confirm Check",
},
punch: {
status: {
notChecked: "Not Checked In",
checkedIn: "Checked In",
checkedOut: "Checked Out",
lateIn: "Late Check-In",
earlyOut: "Early Check-Out",
updateOut: "Updated Check",
error: "Check-In Failed",
todayChecked: "Today's check-in is complete",
success: "Check-In Success",
clockInTime: "Check-In Time",
clockInLocation: "Check-In Location",
},
remark: {
placeholder: "Please enter the remark information (optional)",
},
},
area: {
inRange: "In Range",
outRange: "Out Range",
},
error: {
outOfRange: "Please check in within the range",
submitFailed: "Submit failed",
},
},

warmMessage: {
message1: "Every day smile, the sun will be brighter, {name} !",
message2:
"Feel the beauty of life from the bottom of your heart, {name}, today will be a happy day!",
message3: "Believe in yourself, you are the best, {name}!",
message4:
"Feel the small happiness of every day, {name}, find the full happiness!",
message5: "Friendly actions spread good mood, {name}, stay happy!",
message6:
"In every small happiness, {name}, you can find the full happiness.",
message7: "Thankfulness makes happiness closer, {name}, keep going!",
message8:
"Be the best version of yourself, {name}, you deserve the best life.",
message9:
"Every day is a new beginning, {name}, seize the opportunity to chase your dreams.",
message10: "Smile is the brightest color in life, {name}, keep smiling!",
message11:
"The sun is in your heart, warmth never stops, {name}, you will always be warm.",
message12:
"Happiness is in your heart, {name}, your heart is full of infinite happiness.",
message13: "Every day's effort is not in vain, {name}, keep going!",
message14: "Use your creativity, {name}, create a better future.",
message15:
"Use kindness to infect the world, {name}, the world will be better because of you.",
message16:
"Face challenges with courage, {name}, you can overcome any obstacle.",
message17: "Enjoy life, {name}, life will be enjoyable because of you.",
message18:
"Don't forget to smile, {name}, because your smile can light up the world.",
message19: "No matter what, {name}, keep a positive attitude.",
message20: "Time flies, {name}, your beauty and age fly together.",
message21:
"Hope in your heart, {name}, the future is full of infinite possibilities.",
},
};

+ 253
- 0
src/i18n/lang/ja.js Ver arquivo

@@ -0,0 +1,253 @@
export default {
// システム共通
system: {
title: "デジタルオフィス自動化システム",
name: "DOAS",
welcome: "ようこそ",
logout: "ログアウト",
login: "ログイン",
register: "登録",
home: "ホーム",
dashboard: "ダッシュボード",
profile: "プロフィール",
settings: "システム設定",
about: "私たちについて",
},

// ナビゲーションメニュー
navbar: {
dashboard: "ホーム",
system: "システム管理",
user: "ユーザー管理",
role: "ロール管理",
dept: "部門管理",
menu: "メニュー管理",
dict: "辞書管理",
config: "パラメータ設定",
monitor: "システム監視",
workflow: "ワークフロー",
oa: "オフィス管理",
tool: "システムツール",
},

// 共通ボタン
button: {
search: "検索",
reset: "リセット",
add: "追加",
edit: "編集",
delete: "削除",
save: "保存",
cancel: "キャンセル",
confirm: "確認",
submit: "送信",
export: "エクスポート",
import: "インポート",
refresh: "更新",
close: "閉じる",
view: "表示",
detail: "詳細",
back: "戻る",
next: "次へ",
prev: "前へ",
finish: "完了",
},

// フォームラベル
form: {
username: "ユーザー名",
password: "パスワード",
confirmPassword: "パスワード確認",
name: "名前",
email: "メールアドレス",
phone: "電話番号",
status: "ステータス",
remark: "備考",
createTime: "作成時間",
updateTime: "更新時間",
operation: "操作",
required: "必須",
optional: "任意",
},

menu: {
checkin: "出勤退勤システム",
dashboard: "ダッシュボード",
profile: "プロフィール",
},

// ステータステキスト
status: {
enabled: "有効",
disabled: "無効",
normal: "正常",
locked: "ロック",
deleted: "削除済み",
pending: "保留中",
processing: "処理中",
completed: "完了",
cancelled: "キャンセル済み",
},

// メッセージ
message: {
success: "操作が成功しました",
error: "操作が失敗しました",
warning: "警告",
info: "情報",
confirm: "操作を確認",
deleteConfirm: "削除してもよろしいですか?",
saveSuccess: "保存が成功しました",
deleteSuccess: "削除が成功しました",
updateSuccess: "更新が成功しました",
createSuccess: "作成が成功しました",
loading: "読み込み中...",
noData: "データがありません",
selectData: "データを選択してください",
inputRequired: "必須項目を入力してください",
selectRequired: "必須項目を選択してください",
},

// ページネーション
pagination: {
total: "合計 {total} 件",
page: "ページ {page}",
pageSize: "1ページあたり {size} 件",
goto: "ジャンプ",
prev: "前のページ",
next: "次のページ",
},

// 言語選択
language: {
zh: "中文",
en: "English",
ja: "日本語",
switch: "言語切替",
},

// エラーページ
error: {
401: {
title: "401",
message: "申し訳ございません。このページにアクセスする権限がありません",
back: "ホームに戻る",
},
404: {
title: "404",
message: "申し訳ございません。お探しのページが見つかりません",
back: "ホームに戻る",
},
500: {
title: "500",
message: "申し訳ございません。サーバー内部エラーが発生しました",
back: "ホームに戻る",
},
},

login: {
title: "Digital Office Automation System",
username: "ユーザー名",
password: "パスワード",
rememberMe: "ログイン情報を記憶する",
forgetPassword: "パスワードを忘れた場合",
login: "ログイン",
register: "登録",
code: "検証コード",
placeholder: {
username: "ユーザー名を入力してください",
password: "パスワードを入力してください",
code: "検証コードを入力してください",
},
},

home: {
checkin: {
title: "出勤退勤システム",
},
dept: {
noSet: "未設定部門",
},
joinedDate: "入社日",
birthday:
"{name}、{birthday}はあなたの誕生日です。お誕生日おめでとうございます!",
birthdayMonth: "月",
birthdayDay: "日",
},

checkin: {
workStartTime: "出勤",
workEndTime: "退勤",
loading: "考勤情報を読み込んでいます...",
button: {
notChecked: "出勤打刻",
checkedIn: "退勤打刻",
checkedOut: "打刻済み",
confirmCheck: "打刻確認",
},
punch: {
status: {
notChecked: "未打刻",
checkedIn: "打刻済み",
checkedOut: "打刻済み",
lateIn: "遅刻打刻",
earlyOut: "早退打刻",
updateOut: "打刻更新",
error: "打刻失敗",
todayChecked: "今日の打刻は完了しました",
success: "打刻成功",
clockInTime: "打刻時間",
clockInLocation: "打刻地点",
},
remark: {
placeholder: "備考を入力してください(任意)",
},
},
area: {
inRange: "打刻範囲内",
outRange: "打刻範囲外",
},
error: {
outOfRange: "打刻範囲外です。打刻範囲内に入ってから打刻してください。",
submitFailed: "送信失敗",
},
},

warmMessage: {
message1:
"毎日笑顔をあげると、陽光はより輝きます。{name}、頑張ってください!",
message2:
"心の底から生活の美を感じて、{name}、今日も楽しい日になりますように。",
message3: "自分を信じて、あなたは最も優れた存在です。{name}!",
message4:
"毎日の小さな喜びを感じて、{name}、満ち満ちた幸せを見つけてください。",
message5: "友好的な行動は良い気分を伝えます。{name}、元気でいてください!",
message6:
"毎日の小さな喜びを感じて、{name}、満ち満ちた幸せを見つけてください。",
message7:
"心からの感謝は、幸せをより近づけます。{name}、頑張ってください!",
message8:
"最善の自分を目指して、{name}、あなたは最も美しい生活を手に入れることができます。",
message9:
"新しい始まりは、{name}、夢を追いかけるチャンスを捉えてください。",
message10: "笑顔は生活の最も輝かしい色です。{name}、元気でいてください!",
message11:
"陽光は心の中にあり、暖かさは止まりません。{name}、あなたはいつまでも暖かいままです。",
message12:
"幸せは心の中にあります。{name}、あなたの心は無限の幸せを持っています。",
message13: "毎日の努力は無駄になりません。{name}、頑張ってください!",
message14:
"あなたの創造力を発揮して、{name}、未来をより美しくしてください。",
message15:
"あなたの善行は世界をより良くします。{name}、世界はあなたによってより良くなります。",
message16:
"勇気を持って挑戦して、{name}、あなたはどんな障害も克服できます。",
message17:
"生活を楽しむ、{name}、生活はあなたによって楽しいものになります。",
message18:
"笑顔は世界を輝かせます。{name}、あなたの笑顔は世界を輝かせます。",
message19: "どんな困難があっても、{name}、楽観的な態度を持ってください。",
message20: "時間は流れ、{name}、あなたの美しさと年齢は共に進みます。",
message21: "希望を持って、{name}、未来は無限の可能性を持っています。",
},
};

+ 238
- 0
src/i18n/lang/zh.js Ver arquivo

@@ -0,0 +1,238 @@
export default {
// 系统通用
system: {
title: "数字化办公自动化系统",
name: "DOAS",
welcome: "欢迎使用",
logout: "退出登录",
login: "登录",
register: "注册",
home: "首页",
dashboard: "工作台",
profile: "个人资料",
settings: "系统设置",
about: "关于我们",
},

// 导航菜单
navbar: {
dashboard: "首页",
system: "系统管理",
user: "用户管理",
role: "角色管理",
dept: "部门管理",
menu: "菜单管理",
dict: "字典管理",
config: "参数设置",
monitor: "系统监控",
workflow: "工作流程",
oa: "办公管理",
tool: "系统工具",
},

// 通用按钮
button: {
search: "搜索",
reset: "重置",
add: "新增",
edit: "修改",
delete: "删除",
save: "保存",
cancel: "取消",
confirm: "确定",
submit: "提交",
export: "导出",
import: "导入",
refresh: "刷新",
close: "关闭",
view: "查看",
detail: "详情",
back: "返回",
next: "下一步",
prev: "上一步",
finish: "完成",
},

// 表单标签
form: {
username: "用户名",
password: "密码",
confirmPassword: "确认密码",
name: "姓名",
email: "邮箱",
phone: "手机号",
status: "状态",
remark: "备注",
createTime: "创建时间",
updateTime: "更新时间",
operation: "操作",
required: "必填",
optional: "选填",
},

menu: {
checkin: "考勤打卡",
dashboard: "首页",
profile: "个人中心",
},

// 状态文本
status: {
enabled: "启用",
disabled: "禁用",
normal: "正常",
locked: "锁定",
deleted: "已删除",
pending: "待处理",
processing: "处理中",
completed: "已完成",
cancelled: "已取消",
},

// 提示信息
message: {
success: "操作成功",
error: "操作失败",
warning: "警告",
info: "提示",
confirm: "确认操作",
deleteConfirm: "确定要删除吗?",
saveSuccess: "保存成功",
deleteSuccess: "删除成功",
updateSuccess: "更新成功",
createSuccess: "创建成功",
loading: "加载中...",
noData: "暂无数据",
selectData: "请选择数据",
inputRequired: "请输入必填项",
selectRequired: "请选择必填项",
},

// 分页
pagination: {
total: "共 {total} 条",
page: "第 {page} 页",
pageSize: "每页 {size} 条",
goto: "跳转到",
prev: "上一页",
next: "下一页",
},

// 语言选择
language: {
zh: "中文",
en: "English",
ja: "日本語",
switch: "切换语言",
},

// 错误页面
error: {
401: {
title: "401",
message: "抱歉,您无权访问该页面",
back: "返回首页",
},
404: {
title: "404",
message: "抱歉,您访问的页面不存在",
back: "返回首页",
},
500: {
title: "500",
message: "抱歉,服务器内部错误",
back: "返回首页",
},
},

login: {
title: "Digital Office Automation System",
username: "用户名",
password: "密码",
rememberMe: "记住密码",
forgetPassword: "忘记密码",
login: "登录",
register: "注册",
code: "验证码",
placeholder: {
username: "请输入用户名",
password: "请输入密码",
code: "请输入验证码",
},
},

home: {
checkin: {
title: "考勤打卡",
},
dept: {
noSet: "未设置部门",
},
joinedDate: "入职时间",
birthday: "{name},{birthday}是你的生日,祝你生日快乐!",
birthdayMonth: "月",
birthdayDay: "日",
},

checkin: {
workStartTime: "上班",
workEndTime: "下班",
loading: "正在加载考勤信息...",
button: {
notChecked: "上班打卡",
checkedIn: "下班打卡",
checkedOut: "已打卡",
confirmCheck: "确认打卡",
},
punch: {
status: {
notChecked: "未打卡",
checkedIn: "已打卡",
checkedOut: "已打卡",
lateIn: "迟到打卡",
earlyOut: "早退打卡",
updateOut: "更新打卡",
error: "打卡失败",
todayChecked: "今日已完成打卡",
success: "打卡成功",
clockInTime: "打卡时间",
clockInLocation: "打卡地点",
},
remark: {
placeholder: "请输入备注信息(选填)",
},
},
area: {
inRange: "已进入打卡范围",
outRange: "未进入打卡范围",
},
error: {
outOfRange: "请在打卡范围内进行打卡",
submitFailed: "提交失败",
},
},

warmMessage: {
message1: "每天微笑,阳光会更灿烂,{name}加油!",
message2: "用心感受生活的美好,{name},今天也要快乐哦!",
message3: "坚持自己,你是最棒的,{name}!",
message4: "愿每个瞬间都充满温馨,{name},加油!",
message5: "友善的举止传递好心情,{name},保持开心!",
message6: "在每个小小的喜悦中,{name},都能找到满满的幸福。",
message7: "心怀感激,幸福会更加靠近,{name},加油!",
message8: "做最好的自己,{name},你值得拥有最美好的生活。",
message9: "每一天都是新的开始,{name},抓住机会追寻梦想。",
message10: "微笑是生活中最灿烂的颜色,{name},请保持灿烂!",
message11: "阳光洒在心间,温暖不止,{name},愿你永远温暖如初。",
message12: "快乐源于内心,{name},愿你的内心充满无限快乐。",
message13: "每一份努力都不会白费,{name},坚持就是胜利。",
message14: "发挥你的创造力,{name},创造美好的未来。",
message15: "用善良去感染世界,{name},世界会因你而更美好。",
message16: "勇敢面对挑战,{name},你就是不可阻挡的力量。",
message17: "热爱生活,{name},生活也会热爱你。",
message18: "别忘了微笑,{name},因为你的笑容能够点亮整个世界。",
message19: "不论遇到什么,{name},都要保持乐观的心态。",
message20: "时光荏苒,{name},愿你的美好与年华齐飞。",
message21: "心怀希望,{name},未来充满无限可能。",
},
};

+ 3
- 1
src/layout/components/Navbar.vue Ver arquivo

@@ -22,6 +22,8 @@
<el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>

<lang-select id="lang-select" class="right-menu-item hover-effect" />
</template>

<el-dropdown
@@ -185,7 +187,7 @@ export default {
border-radius: 24px;
}

span{
span {
font-size: 14px;
}


+ 3
- 2
src/layout/components/Sidebar/Item.vue Ver arquivo

@@ -1,4 +1,5 @@
<script>
import i18n from '@/i18n'
export default {
name: 'MenuItem',
functional: true,
@@ -22,9 +23,9 @@ export default {

if (title) {
if (title.length > 5) {
vnodes.push(<span slot='title' title={(title)}>{(title)}</span>)
vnodes.push(<span slot='title' title={(i18n.t(`${title}`))}>{(i18n.t(`${title}`))}</span>)
} else {
vnodes.push(<span slot='title'>{(title)}</span>)
vnodes.push(<span slot='title'>{(i18n.t(`${title}`))}</span>)
}
}
return vnodes

+ 1
- 1
src/layout/components/TagsView/index.vue Ver arquivo

@@ -16,7 +16,7 @@
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent.native="openMenu(tag, $event)"
>
{{ tag.title }}
{{ $t(tag.title) }}
<span
v-if="isActive(tag)"
class="el-icon-refresh-right"

+ 10
- 1
src/main.js Ver arquivo

@@ -3,6 +3,7 @@ import Vue from 'vue'
import Cookies from 'js-cookie'

import Element from 'element-ui'
import locale from 'element-ui/lib/locale' // Element UI 语言配置
import './assets/styles/element-variables.scss'

import '@/assets/styles/index.scss' // global css
@@ -15,6 +16,9 @@ import plugins from './plugins' // plugins
import { download } from '@/utils/request'
import Toast from '@/utils/toast' // 移动端Toast组件

// 国际化配置
import i18n from './i18n' // 多语言配置

import './assets/icons' // icon
import './permission' // permission control
import { getDicts } from '@/api/system/dict/data'
@@ -51,6 +55,8 @@ import DictData from '@/components/DictData'
import FormDesigner from '@/components/FormDesigner/index'
// 通用容器组件
import AppContainer from '@/components/AppContainer'
// 语言选择组件
import LangSelect from '@/components/LangSelect'

// 全局方法挂载
Vue.prototype.getDicts = getDicts
@@ -74,6 +80,7 @@ Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)
Vue.component('Layout', AppContainer)
Vue.component('LangSelect', LangSelect)

Vue.use(directive)
Vue.use(plugins)
@@ -95,7 +102,8 @@ DictData.install()
Element.Dialog.props.closeOnClickModal.default = false

Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
size: Cookies.get('size') || 'medium', // set element-ui default size
i18n: (key, value) => i18n.t(key, value) // Element UI 多语言支持
})

Vue.config.productionTip = false
@@ -104,5 +112,6 @@ new Vue({
el: '#app',
router,
store,
i18n,
render: (h) => h(App)
})

+ 4
- 3
src/router/index.js Ver arquivo

@@ -1,5 +1,6 @@
import Vue from "vue";
import Router from "vue-router";
import i18n from "@/i18n";

Vue.use(Router);

@@ -70,7 +71,7 @@ export const constantRoutes = [
path: "index",
component: () => import("@/views/index"),
name: "Index",
meta: { title: "首页", icon: "dashboard", affix: true },
meta: { title: "menu.dashboard", icon: "dashboard", affix: true },
},
],
},
@@ -84,14 +85,14 @@ export const constantRoutes = [
path: "profile",
component: () => import("@/views/system/user/profile/index"),
name: "Profile",
meta: { title: "个人中心", icon: "user" },
meta: { title: "menu.profile", icon: "user" },
},
],
},
{
path: "/m/checkin",
component: () => import("@/views/oa/attendance/checkin"),
meta: { title: "考勤打卡", icon: "date" },
meta: { title: "menu.checkin", icon: "date" },
},
];


+ 2
- 1
src/store/getters.js Ver arquivo

@@ -16,6 +16,7 @@ const getters = {
permission_routes: (state) => state.permission.routes,
topbarRouters: (state) => state.permission.topbarRouters,
defaultRoutes: (state) => state.permission.defaultRoutes,
sidebarRouters: (state) => state.permission.sidebarRouters
sidebarRouters: (state) => state.permission.sidebarRouters,
language: (state) => state.language.language
}
export default getters

+ 3
- 1
src/store/index.js Ver arquivo

@@ -7,6 +7,7 @@ import tagsView from './modules/tagsView'
import permission from './modules/permission'
import settings from './modules/settings'
import message from './modules/message'
import language from './modules/language'
import getters from './getters'

Vue.use(Vuex)
@@ -19,7 +20,8 @@ const store = new Vuex.Store({
tagsView,
permission,
settings,
message
message,
language
},
getters
})

+ 36
- 0
src/store/modules/language.js Ver arquivo

@@ -0,0 +1,36 @@
import Cookies from 'js-cookie'
import { getLanguage } from '@/i18n'

const state = {
language: getLanguage()
}

const mutations = {
/**
* 设置语言
* @param {Object} state - 状态对象
* @param {string} language - 语言代码
*/
SET_LANGUAGE: (state, language) => {
state.language = language
Cookies.set('language', language)
}
}

const actions = {
/**
* 设置语言
* @param {Object} context - 上下文对象
* @param {string} language - 语言代码
*/
setLanguage({ commit }, language) {
commit('SET_LANGUAGE', language)
}
}

export default {
namespaced: true,
state,
mutations,
actions
}

+ 9
- 26
src/utils/warmMessageGenerator.js Ver arquivo

@@ -1,31 +1,14 @@
const WarmMessageGenerator = (name) => {
const messages = [
'每天微笑,阳光会更灿烂,{name}加油!',
'用心感受生活的美好,{name},今天也要快乐哦!',
'坚持自己,你是最棒的,{name}!',
'愿每个瞬间都充满温馨,{name},加油!',
'友善的举止传递好心情,{name},保持开心!',
'在每个小小的喜悦中,{name},都能找到满满的幸福。',
'心怀感激,幸福会更加靠近,{name},加油!',
'做最好的自己,{name},你值得拥有最美好的生活。',
'每一天都是新的开始,{name},抓住机会追寻梦想。',
'微笑是生活中最灿烂的颜色,{name},请保持灿烂!',
'阳光洒在心间,温暖不止,{name},愿你永远温暖如初。',
'快乐源于内心,{name},愿你的内心充满无限快乐。',
'每一份努力都不会白费,{name},坚持就是胜利。',
'发挥你的创造力,{name},创造美好的未来。',
'用善良去感染世界,{name},世界会因你而更美好。',
'勇敢面对挑战,{name},你就是不可阻挡的力量。',
'热爱生活,{name},生活也会热爱你。',
'别忘了微笑,{name},因为你的笑容能够点亮整个世界。',
'不论遇到什么,{name},都要保持乐观的心态。',
'时光荏苒,{name},愿你的美好与年华齐飞。',
'心怀希望,{name},未来充满无限可能。'
]
import i18n from '@/i18n'

/**
* 生成随机暖心的消息
* @param {string} name - 用户名
* @returns {string} 随机暖心的消息
*/
const WarmMessageGenerator = (name) => {
const getRandomMessage = () => {
const randomIndex = Math.floor(Math.random() * messages.length)
return messages[randomIndex].replace('{name}', name)
const randomIndex = Math.floor(Math.random() * 20)
return i18n.t(`warmMessage.message${randomIndex + 1}`, { name })
}

const generateMessage = () => {

+ 261
- 0
src/views/demo/i18n/index.vue Ver arquivo

@@ -0,0 +1,261 @@
<template>
<div class="app-container">
<div class="i18n-demo">
<el-card>
<div slot="header" class="clearfix">
<span>{{ $t('system.title') }} - 多语言示例</span>
<div style="float: right">
<lang-select></lang-select>
</div>
</div>

<!-- 基础用法示例 -->
<el-row :gutter="20">
<el-col :span="12">
<h3>{{ $t('button.search') }}功能演示</h3>
<el-form :model="form" :rules="rules" ref="form" label-width="120px" class="demo-form">
<el-form-item :label="$t('form.username')" prop="username">
<el-input
v-model="form.username"
:placeholder="$t('form.username')"
></el-input>
</el-form-item>
<el-form-item :label="$t('form.email')" prop="email">
<el-input
v-model="form.email"
:placeholder="$t('form.email')"
></el-input>
</el-form-item>
<el-form-item :label="$t('form.status')" prop="status">
<el-select v-model="form.status" :placeholder="$t('message.selectRequired')">
<el-option
:label="$t('status.enabled')"
value="enabled"
></el-option>
<el-option
:label="$t('status.disabled')"
value="disabled"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">{{ $t('button.submit') }}</el-button>
<el-button @click="resetForm">{{ $t('button.reset') }}</el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12">
<h3>{{ $t('message.info') }}提示示例</h3>
<div class="button-group">
<el-button @click="showSuccess">{{ $t('message.success') }}</el-button>
<el-button @click="showWarning">{{ $t('message.warning') }}</el-button>
<el-button @click="showError">{{ $t('message.error') }}</el-button>
<el-button @click="showInfo">{{ $t('message.info') }}</el-button>
</div>
<h3>{{ $t('pagination.total') }}示例</h3>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-col>
</el-row>

<!-- 使用示例代码 -->
<el-divider content-position="left">使用示例代码</el-divider>
<div class="code-example">
<h4>1. 基础使用</h4>
<pre><code>{{ basicUsage }}</code></pre>
<h4>2. 带参数的翻译</h4>
<pre><code>{{ paramUsage }}</code></pre>
<h4>3. 在 JavaScript 中使用</h4>
<pre><code>{{ jsUsage }}</code></pre>
</div>
</el-card>
</div>
</div>
</template>

<script>
export default {
name: 'I18nDemo',
data() {
return {
form: {
username: '',
email: '',
status: ''
},
rules: {
username: [
{ required: true, message: this.$t('message.inputRequired'), trigger: 'blur' }
],
email: [
{ required: true, message: this.$t('message.inputRequired'), trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
],
status: [
{ required: true, message: this.$t('message.selectRequired'), trigger: 'change' }
]
},
currentPage: 1,
pageSize: 10,
total: 400,
basicUsage: `<!-- 模板中使用 -->
<span>{{ $t('system.title') }}</span>
<el-button>{{ $t('button.save') }}</el-button>

<!-- 属性中使用 -->
<el-input :placeholder="$t('form.username')"></el-input>`,
paramUsage: `<!-- 带参数的翻译 -->
<span>{{ $t('pagination.total', { total: 100 }) }}</span>
<span>{{ $t('pagination.page', { page: 2 }) }}</span>

// 语言包中定义
{
pagination: {
total: '共 {total} 条', // 中文
total: 'Total {total} items' // 英文
}
}`,
jsUsage: `// 在 methods 中使用
methods: {
showMessage() {
this.$message.success(this.$t('message.success'))
},
// 表单验证中使用
rules: {
username: [
{ required: true, message: this.$t('message.inputRequired'), trigger: 'blur' }
]
}
}`
}
},
methods: {
/**
* 提交表单
*/
submitForm() {
this.$refs.form.validate((valid) => {
if (valid) {
this.$message.success(this.$t('message.saveSuccess'))
}
})
},

/**
* 重置表单
*/
resetForm() {
this.$refs.form.resetFields()
this.$message.info(this.$t('button.reset') + this.$t('message.success'))
},

/**
* 显示成功消息
*/
showSuccess() {
this.$message.success(this.$t('message.success'))
},

/**
* 显示警告消息
*/
showWarning() {
this.$message.warning(this.$t('message.warning'))
},

/**
* 显示错误消息
*/
showError() {
this.$message.error(this.$t('message.error'))
},

/**
* 显示信息消息
*/
showInfo() {
this.$message.info(this.$t('message.info'))
},

/**
* 处理页面大小变化
* @param {number} val - 新的页面大小
*/
handleSizeChange(val) {
this.pageSize = val
console.log(`每页 ${val} 条`)
},

/**
* 处理当前页变化
* @param {number} val - 新的当前页
*/
handleCurrentChange(val) {
this.currentPage = val
console.log(`当前页: ${val}`)
}
}
}
</script>

<style lang="scss" scoped>
.i18n-demo {
.demo-form {
margin-top: 20px;
}

.button-group {
margin-bottom: 20px;
.el-button {
margin-right: 10px;
margin-bottom: 10px;
}
}

.code-example {
background-color: #f5f5f5;
padding: 20px;
border-radius: 4px;
h4 {
margin-top: 20px;
margin-bottom: 10px;
color: #409eff;
&:first-child {
margin-top: 0;
}
}
pre {
background-color: #263238;
color: #eeffff;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.5;
}
}
}
}
</style>

+ 56
- 14
src/views/index.vue Ver arquivo

@@ -11,12 +11,17 @@
<el-divider direction="vertical" />
<span>{{ userinfo.userName }}</span>
<el-divider direction="vertical" />
<span>{{ userinfo.dept.deptName || '未设置部门' }}</span>
<el-divider direction="vertical" />
<span>{{ userinfo.postGroup || '未设置岗位' }}</span>
<span>{{ userinfo.dept.deptName || $t('home.dept.noSet') }}</span>
<template v-if="userinfo.joinedDate">
<el-divider direction="vertical" />
<span>{{ $t('home.joinedDate') }}:{{ userinfo.joinedDate }} </span>
</template>
</p>
<p v-if="hasBirthday" class="birthday-message">
🎂{{ birthdayMessage }}🎉🎁🎈
</p>
</div>
<div class="count" v-show="false">
<div v-show="false" class="count">
<el-statistic
:value="0"
:value-style="{ fontSize: '32px' }"
@@ -33,7 +38,8 @@
<section>
<div class="apps">
<router-link to="/m/checkin" class="app-card">
<span>考勤打卡</span>
<svg-icon icon-class="icon-one" style="font-size: 24px" />
<span>{{ $t('home.checkin.title') }}</span>
<i class="el-icon-arrow-right" />
</router-link>
</div>
@@ -44,6 +50,7 @@
<script>
import { mapGetters } from 'vuex'
import messageGenerator from '@/utils/warmMessageGenerator'
import moment from 'moment'
export default {
name: 'Index',
data() {
@@ -52,13 +59,43 @@ export default {
}
},
computed: {
...mapGetters(['userinfo', 'avatar', 'nickname'])
...mapGetters(['userinfo', 'avatar', 'nickname', 'language']),
hasBirthday() {
if (!this.userinfo.birthday) return false
// this.userinfo.birthday 格式为 2025-07-24, 比对月、日
const today = new Date().toISOString().split('T')[0]
const birthday = this.userinfo.birthday.split('-')

const month = birthday[1]
const day = birthday[2]

const todayMonth = today.split('-')[1]
const todayDay = today.split('-')[2]

console.log(month, todayMonth)

return month == todayMonth && day == todayDay
},
birthdayMessage() {
if (!this.userinfo.birthday) return ''
return this.$t('home.birthday', {
name: this.nickname,
birthday: moment(this.userinfo.birthday).format(`M/D`)
})
}
},
created() {
this.warmMessage = messageGenerator(
this.userinfo.nickName
).generateMessage()
watch: {
language: {
handler(newVal) {
console.log(newVal)
this.warmMessage = messageGenerator(
this.userinfo.nickName
).generateMessage()
},
immediate: true
}
},
created() {},
methods: {
goTarget(href) {
window.open(href, '_blank')
@@ -121,9 +158,14 @@ export default {
padding: 16px;
}
}
.birthday-message {
margin: 10px 0 0 0 !important;
overflow: hidden !important;
color: #e6a23c;
}
.apps {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-columns: repeat(5, 1fr);

@media (max-width: 1024px) {
grid-template-columns: repeat(4, 1fr);
@@ -139,15 +181,15 @@ export default {

gap: 16px;
.app-card {
background-color: #fff;
background: linear-gradient(to top, #fff, #f1f1f1);
border-radius: 8px;
padding: 16px;
padding: 30px 16px;
display: flex;
align-items: center;
justify-content: space-between;
color: #333;
text-decoration: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
border: 1px solid #e0e0e0;
span {
font-size: 16px;
font-weight: 600;

+ 82
- 63
src/views/login.vue Ver arquivo

@@ -6,13 +6,15 @@
:rules="loginRules"
class="login-form"
>
<h3 class="title">Digital Office Automation System</h3>
<h3 class="title">{{ $t('login.title') }}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
:placeholder="$t('login.username')"
>
<i slot="prefix" class="el-input__icon input-icon el-icon-user" />
</el-input>
@@ -22,7 +24,7 @@
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
:placeholder="$t('login.password')"
@keyup.enter.native="handleLogin"
>
<i slot="prefix" class="el-input__icon input-icon el-icon-lock" />
@@ -32,7 +34,7 @@
<el-input
v-model="loginForm.code"
auto-complete="off"
placeholder="验证码"
:placeholder="$t('login.code')"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
@@ -43,13 +45,16 @@
/>
</el-input>
<div class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
</el-form-item>
<el-checkbox
v-model="loginForm.rememberMe"
style="margin: 0px 0px 25px 0px"
>记住密码</el-checkbox>
>{{ $t('login.rememberMe') }}</el-checkbox
>

<el-form-item style="width: 100%">
<el-button
:loading="loading"
@@ -58,130 +63,144 @@
style="width: 100%"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
<span v-if="!loading">{{ $t('login.login') }}</span>
<span v-else>{{ $t('login.login') }}...</span>
</el-button>
<div v-if="register" style="float: right">
<router-link
class="link-type"
:to="'/register'"
>立即注册</router-link>
<router-link class="link-type" :to="'/register'"
>{{ $t('login.register') }}</router-link
>
</div>
</el-form-item>
</el-form>

<div class="lang-select">
<LangSelect />
</div>
</div>
</template>

<script>
import { getCodeImg } from '@/api/login'
import Cookies from 'js-cookie'
import { encrypt, decrypt } from '@/utils/jsencrypt'
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import LangSelect from "@/components/LangSelect";

export default {
name: 'Login',
name: "Login",
components: {
LangSelect,
},
data() {
return {
codeUrl: '',
codeUrl: "",
loginForm: {
username: '', // admin
password: '', // admin123
username: "", // admin
password: "", // admin123
rememberMe: false,
code: '',
uuid: ''
code: "",
uuid: "",
},
loginRules: {
username: [
{ required: true, trigger: 'blur', message: '请输入您的账号' }
{ required: true, trigger: "blur", message: this.$t('login.placeholder.username') },
],
password: [
{ required: true, trigger: 'blur', message: '请输入您的密码' }
{ required: true, trigger: "blur", message: this.$t('login.placeholder.password') },
],
code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
code: [{ required: true, trigger: "change", message: this.$t('login.placeholder.code') }],
},
loading: false,
// 验证码开关
captchaEnabled: true,
// 注册开关
register: false,
redirect: undefined
}
redirect: undefined,
};
},
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
handler: function (route) {
this.redirect = route.query && route.query.redirect;
},
immediate: true
}
immediate: true,
},
},
created() {
this.getCode()
this.getCookie()
this.getCode();
this.getCookie();
},
methods: {
getCode() {
getCodeImg().then((res) => {
this.captchaEnabled =
res.data.captchaEnabled === undefined ? true : res.data.captchaEnabled
res.data.captchaEnabled === undefined
? true
: res.data.captchaEnabled;
if (this.captchaEnabled) {
this.codeUrl = 'data:image/gif;base64,' + res.data.img
this.loginForm.uuid = res.data.uuid
this.codeUrl = "data:image/gif;base64," + res.data.img;
this.loginForm.uuid = res.data.uuid;
}
})
});
},
getCookie() {
const username = Cookies.get('username')
const password = Cookies.get('password')
const rememberMe = Cookies.get('rememberMe')
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
this.loginForm = {
username: username === undefined ? this.loginForm.username : username,
password:
password === undefined ? this.loginForm.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
};
},
handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set('username', this.loginForm.username, { expires: 30 })
Cookies.set('password', encrypt(this.loginForm.password), {
expires: 30
})
Cookies.set('rememberMe', this.loginForm.rememberMe, {
expires: 30
})
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), {
expires: 30,
});
Cookies.set("rememberMe", this.loginForm.rememberMe, {
expires: 30,
});
} else {
Cookies.remove('username')
Cookies.remove('password')
Cookies.remove('rememberMe')
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
this.$store
.dispatch('Login', this.loginForm)
.dispatch("Login", this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || '/' }).catch(() => {})
this.$router.push({ path: this.redirect || "/" }).catch(() => {});
})
.catch(() => {
this.loading = false
this.loading = false;
if (this.captchaEnabled) {
this.getCode()
this.getCode();
}
})
});
}
})
}
}
}
});
},
},
};
</script>

<style rel="stylesheet/scss" lang="scss">
<style rel="stylesheet/scss" lang="scss" scoped>
.lang-select{
position: fixed;
right: 20px;
top: 20px;
}
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url('../assets/images/login-background.svg');
background-image: url("../assets/images/login-background.svg");
background-size: cover;
}
.title {

+ 14
- 16
src/views/oa/attendance/checkin/components/LocationInfo.vue Ver arquivo

@@ -5,42 +5,40 @@
<div class="location-text">
<em :class="isInRange ? 'text-success' : 'text-error'">
<i :class="isInRange ? 'el-icon-success' : 'el-icon-error'" />
{{ isInRange ? '已进入打卡范围' : '未进入打卡范围' }}
<!-- {{ isInRange ? "已进入打卡范围" : "未进入打卡范围" }} -->
{{ isInRange ? $t("checkin.area.inRange") : $t("checkin.area.outRange") }}
</em>
<span>{{ areaName }}</span>
</div>
</div>

<!-- 调试信息 -->
<p class="coordinate-info">
经度:{{ longitude }} <br>
纬度:{{ latitude }}
</p>
<p class="coordinate-info">{{ longitude }},{{ latitude }}</p>
</div>
</template>

<script>
export default {
name: 'LocationInfo',
name: "LocationInfo",
props: {
isInRange: {
type: Boolean,
default: false
default: false,
},
areaName: {
type: String,
default: ''
default: "",
},
longitude: {
type: Number,
default: null
default: null,
},
latitude: {
type: Number,
default: null
}
}
}
default: null,
},
},
};
</script>

<style lang="scss" scoped>
@@ -71,7 +69,7 @@ export default {
flex-direction: column;
gap: 6px;
align-items: center;
em {
font-style: normal;
display: flex;
@@ -79,7 +77,7 @@ export default {
justify-content: center;
gap: 4px;
font-weight: 600;
i {
font-size: 20px;
}
@@ -92,4 +90,4 @@ export default {
color: #999;
margin: 10px 0;
}
</style>
</style>

+ 3
- 2
src/views/oa/attendance/checkin/components/RemarkDialog.vue Ver arquivo

@@ -12,7 +12,7 @@
<textarea
:value="remark"
@input="$emit('update:remark', $event.target.value)"
placeholder="请输入备注信息(选填)"
:placeholder="$t('checkin.punch.remark.placeholder')"
maxlength="200"
rows="4"
/>
@@ -21,7 +21,8 @@
</div>
<div class="remark-footer">
<button class="remark-submit" @click="$emit('submit')">
确认打卡
<!-- 确认打卡 -->
{{ $t("checkin.button.confirmCheck") }}
</button>
</div>
</div>

+ 12
- 3
src/views/oa/attendance/checkin/components/SuccessDialog.vue Ver arquivo

@@ -8,14 +8,23 @@
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
</svg>
</div>
<div class="success-title">打卡成功</div>
<div class="success-title">
<!-- 打卡成功 -->
{{ $t("checkin.punch.status.success") }}
</div>
<div class="success-info">
<div class="info-item">
<span class="label">打卡时间</span>
<span class="label">
<!-- 打卡时间 -->
{{ $t("checkin.punch.status.clockInTime") }}
</span>
<span class="value">{{ punchTime }}</span>
</div>
<div class="info-item">
<span class="label">打卡地点</span>
<span class="label">
<!-- 打卡地点 -->
{{ $t("checkin.punch.status.clockInLocation") }}
</span>
<span class="value">{{ location }}</span>
</div>
</div>

+ 232
- 131
src/views/oa/attendance/checkin/index.vue Ver arquivo

@@ -9,18 +9,27 @@
-->
<template>
<div class="page">
<div v-if="attendanceGroup.attendanceGroupName" class="container">
<div
v-if="attendanceGroup.attendanceGroupName"
class="container"
:class="{ 'has-birthday': hasBirthday }"
>
<!-- 用户信息头部 -->
<div class="header">
<div class="user-info">
<div class="user-left">
<div class="avatar">
<el-avatar>{{ userinfo.nickName }}</el-avatar>
<el-avatar :src="userinfo.avatar">{{
userinfo.nickName
}}</el-avatar>
</div>
<div class="user-name">{{ userinfo.nickName }}</div>
</div>
<div class="user-right">
<div class="user-department">{{ userinfo.dept.deptName }}</div>
<div class="user-department">
{{ userinfo.dept.deptName
}}<em v-if="userinfo.language">({{ userinfo.language }})</em>
</div>
</div>
</div>

@@ -28,21 +37,31 @@
<div class="status-cards">
<div class="status-card">
<div class="status-time">
上班{{ attendanceGroup.workStartTime }}
<!-- 上班 -->
{{ $t("checkin.workStartTime") }}
{{ attendanceGroup.workStartTime }}
</div>
<div class="status-label">
<span>{{ getCheckInStatusText('clockIn') }}</span>
<span>{{ getCheckInStatusText("clockIn") }}</span>
<span>{{ formatTime(attendanceStatus.clockInTime) }}</span>
</div>
</div>
<div class="status-card">
<div class="status-time">下班{{ attendanceGroup.workEndTime }}</div>
<div class="status-time">
<!-- 下班 -->
{{ $t("checkin.workEndTime") }}
{{ attendanceGroup.workEndTime }}
</div>
<div class="status-label">
<span>{{ getCheckInStatusText('clockOut') }}</span>
<span>{{ getCheckInStatusText("clockOut") }}</span>
<span>{{ formatTime(attendanceStatus.clockOutTime) }}</span>
</div>
</div>
</div>

<p v-if="hasBirthday" class="birthday-message">
🎂{{ birthdayMessage }}🎉🎁🎈
</p>
</div>

<!-- 打卡按钮 -->
@@ -82,7 +101,7 @@
:punch-time="currentCompleteDate"
:location="formattedAddress"
@close="closeSuccessDialog"
/>
/>

<!-- 位置信息显示 -->
<location-info
@@ -95,44 +114,42 @@

<!-- 加载状态 -->
<div v-else class="container loading">
<div class="loading-text">正在加载考勤信息...</div>
<div class="loading-text">
<!-- 正在加载考勤信息... -->
<i class="el-icon-loading"></i>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import {
AttendanceService,
PUNCH_STATUS,
PUNCH_TYPE
} from './service'
import RemarkDialog from './components/RemarkDialog.vue'
import SuccessDialog from './components/SuccessDialog.vue'
import LocationInfo from './components/LocationInfo.vue'

import { getGeocode } from '@/api/oa/attendance/clockIn'

import { mapGetters } from "vuex";
import { AttendanceService, PUNCH_STATUS, PUNCH_TYPE } from "./service";
import RemarkDialog from "./components/RemarkDialog.vue";
import SuccessDialog from "./components/SuccessDialog.vue";
import LocationInfo from "./components/LocationInfo.vue";
import { getGeocode } from "@/api/oa/attendance/clockIn";
import moment from "moment";
export default {
name: 'MCheckin',
name: "MCheckin",
components: {
RemarkDialog,
SuccessDialog,
LocationInfo
LocationInfo,
},
data() {
return {
// 时间相关
currentTime: '',
currentCompleteDate: '',
currentTime: "",
currentCompleteDate: "",
timeIntervalId: null,

// 打卡成功地址
formattedAddress: '',
formattedAddress: "",

// 位置相关
userLocation: {
latitude: null,
longitude: null
longitude: null,
},
isInRange: false,
watchId: null,
@@ -143,29 +160,50 @@ export default {
clockInTime: null,
clockOutTime: null,
clockInStatus: PUNCH_STATUS.NOT_CHECKED,
clockOutStatus: PUNCH_STATUS.NOT_CHECKED
clockOutStatus: PUNCH_STATUS.NOT_CHECKED,
},

// 弹窗状态
showRemarkDialog: false,
showSuccessDialog: false,
remarkDialogTitle: '',
remarkDialogTitle: "",
remarkForm: {
remark: '',
type: ''
}
}
remark: "",
type: "",
},
};
},

computed: {
...mapGetters(['userinfo']),
...mapGetters(["userinfo", "nickname"]),

hasBirthday() {
if (!this.userinfo.birthday) return false;
const today = new Date().toISOString().split("T")[0];
const birthday = this.userinfo.birthday.split("-");

const month = birthday[1];
const day = birthday[2];

const todayMonth = today.split("-")[1];
const todayDay = today.split("-")[2];

return month == todayMonth && day == todayDay;
},
birthdayMessage() {
if (!this.userinfo.birthday) return "";
return this.$t("home.birthday", {
name: this.nickname,
birthday: moment(this.userinfo.birthday).format(`M/D`),
});
},

/**
* 获取当前时区
* @returns {string} 时区信息
*/
timeZone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone
return Intl.DateTimeFormat().resolvedOptions().timeZone;
},

/**
@@ -173,34 +211,41 @@ export default {
* @returns {string} 按钮文本
*/
punchButtonText() {
const { clockInStatus, clockOutStatus } = this.attendanceStatus
const { clockInStatus, clockOutStatus } = this.attendanceStatus;

console.log(this.attendanceStatus)
console.log(PUNCH_STATUS);

if (clockInStatus == PUNCH_STATUS.NOT_CHECKED) {
return '上班打卡'
// return '上班打卡'
return this.$t("checkin.button.notChecked");
}
if (
clockOutStatus == PUNCH_STATUS.NOT_CHECKED ||
clockOutStatus == PUNCH_STATUS.CHECKED_OUT ||
clockOutStatus == PUNCH_STATUS.LATE_IN
) {
return '下班打卡'
// return '下班打卡'
return this.$t("checkin.button.checkedIn");
}
return '已打卡'
}
// return '已打卡'
return this.$t("checkin.button.checkedOut");
},
},

async mounted() {
try {
await this.initializeApp()
if (this.userinfo.language) {
this.$i18n.locale = this.userinfo.language;
this.$store.dispatch("language/setLanguage", this.userinfo.language);
}
await this.initializeApp();
} catch (error) {
this.handleError('初始化失败', error)
this.handleError(i18n.t("checkin.error.init"), error);
}
},

beforeDestroy() {
this.cleanup()
this.cleanup();
},

methods: {
@@ -208,28 +253,28 @@ export default {
* 初始化应用
*/
async initializeApp() {
this.startTimeUpdate()
await this.startLocationTracking()
await this.loadAttendanceData()
this.startTimeUpdate();
await this.startLocationTracking();
await this.loadAttendanceData();
},

/**
* 开始时间更新
*/
startTimeUpdate() {
this.updateCurrentTime()
this.updateCurrentTime();
this.timeIntervalId = setInterval(() => {
this.updateCurrentTime()
}, 1000)
this.updateCurrentTime();
}, 1000);
},

/**
* 更新当前时间显示
*/
updateCurrentTime() {
this.currentTime = new Date().toLocaleTimeString('zh-CN', {
hour12: false
})
this.currentTime = new Date().toLocaleTimeString("zh-CN", {
hour12: false,
});
},

/**
@@ -239,14 +284,14 @@ export default {
const options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
maximumAge: 0,
};

this.watchId = navigator.geolocation.watchPosition(
this.handleLocationSuccess,
this.handleLocationError,
options
)
);
},

/**
@@ -256,9 +301,9 @@ export default {
handleLocationSuccess(position) {
this.userLocation = {
latitude: position.coords.latitude,
longitude: position.coords.longitude
}
this.updateLocationRange()
longitude: position.coords.longitude,
};
this.updateLocationRange();
},

/**
@@ -267,13 +312,13 @@ export default {
*/
handleLocationError(error) {
const errorMessages = {
[error.PERMISSION_DENIED]: '请允许获取位置权限',
[error.POSITION_UNAVAILABLE]: '位置信息不可用',
[error.TIMEOUT]: '获取位置超时'
}
[error.PERMISSION_DENIED]: "请允许获取位置权限",
[error.POSITION_UNAVAILABLE]: "位置信息不可用",
[error.TIMEOUT]: "获取位置超时",
};

const message = errorMessages[error.code] || '获取位置失败'
this.$toast.error(message)
const message = errorMessages[error.code] || "获取位置失败";
this.$toast.error(message);
},

/**
@@ -283,22 +328,22 @@ export default {
this.isInRange = AttendanceService.isInAttendanceRange(
this.userLocation,
this.attendanceGroup
)
);
},

/**
* 加载考勤数据
*/
async loadAttendanceData() {
const { userId } = this.userinfo
const { userId } = this.userinfo;
if (!userId) {
throw new Error('用户信息获取失败')
throw new Error("用户信息获取失败");
}

await Promise.all([
this.loadAttendanceGroup(userId),
this.loadTodayAttendance(userId)
])
this.loadTodayAttendance(userId),
]);
},

/**
@@ -306,8 +351,8 @@ export default {
* @param {string} userId 用户ID
*/
async loadAttendanceGroup(userId) {
this.attendanceGroup = await AttendanceService.getAttendanceGroup(userId)
this.updateLocationRange()
this.attendanceGroup = await AttendanceService.getAttendanceGroup(userId);
this.updateLocationRange();
},

/**
@@ -315,9 +360,9 @@ export default {
* @param {string} userId 用户ID
*/
async loadTodayAttendance(userId) {
const records = await AttendanceService.getTodayAttendance(userId)
const records = await AttendanceService.getTodayAttendance(userId);
this.attendanceStatus =
AttendanceService.processAttendanceRecords(records)
AttendanceService.processAttendanceRecords(records);
},

/**
@@ -325,14 +370,14 @@ export default {
*/
async handlePunchClick() {
if (!this.validatePunchConditions()) {
return
return;
}

try {
const punchAction = this.determinePunchAction()
await this.executePunchAction(punchAction)
const punchAction = this.determinePunchAction();
await this.executePunchAction(punchAction);
} catch (error) {
this.handleError('打卡失败', error)
this.handleError("打卡失败", error);
}
},

@@ -342,20 +387,22 @@ export default {
*/
validatePunchConditions() {
if (!this.isInRange) {
this.$toast.error('请在打卡范围内进行打卡')
return false
// this.$toast.error("请在打卡范围内进行打卡");
this.$toast.error(this.$t("checkin.error.outOfRange"));
return false;
}

const { clockInStatus, clockOutStatus } = this.attendanceStatus
const { clockInStatus, clockOutStatus } = this.attendanceStatus;
if (
clockInStatus !== PUNCH_STATUS.NOT_CHECKED &&
clockOutStatus !== PUNCH_STATUS.NOT_CHECKED
) {
this.$toast.info('今日已完成打卡')
return false
// this.$toast.info("今日已完成打卡");
this.$toast.info(this.$t("checkin.punch.status.todayChecked"));
return false;
}

return true
return true;
},

/**
@@ -363,21 +410,21 @@ export default {
* @returns {Object} 打卡动作信息
*/
determinePunchAction() {
const { clockInStatus } = this.attendanceStatus
const { clockInStatus } = this.attendanceStatus;

if (clockInStatus === PUNCH_STATUS.NOT_CHECKED) {
return AttendanceService.isLateForWork(
this.attendanceGroup.workStartTime
)
? { type: 'morning_late', needRemark: true }
: { type: 'morning', needRemark: false }
? { type: "morning_late", needRemark: true }
: { type: "morning", needRemark: false };
}

return AttendanceService.isEarlyForLeaving(
this.attendanceGroup.workEndTime
)
? { type: 'evening_early', needRemark: true }
: { type: 'evening', needRemark: false }
? { type: "evening_early", needRemark: true }
: { type: "evening", needRemark: false };
},

/**
@@ -386,9 +433,9 @@ export default {
*/
async executePunchAction(action) {
if (action.needRemark) {
this.showRemarkDialogForType(action.type)
this.showRemarkDialogForType(action.type);
} else {
await this.submitPunch(action.type)
await this.submitPunch(action.type);
}
},

@@ -397,15 +444,20 @@ export default {
* @param {string} type 打卡类型
*/
showRemarkDialogForType(type) {
const titles = {
morning_late: '迟到打卡',
evening_early: '早退打卡'
}
// const titles = {
// morning_late: "迟到打卡",
// evening_early: "早退打卡",
// };

this.remarkDialogTitle = titles[type]
this.remarkForm.type = type
this.remarkForm.remark = ''
this.showRemarkDialog = true
const titles = {
morning_late: this.$t("checkin.punch.status.lateIn"),
evening_early: this.$t("checkin.punch.status.earlyOut"),
};

this.remarkDialogTitle = titles[type];
this.remarkForm.type = type;
this.remarkForm.remark = "";
this.showRemarkDialog = true;
},

/**
@@ -413,10 +465,11 @@ export default {
*/
async submitRemarkPunch() {
try {
await this.submitPunch(this.remarkForm.type)
this.closeRemarkDialog()
await this.submitPunch(this.remarkForm.type);
this.closeRemarkDialog();
} catch (error) {
this.handleError('提交失败', error)
// this.handleError("提交失败", error);
this.handleError(this.$t("checkin.error.submitFailed"), error);
}
},

@@ -431,30 +484,32 @@ export default {
userLocation: this.userLocation,
attendanceGroupId: this.attendanceGroup.attendanceGroupId,
attendanceGroupName: this.attendanceGroup.attendanceGroupName,
remark: this.remarkForm.remark
})
remark: this.remarkForm.remark,
});

await AttendanceService.submitPunch(punchData)
await AttendanceService.submitPunch(punchData);

this.currentCompleteDate = AttendanceService.formatDateTime(new Date())
this.showSuccessDialog = true
this.formattedAddress = 'Loading...'
this.currentCompleteDate = AttendanceService.formatDateTime(new Date());
this.showSuccessDialog = true;
this.formattedAddress = "Loading...";

try {
const res = await getGeocode(this.userLocation.longitude, this.userLocation.latitude)
console.log(res)
const {status, results} = res
const res = await getGeocode(
this.userLocation.longitude,
this.userLocation.latitude
);
console.log(res);
const { status, results } = res;
if (status.code === 200 && results.length > 0) {
const {formatted} = results[0]
this.formattedAddress = formatted
const { formatted } = results[0];
this.formattedAddress = formatted;
}
} catch (error) {
this.formattedAddress = this.attendanceGroup.areaName
console.error(error)
this.formattedAddress = this.attendanceGroup.areaName;
console.error(error);
}

await this.loadTodayAttendance(this.userinfo.userId)

await this.loadTodayAttendance(this.userinfo.userId);
},

/**
@@ -466,9 +521,9 @@ export default {
const status =
type === PUNCH_TYPE.CLOCK_IN
? this.attendanceStatus.clockInStatus
: this.attendanceStatus.clockOutStatus
: this.attendanceStatus.clockOutStatus;

return AttendanceService.getStatusText(status)
return AttendanceService.getStatusText(status);
},

/**
@@ -477,22 +532,22 @@ export default {
* @returns {string} 格式化后的时间
*/
formatTime(time) {
return AttendanceService.formatTime(time)
return AttendanceService.formatTime(time);
},

/**
* 关闭备注对话框
*/
closeRemarkDialog() {
this.showRemarkDialog = false
this.remarkForm.remark = ''
this.showRemarkDialog = false;
this.remarkForm.remark = "";
},

/**
* 关闭成功对话框
*/
closeSuccessDialog() {
this.showSuccessDialog = false
this.showSuccessDialog = false;
},

/**
@@ -501,8 +556,8 @@ export default {
* @param {Error} error 错误对象
*/
handleError(message, error) {
console.error(message, error)
this.$toast.error(error.message || message)
console.error(message, error);
this.$toast.error(error.message || message);
},

/**
@@ -510,18 +565,18 @@ export default {
*/
cleanup() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId)
navigator.geolocation.clearWatch(this.watchId);
}
if (this.timeIntervalId) {
clearInterval(this.timeIntervalId)
clearInterval(this.timeIntervalId);
}
}
}
}
},
},
};
</script>
<style lang="scss" scoped>
.page {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
color: #333;
@media (min-width: 480px) {
@@ -538,6 +593,27 @@ export default {
position: relative;
user-select: none;

&.has-birthday {
background: linear-gradient(135deg, #ffd6e8, #fff3b0, #b5ead7, #c9f0ff);
background-size: 400% 400%;
animation: birthdayGradient 15s ease infinite;

.header {
background: rgba(255, 255, 255, 0.4);
box-shadow: none;
}

.status-card {
background: rgba(255, 255, 255, 0.4);
box-shadow: none;
}

::v-deep .location-info {
background: rgba(255, 255, 255, 0.4);
box-shadow: none;
}
}

&.loading {
display: flex;
align-items: center;
@@ -545,6 +621,18 @@ export default {
}
}

@keyframes birthdayGradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}

.loading-text {
font-size: 16px;
color: #666;
@@ -558,6 +646,19 @@ export default {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
text-align: center;
background: white;
display: flex;
align-items: center;
justify-content: flex-end;
}

.user-info {
display: flex;
align-items: center;

+ 110
- 95
src/views/oa/attendance/checkin/service.js Ver arquivo

@@ -1,10 +1,12 @@
import { parseTime } from '@/utils/tools'
import { parseTime } from "@/utils/tools";
import {
queryAttendanceGroupByUserId,
checkIn,
checkOut,
getCurrentDayRecord
} from '@/api/oa/attendance/clockIn'
getCurrentDayRecord,
} from "@/api/oa/attendance/clockIn";

import i18n from "@/i18n";

// 打卡状态常量
export const PUNCH_STATUS = {
@@ -13,14 +15,14 @@ export const PUNCH_STATUS = {
CHECKED_OUT: 2,
LATE_IN: 3,
EARLY_OUT: 4,
UPDATE_OUT: 5
}
UPDATE_OUT: 5,
};

// 打卡类型常量
export const PUNCH_TYPE = {
CLOCK_IN: 'clockIn',
CLOCK_OUT: 'clockOut'
}
CLOCK_IN: "clockIn",
CLOCK_OUT: "clockOut",
};

/**
* 考勤服务类
@@ -34,27 +36,27 @@ export class AttendanceService {
* @throws {Error} 当无法创建有效日期时抛出错误
*/
static createSafeDate(input) {
let date
let date;

if (!input) {
date = new Date()
date = new Date();
} else if (input instanceof Date) {
date = input
} else if (typeof input === 'string') {
date = input;
} else if (typeof input === "string") {
// 处理 IOS 系统的日期格式兼容性问题
// 将 'YYYY-MM-DD HH:mm:ss' 格式转换为 'YYYY/MM/DD HH:mm:ss'
const normalizedInput = input.replace(/-/g, '/')
date = new Date(normalizedInput)
const normalizedInput = input.replace(/-/g, "/");
date = new Date(normalizedInput);
} else {
date = new Date(input)
date = new Date(input);
}

// 验证创建的日期是否有效
if (!date || isNaN(date.getTime())) {
throw new Error('无法创建有效的日期对象')
throw new Error("无法创建有效的日期对象");
}

return date
return date;
}

/**
@@ -66,19 +68,19 @@ export class AttendanceService {
* @returns {number} 距离,米
*/
static calculateDistance(lat1, lng1, lat2, lng2) {
const R = 6371000 // 地球半径,单位:米
const dLat = (lat2 - lat1) * (Math.PI / 180)
const dLng = (lng2 - lng1) * (Math.PI / 180)
const R = 6371000; // 地球半径,单位:米
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLng = (lng2 - lng1) * (Math.PI / 180);

const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) *
Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLng / 2) *
Math.sin(dLng / 2)
Math.sin(dLng / 2);

const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c // 返回单位:米
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // 返回单位:米
}

/**
@@ -88,15 +90,15 @@ export class AttendanceService {
* @returns {boolean} 是否在范围内
*/
static isInAttendanceRange(userLocation, attendanceGroup) {
const { latitude, longitude } = userLocation
const { lat, lng, radius } = attendanceGroup
const { latitude, longitude } = userLocation;
const { lat, lng, radius } = attendanceGroup;

if (!latitude || !longitude || !lat || !lng) {
return false
return false;
}

const distance = this.calculateDistance(latitude, longitude, lat, lng)
return distance <= radius
const distance = this.calculateDistance(latitude, longitude, lat, lng);
return distance <= radius;
}

/**
@@ -105,13 +107,13 @@ export class AttendanceService {
* @returns {number} 比较结果 (-1: 早于, 0: 等于, 1: 晚于)
*/
static compareTimeWithSchedule(scheduleTime) {
const now = new Date()
const [scheduleHour, scheduleMinute] = scheduleTime.split(':').map(Number)
const now = new Date();
const [scheduleHour, scheduleMinute] = scheduleTime.split(":").map(Number);

const currentMinutes = now.getHours() * 60 + now.getMinutes()
const scheduleMinutes = scheduleHour * 60 + scheduleMinute
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const scheduleMinutes = scheduleHour * 60 + scheduleMinute;

return Math.sign(currentMinutes - scheduleMinutes)
return Math.sign(currentMinutes - scheduleMinutes);
}

/**
@@ -120,7 +122,7 @@ export class AttendanceService {
* @returns {boolean} 是否迟到
*/
static isLateForWork(workStartTime) {
return this.compareTimeWithSchedule(workStartTime) > 0
return this.compareTimeWithSchedule(workStartTime) > 0;
}

/**
@@ -129,7 +131,7 @@ export class AttendanceService {
* @returns {boolean} 是否早退
*/
static isEarlyForLeaving(workEndTime) {
return this.compareTimeWithSchedule(workEndTime) < 0
return this.compareTimeWithSchedule(workEndTime) < 0;
}

/**
@@ -139,15 +141,15 @@ export class AttendanceService {
*/
static getStatusText(status) {
const statusTexts = {
[PUNCH_STATUS.NOT_CHECKED]: '未打卡',
[PUNCH_STATUS.CHECKED_IN]: '已打卡',
[PUNCH_STATUS.CHECKED_OUT]: '已打卡',
[PUNCH_STATUS.LATE_IN]: '迟到打卡',
[PUNCH_STATUS.EARLY_OUT]: '早退打卡',
[PUNCH_STATUS.UPDATE_OUT]: '更新打卡'
}
return statusTexts[status] || '未打卡'
[PUNCH_STATUS.NOT_CHECKED]: i18n.t("checkin.punch.status.notChecked"), // 未打卡
[PUNCH_STATUS.CHECKED_IN]: i18n.t("checkin.punch.status.checkedIn"), // 已打卡
[PUNCH_STATUS.CHECKED_OUT]: i18n.t("checkin.punch.status.checkedOut"), // 已打卡
[PUNCH_STATUS.LATE_IN]: i18n.t("checkin.punch.status.lateIn"), // 迟到打卡
[PUNCH_STATUS.EARLY_OUT]: i18n.t("checkin.punch.status.earlyOut"), // 早退打卡
[PUNCH_STATUS.UPDATE_OUT]: i18n.t("checkin.punch.status.updateOut"), // 更新打卡
};
return statusTexts[status] || i18n.t("checkin.punch.status.notChecked"); // 未打卡
}

/**
@@ -156,7 +158,7 @@ export class AttendanceService {
* @returns {string} 格式化后的日期时间
*/
static formatDateTime(date) {
return parseTime(date, '{y}-{m}-{d} {h}:{i}:{s}')
return parseTime(date, "{y}-{m}-{d} {h}:{i}:{s}");
}

/**
@@ -165,7 +167,7 @@ export class AttendanceService {
* @returns {string} 格式化后的时间
*/
static formatTime(time) {
return time ? parseTime(time, '{h}:{i}:{s}') : ''
return time ? parseTime(time, "{h}:{i}:{s}") : "";
}

/**
@@ -177,29 +179,39 @@ export class AttendanceService {
static calculateDateInfo(date) {
// 参数验证:确保传入的是有效的 Date 对象
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
throw new Error('传入的日期参数无效,请确保是有效的 Date 对象')
// 传入的日期参数无效,请确保是有效的 Date 对象
throw new Error("传入的日期参数无效,请确保是有效的 Date 对象");
}

// 获取月份(1-12)
const month = date.getMonth() + 1
const month = date.getMonth() + 1;

// 获取日期(1-31)
const day = date.getDate()
const day = date.getDate();

// 计算是一年中的第几周(0-6,0为周日)
const week = date.getDay()
const week = date.getDay();

// 二次验证:确保返回值都是有效数字
if (isNaN(month) || isNaN(day) || isNaN(week)) {
throw new Error('日期计算结果异常,请检查传入的日期对象')
// 日期计算结果异常,请检查传入的日期对象
throw new Error("日期计算结果异常,请检查传入的日期对象");
}

// 范围验证:确保返回值在合理范围内
if (month < 1 || month > 12 || day < 1 || day > 31 || week < 0 || week > 6) {
throw new Error('日期计算结果超出有效范围')
if (
month < 1 ||
month > 12 ||
day < 1 ||
day > 31 ||
week < 0 ||
week > 6
) {
// 日期计算结果超出有效范围
throw new Error("日期计算结果超出有效范围");
}

return { month, week, day }
return { month, week, day };
}

/**
@@ -215,40 +227,40 @@ export class AttendanceService {
userLocation,
remark,
attendanceGroupId,
attendanceGroupName
} = params
attendanceGroupName,
} = params;

const statusMap = {
morning: PUNCH_STATUS.CHECKED_IN,
evening: PUNCH_STATUS.CHECKED_OUT,
morning_late: PUNCH_STATUS.LATE_IN,
evening_early: PUNCH_STATUS.EARLY_OUT
}
evening_early: PUNCH_STATUS.EARLY_OUT,
};

const isClockIn = type.includes("morning");

const isClockIn = type.includes('morning')
// 使用安全的日期创建方法
const currentDate = this.createSafeDate()
const currentTime = this.formatDateTime(currentDate)
const currentDate = this.createSafeDate();
const currentTime = this.formatDateTime(currentDate);

// 获取当前日期的年月日信息
let month, week, day
let month, week, day;
try {
const dateInfo = this.calculateDateInfo(currentDate)
month = dateInfo.month
week = dateInfo.week
day = dateInfo.day
const dateInfo = this.calculateDateInfo(currentDate);
month = dateInfo.month;
week = dateInfo.week;
day = dateInfo.day;
} catch (error) {
// 如果日期计算失败,使用当前时间重新计算
try {
const fallbackDate = this.createSafeDate()
const fallbackDateInfo = this.calculateDateInfo(fallbackDate)
month = fallbackDateInfo.month
week = fallbackDateInfo.week
day = fallbackDateInfo.day
const fallbackDate = this.createSafeDate();
const fallbackDateInfo = this.calculateDateInfo(fallbackDate);
month = fallbackDateInfo.month;
week = fallbackDateInfo.week;
day = fallbackDateInfo.day;
} catch (fallbackError) {
// 如果仍然失败,抛出错误
throw new Error(`日期计算失败: ${error.message}`)
throw new Error(`日期计算失败: ${error.message}`);
}
}

@@ -260,14 +272,14 @@ export class AttendanceService {
lat: userLocation.latitude,
checkInStatus: statusMap[type],
checkInType: isClockIn ? PUNCH_TYPE.CLOCK_IN : PUNCH_TYPE.CLOCK_OUT,
[isClockIn ? 'clockIn' : 'clockOut']: currentTime,
[isClockIn ? "clockIn" : "clockOut"]: currentTime,
attendanceGroupId,
attendanceGroupName,
month,
week,
day,
description: remark || ''
}
description: remark || "",
};
}

/**
@@ -280,17 +292,17 @@ export class AttendanceService {
clockInTime: null,
clockOutTime: null,
clockInStatus: PUNCH_STATUS.NOT_CHECKED,
clockOutStatus: PUNCH_STATUS.NOT_CHECKED
}
clockOutStatus: PUNCH_STATUS.NOT_CHECKED,
};

records.forEach((record) => {
attendanceStatus.clockInTime = record.clockIn
attendanceStatus.clockInStatus = parseInt(record.clockInStatus)
attendanceStatus.clockOutTime = record.clockOut
attendanceStatus.clockOutStatus = parseInt(record.clockOutStatus || 0)
})
attendanceStatus.clockInTime = record.clockIn;
attendanceStatus.clockInStatus = parseInt(record.clockInStatus);
attendanceStatus.clockOutTime = record.clockOut;
attendanceStatus.clockOutStatus = parseInt(record.clockOutStatus || 0);
});

return attendanceStatus
return attendanceStatus;
}

/**
@@ -299,17 +311,19 @@ export class AttendanceService {
* @returns {Promise<Object>} 考勤组信息
*/
static async getAttendanceGroup(userId) {
const response = await queryAttendanceGroupByUserId({ userId })
const response = await queryAttendanceGroupByUserId({ userId });

if (response.code !== 200) {
throw new Error(response.msg || '获取考勤组信息失败')
// 获取考勤组信息失败
throw new Error(response.msg || "获取考勤组信息失败");
}

if (!response.data) {
throw new Error('未配置考勤组,请联系管理员')
// 未配置考勤组,请联系管理员
throw new Error("未配置考勤组,请联系管理员");
}

return response.data
return response.data;
}

/**
@@ -318,13 +332,14 @@ export class AttendanceService {
* @returns {Promise<Array>} 考勤记录数组
*/
static async getTodayAttendance(userId) {
const response = await getCurrentDayRecord({ userId })
const response = await getCurrentDayRecord({ userId });

if (response.code !== 200) {
throw new Error(response.msg || '获取考勤状态失败')
// 获取考勤状态失败
throw new Error(response.msg || "获取考勤状态失败");
}

return response.data || []
return response.data || [];
}

/**
@@ -334,13 +349,13 @@ export class AttendanceService {
*/
static async submitPunch(punchData) {
const apiCall =
punchData.checkInType === PUNCH_TYPE.CLOCK_IN ? checkIn : checkOut
const response = await apiCall(punchData)
punchData.checkInType === PUNCH_TYPE.CLOCK_IN ? checkIn : checkOut;
const response = await apiCall(punchData);

if (response.code !== 200) {
throw new Error(response.msg || '打卡失败')
throw new Error(response.msg || i18n.t("checkin.punch.status.error")); // 打卡失败
}

return response
return response;
}
}

+ 83
- 56
src/views/oa/leave/annual/balance/index.vue Ver arquivo

@@ -42,27 +42,57 @@

<el-table v-loading="loading" :data="tableData" highlight-current-row>
<el-table-column type="index" label="序号" />
<el-table-column label="员工编号" align="center" prop="userName" />
<el-table-column label="员工姓名" align="center" prop="nickName" />
<el-table-column label="所属部门" align="center" prop="deptName" />
<el-table-column label="年假总额" align="center" prop="annualLeave">
<el-table-column
label="员工编号"
align="center"
prop="userName"
sortable
/>
<el-table-column
label="员工姓名"
align="center"
prop="nickName"
sortable
/>
<el-table-column
label="所属部门"
align="center"
prop="deptName"
sortable
/>
<el-table-column
label="年假总额"
align="center"
prop="annualLeave"
sortable
>
<template slot-scope="scope">
<span>{{ parseInt(scope.row.annualLeave) || 0 }} 天</span>
</template>
</el-table-column>
<el-table-column label="已使用天数" align="center" prop="usedDay">
<el-table-column
label="已使用天数"
align="center"
prop="usedDay"
sortable
>
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleDetail(scope.row)"
>{{ parseInt(scope.row.usedDay) || 0 }} 天</el-button
>
</template>
</el-table-column>
<el-table-column label="剩余天数" align="center" prop="unusedDay">
<el-table-column
label="剩余天数"
align="center"
prop="unusedDay"
sortable
>
<template slot-scope="scope">
<span>{{ parseInt(scope.row.unusedDay) || 0 }} 天</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="isEnable">
<el-table-column label="状态" align="center" prop="isEnable" sortable>
<template slot-scope="scope">
<span v-if="scope.row.isEnable == 0"> 清零 </span>
<span v-if="scope.row.isEnable == 1"> 有效 </span>
@@ -158,7 +188,6 @@
</el-dialog>

<el-dialog title="申请休假" :visible.sync="applyDialogVisible" width="50%">
<h3>基本信息</h3>
<el-descriptions :column="3" border style="margin-bottom: 20px">
<el-descriptions-item label="员工编号">
{{ detailRow.userName }}
@@ -179,51 +208,6 @@
{{ detailRow.unusedDay }}
</el-descriptions-item>
</el-descriptions>
<h3>休假记录</h3>
<el-table :data="detailRows" size="mini" style="width: 100%">
<el-table-column label="申请日期" prop="createTime" />
<el-table-column label="休假天数" prop="usedDay" />
<el-table-column label="休假时间" prop="applyDates">
<template slot-scope="scope">
<p
v-for="date in scope.row.applyDates.split(',')"
:key="date"
style="margin: 0"
>
{{ date }}
</p>
</template>
</el-table-column>
<el-table-column label="状态" prop="status">
<template slot-scope="scope">
<span v-if="scope.row.status == '0'" style="color: #409eff">
待提交
</span>
<span v-else-if="scope.row.status == '1'" style="color: #e6a23c">
待审批
</span>
<span v-else-if="scope.row.status == '2'" style="color: #67c23a">
审批通过
</span>
<span v-else-if="scope.row.status == '3'" style="color: #f56c6c">
驳回
</span>
<span v-else-if="scope.row.status == '4'" style="color: #909399">
已作废
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button
v-if="['0', '1', '2', '3'].includes(scope.row.status)"
type="text"
@click="handleDelete(scope.row)"
>作废</el-button
>
</template>
</el-table-column>
</el-table>
<h3>申请信息</h3>
<el-form
:model="applyForm"
@@ -272,6 +256,47 @@
</div>
</el-form-item>
</el-form>
<h3>休假记录</h3>
<el-table
:data="detailRows"
size="mini"
height="180px"
style="width: 100%"
>
<el-table-column label="申请日期" prop="createTime" />
<el-table-column label="休假天数" prop="usedDay" width="100" />
<el-table-column label="休假时间" prop="applyDates"> </el-table-column>
<el-table-column label="状态" prop="status" width="100" >
<template slot-scope="scope">
<span v-if="scope.row.status == '0'" style="color: #409eff">
待提交
</span>
<span v-else-if="scope.row.status == '1'" style="color: #e6a23c">
待审批
</span>
<span v-else-if="scope.row.status == '2'" style="color: #67c23a">
审批通过
</span>
<span v-else-if="scope.row.status == '3'" style="color: #f56c6c">
驳回
</span>
<span v-else-if="scope.row.status == '4'" style="color: #909399">
已作废
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button
v-if="['0', '1', '2', '3'].includes(scope.row.status)"
type="text"
@click="handleDelete(scope.row)"
>作废</el-button
>
</template>
</el-table-column>
</el-table>

<span slot="footer" class="dialog-footer">
<el-button @click="applyDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="handleApplySubmit">提 交</el-button>
@@ -318,7 +343,7 @@ export default {
applyDialogVisible: false,
applyForm: {
userId: "",
usedDay: 0,
usedDay: 1,
applyDate: "",
applyDates: [""],
},
@@ -412,6 +437,8 @@ export default {
userName: row.userName,
}).then((response) => {
this.detailRows = response.rows;
this.detailDialogVisible = false;
this.applyDialogVisible = false;
});
});
})
@@ -468,7 +495,7 @@ export default {
}
return date;
})
.join(", ");
.join(",");

const params = {
userId: this.applyForm.userId,
@@ -543,7 +570,7 @@ export default {
this.applyForm.nickName = this.detailRow.nickName;
this.applyForm.deptName = this.detailRow.deptName;
this.applyForm.applyDates = [""];
this.applyForm.usedDay = 0;
this.applyForm.usedDay = 1;

detailList({
userName: row.userName,

+ 39
- 20
src/views/oa/leave/annual/balance/mine.vue Ver arquivo

@@ -3,7 +3,9 @@
<div class="dashboard-container">
<div class="dashboard-item">
<div class="dashboard-item-title">年假总额</div>
<div class="dashboard-item-value">{{ parseInt(totalAnnualLeave) }} 天</div>
<div class="dashboard-item-value">
{{ parseInt(totalAnnualLeave) }} 天
</div>
</div>
<div class="dashboard-item">
<div class="dashboard-item-title">已使用天数</div>
@@ -11,15 +13,44 @@
</div>
<div class="dashboard-item">
<div class="dashboard-item-title">剩余天数</div>
<div class="dashboard-item-value">{{ parseInt(totalUnusedDay) }} 天</div>
<div class="dashboard-item-value">
{{ parseInt(totalUnusedDay) }} 天
</div>
</div>
</div>

<el-table :data="detailRows" style="width: 100%">
<el-table-column type="index" label="序号" />
<el-table-column label="申请日期" prop="createTime" />
<el-table-column label="休假天数" prop="usedDay" />
<el-table-column label="休假时间" prop="applyDates" />
<el-table-column label="员工编号" align="center" prop="userName" width="100" />
<el-table-column label="员工姓名" align="center" prop="nickName" width="100" />
<el-table-column label="所属部门" align="center" prop="deptName" width="140" />
<el-table-column label="申请日期" prop="createTime" width="160" />
<el-table-column label="休假天数" prop="usedDay" width="120">
<template slot-scope="scope">
{{ parseInt(scope.row.usedDay) }} 天
</template>
</el-table-column>
<el-table-column label="状态" prop="status">
<template slot-scope="scope">
<span v-if="scope.row.status == '0'" style="color: #409eff">
待提交
</span>
<span v-else-if="scope.row.status == '1'" style="color: #e6a23c">
待审批
</span>
<span v-else-if="scope.row.status == '2'" style="color: #67c23a">
审批通过
</span>
<span v-else-if="scope.row.status == '3'" style="color: #f56c6c">
驳回
</span>
<span v-else-if="scope.row.status == '4'" style="color: #909399">
已作废
</span>
</template>
</el-table-column>
<el-table-column label="休假时间" prop="applyDates">
</el-table-column>
</el-table>
</div>
</template>
@@ -68,20 +99,6 @@ export default {
this.getList();
},
methods: {
/**
* 格式化日期为 YYYY-MM-DD 格式
* @param {Date} date - 日期对象
* @returns {string} 格式化后的日期字符串
*/
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
},
handleDetail(row) {
this.detailRow = row;
},
// 加载数据
getList() {
this.loading = true;
@@ -95,7 +112,9 @@ export default {
this.totalUnusedDay = response.rows[0].unusedDay;
}

detailList(this.userinfo.userId).then((response) => {
detailList({
userName: this.userinfo.userName,
}).then((response) => {
this.detailRows = response.rows;
});
});

+ 38
- 26
src/views/oa/leave/annual/calendar/index.vue Ver arquivo

@@ -1,12 +1,10 @@
<template>
<div class="app-container">
<el-calendar v-loading="loading">
<template slot="dateCell" slot-scope="{ date, data }">
<template slot="dateCell" slot-scope="{ data }">
<div class="calendar-cell">
<strong class="text">{{
data.day.split("-").slice(1)[1]
}}</strong>
<span class="rest">{{ findUsed(data.day) }}</span>
<strong class="text">{{ data.day.split("-").slice(1)[1] }}</strong>
<span class="rest">{{ findUser(data.day) }}</span>
</div>
</template>
</el-calendar>
@@ -30,26 +28,40 @@ export default {
this.getList();
},
methods: {
findUsed(day) {
const dayData = this.calendarData.filter((item) => {
const obj = {
day: Object.keys(item)[0],
member: Object.values(item)[0],
};
return obj.day === day ? true : false;
});
if (dayData.length > 0) {
return dayData.map((item) => Object.values(item)[0]).join(",");
/**
* 查找指定日期对应的请假人员名称
* @param {string} targetDay - 目标日期,格式为 YYYY-MM-DD
* @returns {string} 返回请假人员名称,未找到则返回空字符串
*/
findUser(targetDay) {
try {
// 遍历日历数据,查找匹配的日期
for (let dateObj of this.calendarData) {
if (dateObj[targetDay]) {
return dateObj[targetDay];
}
}
return "";
} catch (error) {
console.error("查找请假人员失败:", error);
return "";
}
return "";
},
// 加载数据

/**
* 加载年假日历数据
*/
getList() {
this.loading = true;
getCalendar().then((response) => {
this.calendarData = response;
this.loading = false;
});
getCalendar()
.then((response) => {
this.calendarData = response;
this.loading = false;
})
.catch((error) => {
console.error("获取日历数据失败:", error);
this.loading = false;
});
},
},
};
@@ -69,7 +81,7 @@ export default {
}
}

::v-deep .el-calendar__title{
::v-deep .el-calendar__title {
font-size: 18px;
}

@@ -79,12 +91,12 @@ export default {
thead {
th {
background-color: #f5f5f5;
border: 1px solid #DCDFE6;
border: 1px solid #dcdfe6;
}
}
tbody{
td{
border: 1px solid #DCDFE6;
tbody {
td {
border: 1px solid #dcdfe6;
}
}
}

+ 205
- 177
src/views/system/user/index.vue Ver arquivo

@@ -89,12 +89,11 @@
icon="el-icon-search"
size="mini"
@click="handleQuery"
>搜索</el-button>
<el-button
icon="el-icon-refresh"
size="mini"
@click="resetQuery"
>重置</el-button>
>搜索</el-button
>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"
>重置</el-button
>
</el-form-item>
</el-form>

@@ -112,7 +111,8 @@
icon="el-icon-plus"
size="mini"
@click="handleAdd"
>新增</el-button>
>新增</el-button
>
<el-button
v-hasPermi="['system:user:edit']"
type="success"
@@ -121,7 +121,8 @@
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
>修改</el-button
>
<el-button
v-hasPermi="['system:user:remove']"
type="danger"
@@ -130,7 +131,8 @@
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
>删除</el-button
>
<el-button
v-hasPermi="['system:user:import']"
type="info"
@@ -138,7 +140,8 @@
icon="el-icon-upload2"
size="mini"
@click="handleImport"
>导入</el-button>
>导入</el-button
>
<el-button
v-hasPermi="['system:user:export']"
type="warning"
@@ -146,7 +149,8 @@
icon="el-icon-download"
size="mini"
@click="handleExport"
>导出</el-button>
>导出</el-button
>
<right-toolbar
:show-search.sync="showSearch"
:columns="columns"
@@ -184,6 +188,13 @@
prop="nickName"
:show-overflow-tooltip="true"
/>
<el-table-column
key="birthday"
label="生日"
align="center"
prop="birthday"
:show-overflow-tooltip="true"
/>
<el-table-column
v-if="columns[3].visible"
key="deptName"
@@ -239,35 +250,37 @@
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
>修改</el-button>
>修改</el-button
>
<el-button
v-hasPermi="['system:user:remove']"
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
>删除</el-button>
>删除</el-button
>
<el-dropdown
v-hasPermi="['system:user:resetPwd', 'system:user:edit']"
size="mini"
@command="(command) => handleCommand(command, scope.row)"
>
<el-button
size="mini"
type="text"
icon="el-icon-d-arrow-right"
>更多</el-button>
<el-button size="mini" type="text" icon="el-icon-d-arrow-right"
>更多</el-button
>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-hasPermi="['system:user:resetPwd']"
command="handleResetPwd"
icon="el-icon-key"
>重置密码</el-dropdown-item>
>重置密码</el-dropdown-item
>
<el-dropdown-item
v-hasPermi="['system:user:edit']"
command="handleAuthRole"
icon="el-icon-circle-check"
>分配角色</el-dropdown-item>
>分配角色</el-dropdown-item
>
</el-dropdown-menu>
</el-dropdown>
</template>
@@ -285,7 +298,7 @@
</el-row>

<!-- 添加或修改用户配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="620px" append-to-body>
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
@@ -378,7 +391,8 @@
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio>
>{{ dict.label }}</el-radio
>
</el-radio-group>
</el-form-item>
</el-col>
@@ -427,28 +441,38 @@
type="date"
placeholder="选择日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd">
value-format="yyyy-MM-dd"
>
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生日" prop="birthday">
<el-form-item label="生日" prop="birthday">
<el-date-picker
v-model="form.birthday"
type="date"
placeholder="选择日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd">
value-format="yyyy-MM-dd"
>
</el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-col :span="12">
<el-form-item label="默认语言">
<el-select v-model="form.language" placeholder="请选择语言">
<el-option label="中文" value="zh" />
<el-option label="日文" value="ja" />
<el-option label="英文" value="en" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入内容"
/>
</el-form-item>
@@ -493,7 +517,8 @@
:underline="false"
style="font-size: 12px; vertical-align: baseline"
@click="importTemplate"
>下载模板</el-link>
>下载模板</el-link
>
</div>
</el-upload>
<div slot="footer" class="dialog-footer">
@@ -513,15 +538,15 @@ import {
updateUser,
resetUserPwd,
changeUserStatus,
deptTreeSelect
} from '@/api/system/user'
import { getToken } from '@/utils/auth'
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
deptTreeSelect,
} from "@/api/system/user";
import { getToken } from "@/utils/auth";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";

export default {
name: 'User',
dicts: ['sys_normal_disable', 'sys_user_sex'],
name: "User",
dicts: ["sys_normal_disable", "sys_user_sex"],
components: { Treeselect },
data() {
return {
@@ -540,7 +565,7 @@ export default {
// 用户表格数据
userList: null,
// 弹出层标题
title: '',
title: "",
// 部门树选项
deptOptions: undefined,
// 是否显示弹出层
@@ -558,23 +583,23 @@ export default {
// 表单参数
form: {},
defaultProps: {
children: 'children',
label: 'label'
children: "children",
label: "label",
},
// 用户导入参数
upload: {
// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: '',
title: "",
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: { Authorization: 'Bearer ' + getToken() },
headers: { Authorization: "Bearer " + getToken() },
// 上传的地址
url: process.env.VUE_APP_BASE_API + '/system/user/importData'
url: process.env.VUE_APP_BASE_API + "/system/user/importData",
},
// 查询参数
queryParams: {
@@ -583,7 +608,7 @@ export default {
userName: undefined,
phonenumber: undefined,
status: undefined,
deptId: undefined
deptId: undefined,
},
// 列信息
columns: [
@@ -593,111 +618,111 @@ export default {
{ key: 3, label: `部门`, visible: true },
{ key: 4, label: `手机号码`, visible: true },
{ key: 5, label: `状态`, visible: true },
{ key: 6, label: `创建时间`, visible: true }
{ key: 6, label: `创建时间`, visible: true },
],
// 表单校验
rules: {
userName: [
{ required: true, message: '用户名称不能为空', trigger: 'blur' },
{ required: true, message: "用户名称不能为空", trigger: "blur" },
{
min: 2,
max: 20,
message: '用户名称长度必须介于 2 和 20 之间',
trigger: 'blur'
}
message: "用户名称长度必须介于 2 和 20 之间",
trigger: "blur",
},
],
nickName: [
{ required: true, message: '姓名不能为空', trigger: 'blur' }
{ required: true, message: "姓名不能为空", trigger: "blur" },
],
password: [
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
{ required: true, message: "用户密码不能为空", trigger: "blur" },
{
min: 5,
max: 20,
message: '用户密码长度必须介于 5 和 20 之间',
trigger: 'blur'
}
message: "用户密码长度必须介于 5 和 20 之间",
trigger: "blur",
},
],
email: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change']
}
type: "email",
message: "请输入正确的邮箱地址",
trigger: ["blur", "change"],
},
],
phonenumber: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
trigger: 'blur'
}
message: "请输入正确的手机号码",
trigger: "blur",
},
],
joinedDate: [
{ required: true, message: '入职时间不能为空', trigger: 'blur' }
]
}
}
// joinedDate: [
// { required: true, message: "入职时间不能为空", trigger: "blur" },
// ],
},
};
},
watch: {
// 根据名称筛选部门树
deptName(val) {
this.$refs.tree.filter(val)
}
this.$refs.tree.filter(val);
},
},
created() {
this.getList()
this.getDeptTree()
this.getConfigKey('sys.user.initPassword').then((response) => {
this.initPassword = response.msg
})
this.getList();
this.getDeptTree();
this.getConfigKey("sys.user.initPassword").then((response) => {
this.initPassword = response.msg;
});
},
methods: {
/** 查询用户列表 */
getList() {
this.loading = true
this.loading = true;
listUser(this.addDateRange(this.queryParams, this.dateRange)).then(
(response) => {
this.userList = response.rows
this.total = response.total
this.loading = false
this.userList = response.rows;
this.total = response.total;
this.loading = false;
}
)
);
},
/** 查询部门下拉树结构 */
getDeptTree() {
deptTreeSelect().then((response) => {
this.deptOptions = response.data
})
this.deptOptions = response.data;
});
},
// 筛选节点
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
// 节点单击事件
handleNodeClick(data) {
this.queryParams.deptId = data.id
this.handleQuery()
this.queryParams.deptId = data.id;
this.handleQuery();
},
// 用户状态修改
handleStatusChange(row) {
const text = row.status === '0' ? '启用' : '停用'
const text = row.status === "0" ? "启用" : "停用";
this.$modal
.confirm('确认要"' + text + '""' + row.userName + '"用户吗?')
.then(function() {
return changeUserStatus(row.userId, row.status)
.then(function () {
return changeUserStatus(row.userId, row.status);
})
.then(() => {
this.$modal.msgSuccess(text + '成功')
})
.catch(function() {
row.status = row.status === '0' ? '1' : '0'
this.$modal.msgSuccess(text + "成功");
})
.catch(function () {
row.status = row.status === "0" ? "1" : "0";
});
},
// 取消按钮
cancel() {
this.open = false
this.reset()
this.open = false;
this.reset();
},
// 表单重置
reset() {
@@ -710,172 +735,175 @@ export default {
phonenumber: undefined,
email: undefined,
sex: undefined,
status: '0',
status: "0",
remark: undefined,
language: undefined,
birthday: undefined,
joinedDate: undefined,
postIds: [],
roleIds: []
}
this.resetForm('form')
roleIds: [],
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.queryParams.deptId = undefined
this.$refs.tree.setCurrentKey(null)
this.handleQuery()
this.dateRange = [];
this.resetForm("queryForm");
this.queryParams.deptId = undefined;
this.$refs.tree.setCurrentKey(null);
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.userId)
this.single = selection.length != 1
this.multiple = !selection.length
this.ids = selection.map((item) => item.userId);
this.single = selection.length != 1;
this.multiple = !selection.length;
},
// 更多操作触发
handleCommand(command, row) {
switch (command) {
case 'handleResetPwd':
this.handleResetPwd(row)
break
case 'handleAuthRole':
this.handleAuthRole(row)
break
case "handleResetPwd":
this.handleResetPwd(row);
break;
case "handleAuthRole":
this.handleAuthRole(row);
break;
default:
break
break;
}
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.reset();
getUser().then((response) => {
this.postOptions = response.data.posts
this.roleOptions = response.data.roles
this.open = true
this.title = '添加用户'
this.form.password = this.initPassword
})
this.postOptions = response.data.posts;
this.roleOptions = response.data.roles;
this.open = true;
this.title = "添加用户";
this.form.password = this.initPassword;
});
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const userId = row.userId || this.ids
this.reset();
const userId = row.userId || this.ids;
getUser(userId).then((response) => {
this.form = response.data.user
this.postOptions = response.data.posts
this.roleOptions = response.data.roles
this.$set(this.form, 'postIds', response.data.postIds)
this.$set(this.form, 'roleIds', response.data.roleIds)
this.open = true
this.title = '修改用户'
this.form.password = ''
})
this.form = response.data.user;
this.postOptions = response.data.posts;
this.roleOptions = response.data.roles;
this.$set(this.form, "postIds", response.data.postIds);
this.$set(this.form, "roleIds", response.data.roleIds);
this.open = true;
this.title = "修改用户";
this.form.password = "";
});
},
/** 重置密码按钮操作 */
handleResetPwd(row) {
this.$prompt('请输入"' + row.userName + '"的新密码', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
this.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
closeOnClickModal: false,
inputPattern: /^.{5,20}$/,
inputErrorMessage: '用户密码长度必须介于 5 和 20 之间'
inputErrorMessage: "用户密码长度必须介于 5 和 20 之间",
})
.then(({ value }) => {
resetUserPwd(row.userId, value).then((response) => {
this.$modal.msgSuccess('修改成功,新密码是:' + value)
})
this.$modal.msgSuccess("修改成功,新密码是:" + value);
});
})
.catch(() => {})
.catch(() => {});
},
/** 分配角色操作 */
handleAuthRole: function(row) {
const userId = row.userId
this.$router.push('/system/user-auth/role/' + userId)
handleAuthRole: function (row) {
const userId = row.userId;
this.$router.push("/system/user-auth/role/" + userId);
},
/** 提交按钮 */
submitForm: function() {
this.$refs['form'].validate((valid) => {
submitForm: function () {
this.$refs["form"].validate((valid) => {
if (valid) {
if (this.form.userId != undefined) {
console.log(this.form)
console.log(this.form);
updateUser(this.form).then((response) => {
this.$modal.msgSuccess('修改成功')
this.open = false
this.getList()
})
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addUser(this.form).then((response) => {
this.$modal.msgSuccess('新增成功')
this.open = false
this.getList()
})
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
})
});
},
/** 删除按钮操作 */
handleDelete(row) {
const userIds = row.userId || this.ids
const userIds = row.userId || this.ids;
this.$modal
.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?')
.then(function() {
return delUser(userIds)
.then(function () {
return delUser(userIds);
})
.then(() => {
this.getList()
this.$modal.msgSuccess('删除成功')
this.getList();
this.$modal.msgSuccess("删除成功");
})
.catch(() => {})
.catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.download(
'system/user/export',
"system/user/export",
{
...this.queryParams
...this.queryParams,
},
`user_${new Date().getTime()}.xlsx`
)
);
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = '用户导入'
this.upload.open = true
this.upload.title = "用户导入";
this.upload.open = true;
},
/** 下载模板操作 */
importTemplate() {
this.download(
'system/user/importTemplate',
"system/user/importTemplate",
{},
`user_template_${new Date().getTime()}.xlsx`
)
);
},
// 文件上传中处理
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true
this.upload.isUploading = true;
},
// 文件上传成功处理
handleFileSuccess(response, file, fileList) {
this.upload.open = false
this.upload.isUploading = false
this.$refs.upload.clearFiles()
this.upload.open = false;
this.upload.isUploading = false;
this.$refs.upload.clearFiles();
this.$alert(
"<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
response.msg +
'</div>',
'导入结果',
"</div>",
"导入结果",
{ dangerouslyUseHTMLString: true }
)
this.getList()
);
this.getList();
},
// 提交上传文件
submitFileForm() {
this.$refs.upload.submit()
}
}
}
this.$refs.upload.submit();
},
},
};
</script>

Carregando…
Cancelar
Salvar