mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-12 03:01:12 +00:00
perf: 录音转文字支持自定义语言
This commit is contained in:
parent
e53b65496f
commit
e34aa77a54
@ -1356,7 +1356,15 @@ class DialogController extends AbstractController
|
|||||||
*
|
*
|
||||||
* @apiParam {String} base64 语音base64
|
* @apiParam {String} base64 语音base64
|
||||||
* @apiParam {Number} duration 语音时长(毫秒)
|
* @apiParam {Number} duration 语音时长(毫秒)
|
||||||
* @apiParam {String} [language] 语音语言(比如:zh,默认:当前用户语言)
|
* @apiParam {String} [language] 识别语言
|
||||||
|
* - 比如:zh
|
||||||
|
* - 默认:自动识别
|
||||||
|
* - 格式:符合 ISO_639 标准
|
||||||
|
* - 此参数不一定起效果,AI会根据语音和language参考翻译识别结果
|
||||||
|
* @apiParam {String} [translate] 翻译识别结果
|
||||||
|
* - 比如:zh
|
||||||
|
* - 默认:不翻译结果
|
||||||
|
* - 格式:符合 ISO_639 标准
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -1370,10 +1378,12 @@ class DialogController extends AbstractController
|
|||||||
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/";
|
$path = "uploads/tmp/chat/" . date("Ym") . "/" . $user->userid . "/";
|
||||||
$base64 = Request::input('base64');
|
$base64 = Request::input('base64');
|
||||||
$language = Request::input('language');
|
$language = Request::input('language');
|
||||||
|
$translate = Request::input('translate');
|
||||||
$duration = intval(Request::input('duration'));
|
$duration = intval(Request::input('duration'));
|
||||||
if ($duration < 600) {
|
if ($duration < 600) {
|
||||||
return Base::retError('说话时间太短');
|
return Base::retError('说话时间太短');
|
||||||
}
|
}
|
||||||
|
// 保存录音
|
||||||
$data = Base::record64save([
|
$data = Base::record64save([
|
||||||
"base64" => $base64,
|
"base64" => $base64,
|
||||||
"path" => $path,
|
"path" => $path,
|
||||||
@ -1382,28 +1392,30 @@ class DialogController extends AbstractController
|
|||||||
return Base::retError($data['msg']);
|
return Base::retError($data['msg']);
|
||||||
}
|
}
|
||||||
$recordData = $data['data'];
|
$recordData = $data['data'];
|
||||||
|
// 转文字
|
||||||
$extParams = [];
|
$extParams = [];
|
||||||
if ($language) {
|
if ($language) {
|
||||||
$targetLanguage = Doo::getLanguages($language);
|
|
||||||
if (empty($targetLanguage)) {
|
|
||||||
return Base::retError("参数错误");
|
|
||||||
}
|
|
||||||
$extParams = [
|
$extParams = [
|
||||||
'language' => match ($language) {
|
'language' => $language === 'zh-CHT' ? 'zh' : $language,
|
||||||
'zh-CHT' => 'zh',
|
'prompt' => "将此语音识别为“" . Doo::getLanguages($language) . "”。",
|
||||||
default => $language,
|
|
||||||
},
|
|
||||||
'prompt' => "此音频为“{$targetLanguage}”语言。",
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$res = Extranet::openAItranscriptions($recordData['file'], $extParams);
|
$result = Extranet::openAItranscriptions($recordData['file'], $extParams);
|
||||||
if (Base::isError($res)) {
|
if (Base::isError($result)) {
|
||||||
return $res;
|
return $result;
|
||||||
}
|
}
|
||||||
if (strlen($res['data']) < 1) {
|
if (strlen($result['data']) < 1) {
|
||||||
return Base::retError('转文字失败');
|
return Base::retError('转文字失败');
|
||||||
}
|
}
|
||||||
return $res;
|
// 翻译
|
||||||
|
if ($translate) {
|
||||||
|
$result = Extranet::openAItranslations($result['data'], Doo::getLanguages($translate));
|
||||||
|
if (Base::isError($result)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 返回
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -28,7 +28,6 @@ class Extranet
|
|||||||
if ($systemSetting['voice2text'] !== 'open' || empty($aibotSetting['openai_key'])) {
|
if ($systemSetting['voice2text'] !== 'open' || empty($aibotSetting['openai_key'])) {
|
||||||
return Base::retError("语音转文字功能未开启");
|
return Base::retError("语音转文字功能未开启");
|
||||||
}
|
}
|
||||||
//
|
|
||||||
$extra = [
|
$extra = [
|
||||||
'Content-Type' => 'multipart/form-data',
|
'Content-Type' => 'multipart/form-data',
|
||||||
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
||||||
@ -41,7 +40,6 @@ class Extranet
|
|||||||
'file' => new \CURLFile($filePath),
|
'file' => new \CURLFile($filePath),
|
||||||
'model' => 'whisper-1',
|
'model' => 'whisper-1',
|
||||||
]);
|
]);
|
||||||
// 转文字
|
|
||||||
$cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extra) . '_' . Base::array2json($extParams));
|
$cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extra) . '_' . Base::array2json($extParams));
|
||||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
|
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
|
||||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', $post, $extra, 15);
|
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', $post, $extra, 15);
|
||||||
@ -56,12 +54,6 @@ class Extranet
|
|||||||
});
|
});
|
||||||
if (Base::isError($result)) {
|
if (Base::isError($result)) {
|
||||||
Cache::forget($cacheKey);
|
Cache::forget($cacheKey);
|
||||||
} elseif ($extParams['language']) {
|
|
||||||
// 翻译
|
|
||||||
$translResult = self::openAItranslations($result['data'], Doo::getLanguages($extParams['language']));
|
|
||||||
if (Base::isSuccess($result)) {
|
|
||||||
$result = $translResult;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@ -92,11 +84,19 @@ class Extranet
|
|||||||
"messages" => [
|
"messages" => [
|
||||||
[
|
[
|
||||||
"role" => "system",
|
"role" => "system",
|
||||||
"content" => "你是一个专业的翻译器,请将<text>标签里面的内容翻译成“{$targetLanguage}”语言,翻译的结果尽量符合“项目任务管理系统”的使用并且保持原格式。"
|
"content" => <<<EOF
|
||||||
|
你是一名专业翻译人员,请将 <translation_original_text> 标签内的内容翻译为{$targetLanguage}。
|
||||||
|
|
||||||
|
翻译要求:
|
||||||
|
- 翻译结果需符合“项目任务管理系统”的专业术语和使用场景。
|
||||||
|
- 保持原文格式、结构和排版不变。
|
||||||
|
- 语言表达准确、简洁,符合项目管理领域的行业规范。
|
||||||
|
- 注意专业术语的一致性和连贯性。
|
||||||
|
EOF
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"role" => "user",
|
"role" => "user",
|
||||||
"content" => "<text>{$text}</text>"
|
"content" => "<translation_original_text>{$text}</translation_original_text>"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
@ -112,7 +112,7 @@ class Extranet
|
|||||||
}
|
}
|
||||||
$result = $resData['choices'][0]['message']['content'];
|
$result = $resData['choices'][0]['message']['content'];
|
||||||
$result = preg_replace('/^\"|\"$/', '', trim($result));
|
$result = preg_replace('/^\"|\"$/', '', trim($result));
|
||||||
$result = preg_replace('/^<text>|<\/text>$/', '', trim($result));
|
$result = preg_replace('/<\/*translation_original_text>/', '', trim($result));
|
||||||
if (empty($result)) {
|
if (empty($result)) {
|
||||||
return Base::retError("翻译失败", $result);
|
return Base::retError("翻译失败", $result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,14 +11,17 @@
|
|||||||
<div ref="icon" class="general-operation-icon"></div>
|
<div ref="icon" class="general-operation-icon"></div>
|
||||||
<EDropdownMenu ref="dropdownMenu" slot="dropdown" class="general-operation-more-dropdown menu-dropdown">
|
<EDropdownMenu ref="dropdownMenu" slot="dropdown" class="general-operation-more-dropdown menu-dropdown">
|
||||||
<li class="general-operation-more-warp small">
|
<li class="general-operation-more-warp small">
|
||||||
<ul>
|
<ul :style="ulStyle">
|
||||||
<EDropdownItem
|
<EDropdownItem
|
||||||
v-for="(item, key) in list"
|
v-for="(item, key) in list"
|
||||||
:key="key"
|
:key="key"
|
||||||
:command="item.value"
|
:command="item.value"
|
||||||
:divided="!!item.divided"
|
:divided="!!item.divided"
|
||||||
:disabled="active === item.value">
|
:disabled="active === item.value || !!item.disabled">
|
||||||
<div class="item">{{item.label}}</div>
|
<div class="item">{{item.label}}</div>
|
||||||
|
<div v-if="tickShow" class="tick">
|
||||||
|
<i v-if="active === item.value && !item.disabled" class="taskfont"></i>
|
||||||
|
</div>
|
||||||
</EDropdownItem>
|
</EDropdownItem>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@ -37,6 +40,8 @@ export default {
|
|||||||
active: '', // 当前选中的值
|
active: '', // 当前选中的值
|
||||||
onUpdate: null, // 选中后的回调函数
|
onUpdate: null, // 选中后的回调函数
|
||||||
scrollHide: false, // 滚动立即隐藏
|
scrollHide: false, // 滚动立即隐藏
|
||||||
|
tickShow: true, // 是否显示打勾
|
||||||
|
maxHeight: 0, // 滚动区域最大高度
|
||||||
|
|
||||||
element: null,
|
element: null,
|
||||||
target: null,
|
target: null,
|
||||||
@ -51,7 +56,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['menuOperation'])
|
...mapState(['menuOperation']),
|
||||||
|
|
||||||
|
ulStyle({maxHeight}) {
|
||||||
|
return maxHeight > 0 ? {maxHeight: `${maxHeight}px`} : {};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@ -72,6 +81,8 @@ export default {
|
|||||||
this.active = data.active && this.list.find(item => item.value === data.active) ? data.active : '';
|
this.active = data.active && this.list.find(item => item.value === data.active) ? data.active : '';
|
||||||
this.onUpdate = typeof data.onUpdate === "function" ? data.onUpdate : null;
|
this.onUpdate = typeof data.onUpdate === "function" ? data.onUpdate : null;
|
||||||
this.scrollHide = typeof data.scrollHide === "boolean" ? data.scrollHide : false;
|
this.scrollHide = typeof data.scrollHide === "boolean" ? data.scrollHide : false;
|
||||||
|
this.tickShow = typeof data.tickShow === "boolean" ? data.tickShow : true;
|
||||||
|
this.maxHeight = typeof data.maxHeight === "number" ? data.maxHeight : 0;
|
||||||
//
|
//
|
||||||
this.$refs.icon.focus();
|
this.$refs.icon.focus();
|
||||||
this.show();
|
this.show();
|
||||||
|
|||||||
@ -208,8 +208,9 @@
|
|||||||
<div class="convert-box">
|
<div class="convert-box">
|
||||||
<div class="convert-body">
|
<div class="convert-body">
|
||||||
<div class="convert-content">
|
<div class="convert-content">
|
||||||
<div class="convert-setting" @click="convertSetting">
|
<div class="convert-setting">
|
||||||
<i class="taskfont"></i>
|
<i class="taskfont" :class="{active: !!cacheTranscriptionLanguage}" @click="convertSetting('transcription', $event)"></i>
|
||||||
|
<i class="taskfont" :class="{active: !!recordConvertTranslate}" @click="convertSetting('translate', $event)"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="convert-input">
|
<div class="convert-input">
|
||||||
<Input
|
<Input
|
||||||
@ -390,7 +391,7 @@ export default {
|
|||||||
recordConvertFocus: false,
|
recordConvertFocus: false,
|
||||||
recordConvertStatus: 0, // 0: 转换中 1: 转换成功 2: 转换失败
|
recordConvertStatus: 0, // 0: 转换中 1: 转换成功 2: 转换失败
|
||||||
recordConvertResult: '',
|
recordConvertResult: '',
|
||||||
recordConvertLanguage: '',
|
recordConvertTranslate: '', // 转换之后翻译语言
|
||||||
|
|
||||||
touchStart: {},
|
touchStart: {},
|
||||||
touchFocus: false,
|
touchFocus: false,
|
||||||
@ -528,6 +529,7 @@ export default {
|
|||||||
'cacheDialogs',
|
'cacheDialogs',
|
||||||
'dialogMsgs',
|
'dialogMsgs',
|
||||||
|
|
||||||
|
'cacheTranscriptionLanguage',
|
||||||
'cacheKeyboard',
|
'cacheKeyboard',
|
||||||
'keyboardType',
|
'keyboardType',
|
||||||
'isModKey',
|
'isModKey',
|
||||||
@ -1408,7 +1410,8 @@ export default {
|
|||||||
dialog_id: this.dialogId,
|
dialog_id: this.dialogId,
|
||||||
base64: reader.result,
|
base64: reader.result,
|
||||||
duration: this.recordDuration,
|
duration: this.recordDuration,
|
||||||
language: this.recordConvertLanguage || getLanguage()
|
language: this.cacheTranscriptionLanguage,
|
||||||
|
translate: this.recordConvertTranslate
|
||||||
},
|
},
|
||||||
method: 'post',
|
method: 'post',
|
||||||
}).then(({data}) => {
|
}).then(({data}) => {
|
||||||
@ -1422,22 +1425,43 @@ export default {
|
|||||||
reader.readAsDataURL(this.recordBlob);
|
reader.readAsDataURL(this.recordBlob);
|
||||||
},
|
},
|
||||||
|
|
||||||
async convertSetting(event) {
|
async convertSetting(type, event) {
|
||||||
|
if (this.recordConvertStatus !== 1) {
|
||||||
|
$A.messageWarning("正在识别中,请稍后")
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
const list = Object.keys(languageList).map(item => ({
|
const list = Object.keys(languageList).map(item => ({
|
||||||
label: languageList[item],
|
label: languageList[item],
|
||||||
value: item
|
value: item
|
||||||
}))
|
}))
|
||||||
|
let active
|
||||||
|
if (type === 'transcription') {
|
||||||
|
// 语音转文字
|
||||||
list.unshift(...[
|
list.unshift(...[
|
||||||
|
{label: '选择识别语言', value: '', disabled: true},
|
||||||
{label: '自动识别', value: ''},
|
{label: '自动识别', value: ''},
|
||||||
])
|
])
|
||||||
|
active = this.cacheTranscriptionLanguage
|
||||||
|
} else {
|
||||||
|
// 翻译
|
||||||
|
list.unshift(...[
|
||||||
|
{label: '选择翻译结果', value: '', disabled: true},
|
||||||
|
{label: '不翻译结果', value: ''},
|
||||||
|
])
|
||||||
|
active = this.recordConvertTranslate
|
||||||
|
}
|
||||||
this.$store.state.menuOperation = {
|
this.$store.state.menuOperation = {
|
||||||
event,
|
event,
|
||||||
list,
|
list,
|
||||||
active: this.recordConvertLanguage,
|
active,
|
||||||
scrollHide: true,
|
scrollHide: true,
|
||||||
onUpdate: async (language) => {
|
onUpdate: async (language) => {
|
||||||
this.recordConvertLanguage = language
|
if (type === 'transcription') {
|
||||||
|
await this.$store.dispatch('setTranscriptionLanguage', language)
|
||||||
|
} else {
|
||||||
|
this.recordConvertTranslate = language
|
||||||
|
}
|
||||||
this.convertRecord()
|
this.convertRecord()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
resources/assets/js/store/actions.js
vendored
19
resources/assets/js/store/actions.js
vendored
@ -922,6 +922,7 @@ export default {
|
|||||||
cacheFileSort: await $A.IDBJson("cacheFileSort"),
|
cacheFileSort: await $A.IDBJson("cacheFileSort"),
|
||||||
cacheTaskBrowse: await $A.IDBArray("cacheTaskBrowse"),
|
cacheTaskBrowse: await $A.IDBArray("cacheTaskBrowse"),
|
||||||
cacheTranslationLanguage: await $A.IDBString("cacheTranslationLanguage"),
|
cacheTranslationLanguage: await $A.IDBString("cacheTranslationLanguage"),
|
||||||
|
cacheTranscriptionLanguage: await $A.IDBString("cacheTranscriptionLanguage"),
|
||||||
cacheTranslations: await $A.IDBArray("cacheTranslations"),
|
cacheTranslations: await $A.IDBArray("cacheTranslations"),
|
||||||
cacheEmojis: await $A.IDBArray("cacheEmojis"),
|
cacheEmojis: await $A.IDBArray("cacheEmojis"),
|
||||||
userInfo: await $A.IDBJson("userInfo"),
|
userInfo: await $A.IDBJson("userInfo"),
|
||||||
@ -957,7 +958,8 @@ export default {
|
|||||||
string: [
|
string: [
|
||||||
'clientId',
|
'clientId',
|
||||||
'cacheServerUrl',
|
'cacheServerUrl',
|
||||||
'cacheTranslationLanguage'
|
'cacheTranslationLanguage',
|
||||||
|
'cacheTranscriptionLanguage'
|
||||||
],
|
],
|
||||||
array: [
|
array: [
|
||||||
'cacheUserBasic',
|
'cacheUserBasic',
|
||||||
@ -1002,6 +1004,11 @@ export default {
|
|||||||
state.cacheTranslationLanguage = languageName;
|
state.cacheTranslationLanguage = languageName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TranscriptionLanguage检查
|
||||||
|
if (typeof languageList[state.cacheTranscriptionLanguage] === "undefined") {
|
||||||
|
state.cacheTranscriptionLanguage = '';
|
||||||
|
}
|
||||||
|
|
||||||
// 处理用户信息
|
// 处理用户信息
|
||||||
if (state.userInfo.userid) {
|
if (state.userInfo.userid) {
|
||||||
state.userId = state.userInfo.userid = $A.runNum(state.userInfo.userid);
|
state.userId = state.userInfo.userid = $A.runNum(state.userInfo.userid);
|
||||||
@ -3627,6 +3634,16 @@ export default {
|
|||||||
$A.IDBSave('cacheTranslationLanguage', language);
|
$A.IDBSave('cacheTranslationLanguage', language);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置语音转文字语言
|
||||||
|
* @param state
|
||||||
|
* @param language
|
||||||
|
*/
|
||||||
|
setTranscriptionLanguage({state}, language) {
|
||||||
|
state.cacheTranscriptionLanguage = language
|
||||||
|
$A.IDBSave('cacheTranscriptionLanguage', language);
|
||||||
|
},
|
||||||
|
|
||||||
/** *****************************************************************************************/
|
/** *****************************************************************************************/
|
||||||
/** ************************************* loads *********************************************/
|
/** ************************************* loads *********************************************/
|
||||||
/** *****************************************************************************************/
|
/** *****************************************************************************************/
|
||||||
|
|||||||
3
resources/assets/js/store/state.js
vendored
3
resources/assets/js/store/state.js
vendored
@ -245,6 +245,9 @@ export default {
|
|||||||
cacheTranslationLanguage: '',
|
cacheTranslationLanguage: '',
|
||||||
cacheTranslations: [],
|
cacheTranslations: [],
|
||||||
|
|
||||||
|
// 语音转文字(识别语言)
|
||||||
|
cacheTranscriptionLanguage: '',
|
||||||
|
|
||||||
// 下拉菜单操作
|
// 下拉菜单操作
|
||||||
menuOperation: {}
|
menuOperation: {}
|
||||||
};
|
};
|
||||||
|
|||||||
11
resources/assets/sass/dark.scss
vendored
11
resources/assets/sass/dark.scss
vendored
@ -580,6 +580,17 @@ body.dark-mode-reverse {
|
|||||||
.chat-input-convert-transfer {
|
.chat-input-convert-transfer {
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
.convert-box {
|
.convert-box {
|
||||||
|
.convert-body {
|
||||||
|
.convert-content {
|
||||||
|
.convert-setting {
|
||||||
|
> i {
|
||||||
|
&.active {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.convert-footer {
|
.convert-footer {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
> li {
|
> li {
|
||||||
|
|||||||
@ -768,11 +768,15 @@
|
|||||||
width: 88%;
|
width: 88%;
|
||||||
transform: translateY(12px);
|
transform: translateY(12px);
|
||||||
.convert-setting {
|
.convert-setting {
|
||||||
|
margin: 0 2px 8px 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
> i {
|
||||||
color: #4d4d4d;
|
color: #4d4d4d;
|
||||||
background-color: #c7c7c7;
|
background-color: #c7c7c7;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin: 0 2px 8px 0;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
@ -780,8 +784,12 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
> i {
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
&.active {
|
||||||
|
background-color: $primary-color;
|
||||||
|
color: #ffffff;
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.convert-input {
|
.convert-input {
|
||||||
|
|||||||
@ -32,6 +32,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> li {
|
> li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -63,6 +67,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tick {
|
||||||
|
color: $primary-color;
|
||||||
|
transform: translateX(40%);
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 6px;
|
||||||
|
> i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.flow {
|
.flow {
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user