mirror of
https://github.com/kuaifan/dootask.git
synced 2026-02-08 23:45:35 +00:00
faet: 新增文本消息长按翻译功能
This commit is contained in:
parent
a4a9ab8d2d
commit
0c64cf0546
@ -2,12 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Tasks\PushTask;
|
||||
use DB;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Request;
|
||||
use Redirect;
|
||||
use Carbon\Carbon;
|
||||
use App\Tasks\PushTask;
|
||||
use App\Models\File;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
@ -20,6 +19,8 @@ use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Models\WebSocketDialogMsgRead;
|
||||
use App\Models\WebSocketDialogMsgTodo;
|
||||
use App\Models\WebSocketDialogMsgTranslate;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
|
||||
/**
|
||||
* @apiDefine dialog
|
||||
@ -1539,6 +1540,75 @@ class DialogController extends AbstractController
|
||||
return Base::retSuccess("success", $msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/translation 31. 翻译消息
|
||||
*
|
||||
* @apiDescription 将文本消息翻译成当前语言,需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup dialog
|
||||
* @apiName msg__translation
|
||||
*
|
||||
* @apiParam {Number} msg_id 消息ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function msg__translation()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$msg_id = intval(Request::input("msg_id"));
|
||||
$language = Base::headerOrInput('language');
|
||||
$targetLanguage = match ($language) {
|
||||
"zh" => "简体中文",
|
||||
"zh-CHT" => "繁体中文",
|
||||
"en" => "英语",
|
||||
"ko" => "韩语",
|
||||
"ja" => "日语",
|
||||
"de" => "德语",
|
||||
"fr" => "法语",
|
||||
"id" => "印度尼西亚语",
|
||||
"ru" => "俄语",
|
||||
default => '',
|
||||
};
|
||||
//
|
||||
if (empty($targetLanguage)) {
|
||||
return Base::retError("参数错误");
|
||||
}
|
||||
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
|
||||
if (empty($msg)) {
|
||||
return Base::retError("消息不存在或已被删除");
|
||||
}
|
||||
if (!in_array($msg->type, ['text', 'record'])) {
|
||||
return Base::retError("此消息不支持翻译");
|
||||
}
|
||||
WebSocketDialog::checkDialog($msg->dialog_id);
|
||||
//
|
||||
$row = WebSocketDialogMsgTranslate::whereMsgId($msg_id)->whereLanguage($language)->first();
|
||||
if ($row) {
|
||||
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
|
||||
}
|
||||
//
|
||||
$msgData = Base::json2array($msg->getRawOriginal('msg'));
|
||||
if (empty($msgData['text'])) {
|
||||
return Base::retError("消息内容为空");
|
||||
}
|
||||
$res = Extranet::openAItranslations($msgData['text'], $targetLanguage);
|
||||
if (Base::isError($res)) {
|
||||
return $res;
|
||||
}
|
||||
$row = WebSocketDialogMsgTranslate::createInstance([
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg_id,
|
||||
'language' => $language,
|
||||
'content' => $res['data'],
|
||||
]);
|
||||
$row->save();
|
||||
//
|
||||
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/dialog/msg/mark 32. 消息标记操作
|
||||
*
|
||||
|
||||
@ -40,7 +40,7 @@ class SystemController extends AbstractController
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - all: 获取所有(需要管理员权限)
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home'])
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home'])
|
||||
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@ -67,6 +67,7 @@ class SystemController extends AbstractController
|
||||
'chat_information',
|
||||
'anon_message',
|
||||
'voice2text',
|
||||
'translation',
|
||||
'e2e_message',
|
||||
'auto_archived',
|
||||
'archived_day',
|
||||
@ -97,6 +98,9 @@ class SystemController extends AbstractController
|
||||
if ($all['voice2text'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
|
||||
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
}
|
||||
if ($all['translation'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
|
||||
return Base::retError('开启翻译功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
}
|
||||
$setting = Base::setting('system', Base::newTrim($all));
|
||||
} else {
|
||||
$setting = Base::setting('system');
|
||||
@ -118,6 +122,7 @@ class SystemController extends AbstractController
|
||||
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
|
||||
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
|
||||
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
|
||||
$setting['translation'] = $setting['translation'] ?: 'close';
|
||||
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
|
||||
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
|
||||
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
|
||||
|
||||
36
app/Models/WebSocketDialogMsgTranslate.php
Normal file
36
app/Models/WebSocketDialogMsgTranslate.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogMsgTranslate
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $msg_id 消息ID
|
||||
* @property string|null $language 语言
|
||||
* @property string|null $content 翻译内容
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereContent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereLanguage($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereMsgId($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogMsgTranslate extends AbstractModel
|
||||
{
|
||||
function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->timestamps = false;
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,59 @@ class Extranet
|
||||
return Base::retSuccess("success", $resData['text']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 翻译
|
||||
* @param $text
|
||||
* @param $targetLanguage
|
||||
* @return array
|
||||
*/
|
||||
public static function openAItranslations($text, $targetLanguage)
|
||||
{
|
||||
$systemSetting = Base::setting('system');
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
if ($systemSetting['translation'] !== 'open' || empty($aibotSetting['openai_key'])) {
|
||||
return Base::retError("翻译功能未开启");
|
||||
}
|
||||
$extra = [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
||||
];
|
||||
if ($aibotSetting['openai_agency']) {
|
||||
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
|
||||
} else {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
|
||||
}
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||
"model" => "gpt-3.5-turbo",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言。"
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => $text
|
||||
]
|
||||
]
|
||||
]), $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("翻译失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['choices'])) {
|
||||
return Base::retError("翻译失败", $resData);
|
||||
}
|
||||
$result = $resData['choices'][0]['message']['content'];
|
||||
$result = preg_replace('/^\"|\"$/', '', $result);
|
||||
if (empty($result)) {
|
||||
return Base::retError("翻译失败", $result);
|
||||
}
|
||||
return Base::retSuccess("success", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址经纬度
|
||||
* @param string $ip
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWebSocketDialogMsgTranslatesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('web_socket_dialog_msg_translates', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('dialog_id')->nullable()->default(0)->comment('对话ID');
|
||||
$table->bigInteger('msg_id')->nullable()->default(0)->comment('消息ID');
|
||||
$table->string('language', 50)->nullable()->default('')->comment('语言');
|
||||
$table->longText('content')->nullable()->comment('翻译内容');
|
||||
$table->index(['msg_id', 'language']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('web_socket_dialog_msg_translates');
|
||||
}
|
||||
}
|
||||
@ -26,11 +26,11 @@
|
||||
<!--详情-->
|
||||
<div ref="content" class="dialog-content" :class="contentClass">
|
||||
<!--文本-->
|
||||
<TextMsg v-if="msgData.type === 'text'" :msg="msgData.msg" @viewText="viewText"/>
|
||||
<TextMsg v-if="msgData.type === 'text'" :msgId="msgData.id" :msg="msgData.msg" @viewText="viewText"/>
|
||||
<!--文件-->
|
||||
<FileMsg v-else-if="msgData.type === 'file'" :msg="msgData.msg" @viewFile="viewFile" @downFile="downFile"/>
|
||||
<!--录音-->
|
||||
<RecordMsg v-else-if="msgData.type === 'record'" :msg="msgData.msg" @playRecord="playRecord"/>
|
||||
<RecordMsg v-else-if="msgData.type === 'record'" :msgId="msgData.id" :msg="msgData.msg" @playRecord="playRecord"/>
|
||||
<!--会议-->
|
||||
<MeetingMsg v-else-if="msgData.type === 'meeting'" :msg="msgData.msg" @openMeeting="openMeeting"/>
|
||||
<!--接龙-->
|
||||
|
||||
@ -4,21 +4,45 @@
|
||||
<div class="record-time">{{recordDuration(msg.duration)}}</div>
|
||||
<div class="record-icon taskfont"></div>
|
||||
</div>
|
||||
<div v-if="msg.text" class="dialog-record-text">
|
||||
{{msg.text}}
|
||||
</div>
|
||||
|
||||
<template v-if="msg.text">
|
||||
<div class="content-divider">
|
||||
<span class="divider-full"></span>
|
||||
</div>
|
||||
<div class="content-additional">{{msg.text}}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="translation">
|
||||
<div class="content-divider">
|
||||
<span></span>
|
||||
<div class="divider-label">{{ translation.label }}</div>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="content-additional">{{translation.value}}</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {mapState} from "vuex";
|
||||
import DialogMarkdown from "../DialogMarkdown.vue";
|
||||
import {languageName} from "../../../../language";
|
||||
|
||||
export default {
|
||||
components: {DialogMarkdown},
|
||||
props: {
|
||||
msgId: Number,
|
||||
msg: Object,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['audioPlaying']),
|
||||
...mapState(['audioPlaying', 'cacheTranslations']),
|
||||
|
||||
translation() {
|
||||
const translation = this.cacheTranslations.find(item => {
|
||||
return item.key === `msg-${this.msgId}` && item.lang === languageName;
|
||||
});
|
||||
return translation ? translation : null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
playRecord() {
|
||||
|
||||
@ -2,17 +2,40 @@
|
||||
<div class="content-text no-dark-content">
|
||||
<DialogMarkdown v-if="msg.type === 'md'" @click="viewText" :text="msg.text"/>
|
||||
<pre v-else @click="viewText" v-html="$A.formatTextMsg(msg.text, userId)"></pre>
|
||||
|
||||
<template v-if="translation">
|
||||
<div class="content-divider">
|
||||
<span></span>
|
||||
<div class="divider-label">{{ translation.label }}</div>
|
||||
<span></span>
|
||||
</div>
|
||||
<DialogMarkdown v-if="msg.type === 'md'" :text="translation.value"/>
|
||||
<pre v-else v-html="$A.formatTextMsg(translation.value, userId)"></pre>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {mapState} from "vuex";
|
||||
import DialogMarkdown from "../DialogMarkdown.vue";
|
||||
import {languageName} from "../../../../language";
|
||||
|
||||
export default {
|
||||
components: {DialogMarkdown},
|
||||
props: {
|
||||
msgId: Number,
|
||||
msg: Object,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['cacheTranslations']),
|
||||
|
||||
translation() {
|
||||
const translation = this.cacheTranslations.find(item => {
|
||||
return item.key === `msg-${this.msgId}` && item.lang === languageName;
|
||||
});
|
||||
return translation ? translation : null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
viewText(e) {
|
||||
this.$emit('viewText', e);
|
||||
|
||||
@ -310,6 +310,10 @@
|
||||
<i class="taskfont"></i>
|
||||
<span>{{ $L('转文字') }}</span>
|
||||
</li>
|
||||
<li v-if="actionPermission(operateItem, 'translation')" @click="onOperate('translation')">
|
||||
<i class="taskfont"></i>
|
||||
<span>{{ $L('翻译') }}</span>
|
||||
</li>
|
||||
<li v-for="item in operateCopys" @click="onOperate('copy', item)">
|
||||
<i class="taskfont" v-html="item.icon"></i>
|
||||
<span>{{ $L(item.label) }}</span>
|
||||
@ -2897,6 +2901,10 @@ export default {
|
||||
this.onVoice2text()
|
||||
break;
|
||||
|
||||
case "translation":
|
||||
this.onTranslation()
|
||||
break;
|
||||
|
||||
case "copy":
|
||||
this.onCopy(value)
|
||||
break;
|
||||
@ -3020,6 +3028,9 @@ export default {
|
||||
return;
|
||||
}
|
||||
const {id: msg_id} = this.operateItem
|
||||
if (this.isLoad(`msg-${msg_id}`)) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch("setLoad", `msg-${msg_id}`)
|
||||
this.$store.dispatch("call", {
|
||||
url: 'dialog/msg/voice2text',
|
||||
@ -3035,6 +3046,32 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
onTranslation() {
|
||||
if (!this.actionPermission(this.operateItem, 'translation')) {
|
||||
return;
|
||||
}
|
||||
const {id: msg_id} = this.operateItem
|
||||
if (this.isLoad(`msg-${msg_id}`)) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch("setLoad", `msg-${msg_id}`)
|
||||
this.$store.dispatch("call", {
|
||||
url: 'dialog/msg/translation',
|
||||
data: {
|
||||
msg_id
|
||||
},
|
||||
}).then(({data}) => {
|
||||
this.$store.dispatch("saveTranslation", {
|
||||
key: `msg-${msg_id}`,
|
||||
value: data.content,
|
||||
});
|
||||
}).catch(({msg}) => {
|
||||
$A.messageError(msg);
|
||||
}).finally(_ => {
|
||||
this.$store.dispatch("cancelLoad", `msg-${msg_id}`)
|
||||
});
|
||||
},
|
||||
|
||||
onCopy(data) {
|
||||
if (!$A.isJson(data)) {
|
||||
return
|
||||
@ -3621,10 +3658,10 @@ export default {
|
||||
if (item.msg.text) {
|
||||
return false;
|
||||
}
|
||||
if (this.isLoad(`msg-${item.id}`)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'translation':
|
||||
return ['text', 'record'].includes(item.type) && item.msg.text // 文本、语音消息 支持翻译
|
||||
}
|
||||
return true // 返回 true 允许操作
|
||||
},
|
||||
|
||||
@ -189,6 +189,14 @@
|
||||
<div v-if="formDatum.voice2text == 'open'" class="form-tip">{{$L('长按语音消息可转换成文字。')}} ({{$L('需要在应用中开启 ChatGPT AI 机器人')}})</div>
|
||||
<div v-else class="form-tip">{{$L('关闭语音转文字功能。')}}</div>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('翻译消息')" prop="translation">
|
||||
<RadioGroup v-model="formDatum.translation">
|
||||
<Radio label="open">{{$L('开启')}}</Radio>
|
||||
<Radio label="close">{{$L('关闭')}}</Radio>
|
||||
</RadioGroup>
|
||||
<div v-if="formDatum.translation == 'open'" class="form-tip">{{$L('长按文本消息可翻译成当前设置的语言。')}} ({{$L('需要在应用中开启 ChatGPT AI 机器人')}})</div>
|
||||
<div v-else class="form-tip">{{$L('关闭文本消息翻译功能。')}}</div>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('端到端加密')" prop="e2eMessage">
|
||||
<RadioGroup v-model="formDatum.e2e_message">
|
||||
<Radio label="open">{{$L('开启')}}</Radio>
|
||||
|
||||
26
resources/assets/js/store/actions.js
vendored
26
resources/assets/js/store/actions.js
vendored
@ -1,6 +1,6 @@
|
||||
import {Store} from 'le5le-store';
|
||||
import * as openpgp from 'openpgp_hi/lightweight';
|
||||
import {languageName} from "../language";
|
||||
import {languageList, languageName} from "../language";
|
||||
import {$callData, $urlSafe, SSEClient} from './utils'
|
||||
|
||||
export default {
|
||||
@ -826,6 +826,7 @@ export default {
|
||||
const cacheLoginEmail = await $A.IDBString("cacheLoginEmail");
|
||||
const cacheFileSort = await $A.IDBJson("cacheFileSort");
|
||||
const cacheTaskBrowse = await $A.IDBArray("cacheTaskBrowse")
|
||||
const cacheTranslations = await $A.IDBArray("cacheTranslations")
|
||||
const cacheEmojis = await $A.IDBArray("cacheEmojis")
|
||||
const userInfo = await $A.IDBJson("userInfo")
|
||||
await $A.IDBClear();
|
||||
@ -835,6 +836,7 @@ export default {
|
||||
await $A.IDBSet("cacheLoginEmail", cacheLoginEmail);
|
||||
await $A.IDBSet("cacheFileSort", cacheFileSort);
|
||||
await $A.IDBSet("cacheTaskBrowse", cacheTaskBrowse);
|
||||
await $A.IDBSet("cacheTranslations", cacheTranslations);
|
||||
await $A.IDBSet("cacheEmojis", cacheEmojis);
|
||||
await $A.IDBSet("cacheVersion", state.cacheVersion)
|
||||
|
||||
@ -865,6 +867,7 @@ export default {
|
||||
state.cacheTasks = await $A.IDBArray("cacheTasks")
|
||||
state.cacheProjectParameter = await $A.IDBArray("cacheProjectParameter")
|
||||
state.cacheTaskBrowse = await $A.IDBArray("cacheTaskBrowse")
|
||||
state.cacheTranslations = await $A.IDBArray("cacheTranslations")
|
||||
state.dialogMsgs = await $A.IDBArray("dialogMsgs")
|
||||
state.fileLists = await $A.IDBArray("fileLists")
|
||||
state.userInfo = await $A.IDBJson("userInfo")
|
||||
@ -3341,6 +3344,27 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存翻译
|
||||
* @param state
|
||||
* @param dispatch
|
||||
* @param data {key, value}
|
||||
*/
|
||||
saveTranslation({state, dispatch}, data) {
|
||||
if (!$A.isJson(data)) {
|
||||
return
|
||||
}
|
||||
const item = state.cacheTranslations.find(item => item.key == data.key && item.lang == languageName)
|
||||
if (item) {
|
||||
item.value = data.value
|
||||
} else {
|
||||
data.lang = languageName
|
||||
data.label = languageList[languageName] || languageName
|
||||
state.cacheTranslations.push(data)
|
||||
}
|
||||
$A.IDBSave("cacheTranslations", state.cacheTranslations.slice(-200))
|
||||
},
|
||||
|
||||
/** *****************************************************************************************/
|
||||
/** ************************************* loads *********************************************/
|
||||
/** *****************************************************************************************/
|
||||
|
||||
5
resources/assets/js/store/state.js
vendored
5
resources/assets/js/store/state.js
vendored
@ -231,5 +231,8 @@ export default {
|
||||
model: 'details',
|
||||
id: 0,
|
||||
show: false
|
||||
}
|
||||
},
|
||||
|
||||
// 翻译
|
||||
cacheTranslations: [],
|
||||
};
|
||||
|
||||
@ -1096,22 +1096,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-record-text {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-meeting {
|
||||
@ -1412,6 +1396,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
.content-divider {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 6px 0;
|
||||
> span {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: scaleY(0.5);
|
||||
min-width: 18px;
|
||||
}
|
||||
.divider-label {
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.mention {
|
||||
color: $flow-status-end-color;
|
||||
background-color: transparent;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user