mirror of
https://github.com/kuaifan/dootask.git
synced 2026-07-02 12:25:14 +00:00
fix(frontend): TagInput 体质优化与多分隔符支持 + 后端 saveOrIgnore 加固
TagInput:
- cut prop 支持字符串或字符串数组,多分隔符任一匹配即切;
v-model 写回用第一个分隔符 (joinChar) 拼接保持向后兼容
- 修复 max 仅在 cut 分隔符路径生效,回车/blur/paste 路径未查 max 的 bug
- 修复 data() 初始化与 watch(value) 用硬编码 ',' 切分,
统一走 joinChar (单一输出分隔符) 还原 v-model,避免含空格的列名
(如项目模板 'Sprint 1') 被切碎
- 修复 watch(value) 空值/null 时不清空 disSource 的潜在残留数据
- v-for 加 :key='text',配合初始化去重避免拖拽错位
- 重复 tag 由静默忽略改为提示「该标签已存在」
- 编辑 tag 加 dedup 检查,重命名成已有 tag 文本时阻止关闭
- 粘贴改为按 cut 切分逐个 addTag;addTag 返回 boolean,
pasteText 在 max 满时 break 短路
- splitByCuts/parseValue 拆分:回切用 joinChar,paste/输入用 cutPattern
- 拼接翻译 '最多只能添加X个' 改为 $L('最多只能添加(*)个', max);
新增「最多只能添加(*)个」「该标签已存在」到 language/original-web.txt
manage.vue:
- 创建项目任务列表启用 :cut=\"[',', ',', ' ']\",支持半角/全角逗号/空格
IME (compositionstart/end 时序不可靠,统一走 @on-keydown + W3C 标准 229 守卫):
- login/ProjectPanel/TaskAdd/UserTagsModal 的 onXxxKeydown 加
isComposing || key=='Process' || keyCode==229 三重守卫,
与 TagInput.downEnter 风格统一
- file.vue 块模式重命名 onKeydown 用 \$nextTick 包 onEnter,
避免 keydown 早于 input 事件 commit v-model 导致少一个字符
AbstractModel.performInsertOrIgnore:
- \$uniqueBy 不为 null 时抛 InvalidArgumentException;
MySQL INSERT IGNORE 无法按指定列 scope 冲突,与框架 ON CONFLICT 语义不一致
- lastInsertId 仅在 > 0 时回填主键;
避免在无 auto_increment 列的表上把业务设置的 PK 覆盖为 0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3f5078ec9b
commit
27c65cc582
@ -217,6 +217,12 @@ class AbstractModel extends Model
|
||||
*/
|
||||
protected function performInsertOrIgnore(Builder $query, array|string|null $uniqueBy)
|
||||
{
|
||||
// MySQL INSERT IGNORE 无法按指定列限制冲突范围,所有 unique 冲突一并吞掉。
|
||||
// 若调用方传了 $uniqueBy 期望精确 scope,这里直接抛错,避免与框架语义偷偷不一致。
|
||||
if ($uniqueBy !== null) {
|
||||
throw new \InvalidArgumentException('saveOrIgnore $uniqueBy is not supported on MySQL driver; pass null.');
|
||||
}
|
||||
|
||||
if ($this->usesUniqueIds()) {
|
||||
$this->setUniqueIds();
|
||||
}
|
||||
@ -240,10 +246,12 @@ class AbstractModel extends Model
|
||||
}
|
||||
|
||||
if ($this->getIncrementing()) {
|
||||
$this->setAttribute(
|
||||
$this->getKeyName(),
|
||||
$query->getConnection()->getPdo()->lastInsertId()
|
||||
);
|
||||
$lastId = $query->getConnection()->getPdo()->lastInsertId();
|
||||
// 无 auto_increment 列的表上 INSERT IGNORE 即使插入成功 lastInsertId 也返回 "0",
|
||||
// 别用它去覆盖业务设置的主键。
|
||||
if ($lastId > 0) {
|
||||
$this->setAttribute($this->getKeyName(), $lastId);
|
||||
}
|
||||
}
|
||||
|
||||
$this->exists = true;
|
||||
|
||||
@ -2467,3 +2467,5 @@ AI任务分析
|
||||
页面已切换,引导已结束
|
||||
步骤执行失败
|
||||
操作引导启动失败
|
||||
最多只能添加(*)个
|
||||
该标签已存在
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
tag="ul"
|
||||
draggable=".column-item"
|
||||
>
|
||||
<div class="tags-item column-item" v-for="(text, index) in disSource">
|
||||
<div class="tags-item column-item" v-for="(text, index) in disSource" :key="text">
|
||||
<span class="tags-content" @click.stop="edit(disSource,index)">{{text}}</span><span class="tags-del" @click.stop="delTag(index)">×</span>
|
||||
</div>
|
||||
</Draggable>
|
||||
@ -60,14 +60,6 @@
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const disSource = [];
|
||||
if( this.value ){
|
||||
this.value?.split(",").forEach(item => {
|
||||
if (item) {
|
||||
disSource.push(item)
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
minWidth: 80,
|
||||
|
||||
@ -78,7 +70,7 @@
|
||||
|
||||
content: '',
|
||||
|
||||
disSource,
|
||||
disSource: this.parseValue(this.value),
|
||||
|
||||
isFocus: false,
|
||||
|
||||
@ -103,25 +95,10 @@
|
||||
this.wayMinWidth();
|
||||
},
|
||||
value(val) {
|
||||
if( val && typeof val == 'string' ){
|
||||
let disSource = [];
|
||||
val?.split(",").forEach(item => {
|
||||
if (item) {
|
||||
disSource.push(item)
|
||||
}
|
||||
});
|
||||
this.disSource = disSource;
|
||||
}
|
||||
this.disSource = this.parseValue(val);
|
||||
},
|
||||
disSource(val) {
|
||||
let temp = '';
|
||||
val.forEach(item => {
|
||||
if (temp != '') {
|
||||
temp += this.cut;
|
||||
}
|
||||
temp += item;
|
||||
});
|
||||
this.$emit('input', temp);
|
||||
this.$emit('input', val.join(this.joinChar()));
|
||||
this.$emit('on-change');
|
||||
}
|
||||
},
|
||||
@ -134,6 +111,43 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
normalizedCuts() {
|
||||
const raw = Array.isArray(this.cut) ? this.cut : [this.cut];
|
||||
return raw.filter(c => typeof c === 'string' && c.length > 0);
|
||||
},
|
||||
cutPattern() {
|
||||
const cuts = this.normalizedCuts();
|
||||
if (cuts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const escaped = cuts.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
return new RegExp(escaped.join('|'), 'g');
|
||||
},
|
||||
joinChar() {
|
||||
return this.normalizedCuts()[0] || ',';
|
||||
},
|
||||
parseValue(val) {
|
||||
const list = [];
|
||||
if (typeof val !== 'string' || val === '') {
|
||||
return list;
|
||||
}
|
||||
val.split(this.joinChar()).forEach(item => {
|
||||
const value = item.trim();
|
||||
if (value && list.indexOf(value) === -1) {
|
||||
list.push(value);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
},
|
||||
splitByCuts(str) {
|
||||
const pattern = this.cutPattern();
|
||||
return pattern ? str.split(pattern) : [str];
|
||||
},
|
||||
showTis(msg) {
|
||||
this.tis = msg;
|
||||
clearTimeout(this.tisTimeout);
|
||||
this.tisTimeout = setTimeout(() => { this.tis = ''; }, 2000);
|
||||
},
|
||||
edit(disSource,index){
|
||||
this.editData.disSource = disSource
|
||||
this.editData.index = index
|
||||
@ -144,12 +158,17 @@
|
||||
okText: "确定",
|
||||
value: disSource[index] + '',
|
||||
onOk: (desc) => {
|
||||
if (!desc) {
|
||||
const trimmed = (desc || '').trim()
|
||||
if (!trimmed) {
|
||||
return `请输入名称`
|
||||
}
|
||||
this.editData.name = desc
|
||||
this.editData.disSource[this.editData.index] = desc
|
||||
this.$set(this.disSource,this.editData.index,desc)
|
||||
const exists = this.disSource.indexOf(trimmed)
|
||||
if (exists !== -1 && exists !== this.editData.index) {
|
||||
return `该标签已存在`
|
||||
}
|
||||
this.editData.name = trimmed
|
||||
this.editData.disSource[this.editData.index] = trimmed
|
||||
this.$set(this.disSource, this.editData.index, trimmed)
|
||||
return false
|
||||
},
|
||||
});
|
||||
@ -192,7 +211,14 @@
|
||||
pasteText(e) {
|
||||
e.preventDefault();
|
||||
let content = (e.clipboardData || window.clipboardData).getData('text');
|
||||
this.addTag(false, content)
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
for (const item of this.splitByCuts(content)) {
|
||||
const value = item.trim();
|
||||
if (!value) continue;
|
||||
if (this.addTag(false, value) === false) break;
|
||||
}
|
||||
},
|
||||
downEnter(e) {
|
||||
if (e.isComposing || e.key === 'Process' || e.keyCode === 229) {
|
||||
@ -220,32 +246,45 @@
|
||||
this.$emit("on-blur", e)
|
||||
},
|
||||
onKeyup(e) {
|
||||
if (e.keyCode !== 13) {
|
||||
this.addTag(e, this.content);
|
||||
}
|
||||
this.$emit("on-keyup", e)
|
||||
},
|
||||
addTag(e, content) {
|
||||
if (e === false || e.keyCode === 13) {
|
||||
if (content.trim() != '' && this.disSource.indexOf(content.trim()) === -1) {
|
||||
this.disSource.push(content.trim());
|
||||
const isForce = e === false || e.keyCode === 13;
|
||||
let value;
|
||||
if (isForce) {
|
||||
value = content.trim();
|
||||
} else {
|
||||
if (content === '') return true;
|
||||
let matchedCut = null;
|
||||
for (const c of this.normalizedCuts()) {
|
||||
if (c.length <= content.length && content.substring(content.length - c.length) === c) {
|
||||
matchedCut = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matchedCut === null) return true;
|
||||
value = content.substring(0, content.length - matchedCut.length).trim();
|
||||
}
|
||||
if (value === '') {
|
||||
this.content = '';
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if (this.max > 0 && this.disSource.length >= this.max) {
|
||||
this.content = '';
|
||||
this.tis = '最多只能添加' + this.max + '个';
|
||||
clearTimeout(this.tisTimeout);
|
||||
this.tisTimeout = setTimeout(() => { this.tis = ''; }, 2000);
|
||||
return;
|
||||
this.showTis(this.$L('最多只能添加(*)个', this.max));
|
||||
return false;
|
||||
}
|
||||
let temp = content.trim();
|
||||
let cutPos = temp.length - this.cut.length;
|
||||
if (temp != '' && temp.substring(cutPos) === this.cut) {
|
||||
temp = temp.substring(0, cutPos);
|
||||
if (temp.trim() != '' && this.disSource.indexOf(temp.trim()) === -1) {
|
||||
this.disSource.push(temp.trim());
|
||||
}
|
||||
if (this.disSource.indexOf(value) !== -1) {
|
||||
this.content = '';
|
||||
this.showTis(this.$L('该标签已存在'));
|
||||
return true;
|
||||
}
|
||||
this.disSource.push(value);
|
||||
this.content = '';
|
||||
return true;
|
||||
},
|
||||
delTag(index) {
|
||||
if (index === false) {
|
||||
|
||||
@ -502,6 +502,7 @@ export default {
|
||||
},
|
||||
|
||||
onLoginKeydown(e) {
|
||||
if (e.isComposing || e.key === 'Process' || e.keyCode === 229) return;
|
||||
if (e.keyCode === 13) {
|
||||
this.onLogin();
|
||||
}
|
||||
|
||||
@ -323,7 +323,7 @@
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem v-if="addData.columns" :label="$L('任务列表')">
|
||||
<TagInput v-model="addData.columns"/>
|
||||
<TagInput v-model="addData.columns" :cut="[',', ',', ' ']"/>
|
||||
</FormItem>
|
||||
<FormItem v-else :label="$L('项目模板')">
|
||||
<Select :value="0" @on-change="selectChange" :placeholder="$L('请选择模板')">
|
||||
|
||||
@ -1374,6 +1374,7 @@ export default {
|
||||
},
|
||||
|
||||
onAddColumnKeydown(e) {
|
||||
if (e.isComposing || e.key === 'Process' || e.keyCode === 229) return;
|
||||
if (e.keyCode === 13) {
|
||||
this.addColumnSubmit();
|
||||
}
|
||||
|
||||
@ -470,6 +470,7 @@ export default {
|
||||
},
|
||||
|
||||
onSubNameKeydown(e) {
|
||||
if (e.isComposing || e.key === 'Process' || e.keyCode === 229) return;
|
||||
if (e.keyCode === 13) {
|
||||
this.addSubTask();
|
||||
}
|
||||
|
||||
@ -224,11 +224,13 @@ export default {
|
||||
});
|
||||
},
|
||||
onAddKeydown(e) {
|
||||
if (e.isComposing || e.key === 'Process' || e.keyCode === 229) return;
|
||||
if (e.keyCode === 13) {
|
||||
this.handleAdd();
|
||||
}
|
||||
},
|
||||
onEditKeydown(e, tag) {
|
||||
if (e.isComposing || e.key === 'Process' || e.keyCode === 229) return;
|
||||
if (e.keyCode === 13) {
|
||||
this.confirmEdit(tag);
|
||||
}
|
||||
|
||||
@ -1991,7 +1991,7 @@ export default {
|
||||
|
||||
onKeydown(e, item) {
|
||||
if (e.keyCode === 13) {
|
||||
this.onEnter(item);
|
||||
this.$nextTick(() => this.onEnter(item));
|
||||
} else if (e.keyCode === 27) {
|
||||
const isCreate = !/^\d+$/.test(item.id);
|
||||
if (isCreate) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user