add labeling page
This commit is contained in:
parent
0b9035d5d4
commit
7e81451140
23
api/products/migrations/0103_datalabel.py
Normal file
23
api/products/migrations/0103_datalabel.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.25 on 2025-04-25 07:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0102_remove_codebatch_detection_service'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DataLabel',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=128, verbose_name='名称')),
|
||||||
|
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建日期')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新日期')),
|
||||||
|
('description', models.TextField(verbose_name='描述')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
api/products/migrations/0104_alter_datalabel_name.py
Normal file
18
api/products/migrations/0104_alter_datalabel_name.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.25 on 2025-04-25 21:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0103_datalabel'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='datalabel',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_index=True, max_length=128, unique=True, verbose_name='名称'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -433,3 +433,12 @@ class ABTestSample(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.pk} {self.abtest.name} {self.create_time}"
|
return f"{self.pk} {self.abtest.name} {self.create_time}"
|
||||||
|
|
||||||
|
class DataLabel(models.Model):
|
||||||
|
name = models.CharField(max_length=128, verbose_name="名称", db_index=True, unique=True)
|
||||||
|
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建日期")
|
||||||
|
update_time = models.DateTimeField(auto_now=True, verbose_name="更新日期")
|
||||||
|
description = models.TextField(verbose_name="描述")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}: created {self.create_time} updated {self.update_time}"
|
||||||
@ -419,6 +419,18 @@ class ScanDataResource(BaseResource):
|
|||||||
|
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
def get_object_list(self, request):
|
||||||
|
qs = super().get_object_list(request)
|
||||||
|
if 'include_labels' in request.GET:
|
||||||
|
labels = request.GET.get('include_labels').split(',')
|
||||||
|
for l in labels:
|
||||||
|
qs = qs.filter(labels__contains=l)
|
||||||
|
if 'exclude_labels' in request.GET:
|
||||||
|
labels = request.GET.get('exclude_labels').split(',')
|
||||||
|
for l in labels:
|
||||||
|
qs = qs.exclude(labels__contains=l)
|
||||||
|
return qs
|
||||||
|
|
||||||
class MessageResource(BaseResource):
|
class MessageResource(BaseResource):
|
||||||
class Meta:
|
class Meta:
|
||||||
queryset = UserMessage.objects.all().order_by('-pk');
|
queryset = UserMessage.objects.all().order_by('-pk');
|
||||||
@ -485,6 +497,15 @@ class ABTestSampleResource(BaseResource):
|
|||||||
|
|
||||||
auth_role = 'admin'
|
auth_role = 'admin'
|
||||||
|
|
||||||
|
class DataLabelResource(BaseResource):
|
||||||
|
class Meta:
|
||||||
|
queryset = DataLabel.objects.all().order_by('-pk')
|
||||||
|
resource_name = 'datalabel'
|
||||||
|
authorization = BaseAuthorization()
|
||||||
|
serializer = BaseSerializer()
|
||||||
|
|
||||||
|
auth_role = 'admin'
|
||||||
|
|
||||||
for cls in BaseResource.__subclasses__():
|
for cls in BaseResource.__subclasses__():
|
||||||
v1_api.register(cls())
|
v1_api.register(cls())
|
||||||
|
|
||||||
@ -1122,8 +1143,8 @@ class OssImageView(BaseView):
|
|||||||
name = request.GET.get('name')
|
name = request.GET.get('name')
|
||||||
if not name:
|
if not name:
|
||||||
return http404()
|
return http404()
|
||||||
url = oss_sign_url(name)
|
img_bytes = oss_get(name)
|
||||||
return redirect(url)
|
return HttpResponse(img_bytes, content_type='image/jpeg')
|
||||||
|
|
||||||
class BackupView(BaseView):
|
class BackupView(BaseView):
|
||||||
name = 'backup'
|
name = 'backup'
|
||||||
|
|||||||
@ -57,6 +57,18 @@ export default [
|
|||||||
to: '/scan-data',
|
to: '/scan-data',
|
||||||
icon: 'basket-shopping',
|
icon: 'basket-shopping',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: 'CNavItem',
|
||||||
|
name: '数据标注',
|
||||||
|
to: '/labeling',
|
||||||
|
icon: 'tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'CNavItem',
|
||||||
|
name: '标签管理',
|
||||||
|
to: '/label-mgmt',
|
||||||
|
icon: 'tags',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: 'CNavItem',
|
component: 'CNavItem',
|
||||||
name: '系统日志',
|
name: '系统日志',
|
||||||
|
|||||||
@ -90,6 +90,9 @@
|
|||||||
<th v-for="f, i in list_fields_" :key="i">
|
<th v-for="f, i in list_fields_" :key="i">
|
||||||
{{ f.metadata.verbose_name }}
|
{{ f.metadata.verbose_name }}
|
||||||
</th>
|
</th>
|
||||||
|
<th v-for="a, i in custom_fields" :key="i">
|
||||||
|
{{ a.title }}
|
||||||
|
</th>
|
||||||
<th v-if="!no_actions">操作</th>
|
<th v-if="!no_actions">操作</th>
|
||||||
</CTableRow>
|
</CTableRow>
|
||||||
</CTableHead>
|
</CTableHead>
|
||||||
@ -102,6 +105,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<CBadge class="public-badge" color="info" v-if="r.public">(公共)</CBadge>
|
<CBadge class="public-badge" color="info" v-if="r.public">(公共)</CBadge>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-for="a, i in custom_fields" :key="i">
|
||||||
|
<div v-html="a.get_html(r)"></div>
|
||||||
|
</td>
|
||||||
<td class="row-actions" v-if="!no_actions">
|
<td class="row-actions" v-if="!no_actions">
|
||||||
<button class="btn btn-light btn-sm" title="查看详情" v-if="!no_details" @click="row_detail(r)">
|
<button class="btn btn-light btn-sm" title="查看详情" v-if="!no_details" @click="row_detail(r)">
|
||||||
<font-awesome-icon icon="eye" />
|
<font-awesome-icon icon="eye" />
|
||||||
@ -128,6 +134,7 @@
|
|||||||
<CTableRow v-for="x, i in filler_rows" :key="'filler-row-' + i">
|
<CTableRow v-for="x, i in filler_rows" :key="'filler-row-' + i">
|
||||||
<td v-if="!no_actions && !no_edit"></td>
|
<td v-if="!no_actions && !no_edit"></td>
|
||||||
<td v-for="j in x" :key="'filler-cell-' + j"> </td>
|
<td v-for="j in x" :key="'filler-cell-' + j"> </td>
|
||||||
|
<td v-for="a, i in custom_fields" :key="i"></td>
|
||||||
<td v-if="!no_actions"></td>
|
<td v-if="!no_actions"></td>
|
||||||
</CTableRow>
|
</CTableRow>
|
||||||
</CTableBody>
|
</CTableBody>
|
||||||
@ -181,7 +188,7 @@ export default {
|
|||||||
props: ['uri', 'list_fields', 'visible_fields', 'editable_fields', 'object_name', 'show_search',
|
props: ['uri', 'list_fields', 'visible_fields', 'editable_fields', 'object_name', 'show_search',
|
||||||
'actions', 'delete_confirm_field_name', 'no_details', 'no_edit', 'default_values',
|
'actions', 'delete_confirm_field_name', 'no_details', 'no_edit', 'default_values',
|
||||||
'no_create', 'no_actions', 'no_delete',
|
'no_create', 'no_actions', 'no_delete',
|
||||||
'pre_save', 'hide_title', 'patch_schema'],
|
'pre_save', 'hide_title', 'patch_schema', 'custom_fields'],
|
||||||
components: {
|
components: {
|
||||||
GenericEditModal,
|
GenericEditModal,
|
||||||
GenericDetailModal,
|
GenericDetailModal,
|
||||||
|
|||||||
@ -49,6 +49,18 @@ const routes = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "scan-data" */ '@/views/scan-data.vue'),
|
import(/* webpackChunkName: "scan-data" */ '@/views/scan-data.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/labeling',
|
||||||
|
name: 'Labeling',
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "labeling" */ '@/views/labeling.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/label-mgmt',
|
||||||
|
name: 'LabelMgmt',
|
||||||
|
component: () =>
|
||||||
|
import(/* webpackChunkName: "label-mgmt" */ '@/views/label-mgmt.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/scan-data-export',
|
path: '/scan-data-export',
|
||||||
name: 'ScanDataExport',
|
name: 'ScanDataExport',
|
||||||
|
|||||||
@ -4,4 +4,4 @@ span.subtitle {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
26
web/src/views/label-mgmt.vue
Normal file
26
web/src/views/label-mgmt.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<GenericManager
|
||||||
|
:uri="resource_uri"
|
||||||
|
object_name="标签管理"
|
||||||
|
:visible_fields="['id', 'name', 'description']"
|
||||||
|
:editable_fields="['name', 'description']"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GenericManager from '@/components/generic-manager.vue'
|
||||||
|
export default {
|
||||||
|
name: 'LabelMgmt',
|
||||||
|
components: {
|
||||||
|
GenericManager,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
resource_uri: '/api/v1/datalabel/',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
151
web/src/views/labeling.vue
Normal file
151
web/src/views/labeling.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="row">
|
||||||
|
<div class="preview-size-slide col">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-label">Preview size: {{ preview_size }}</div>
|
||||||
|
<input id="preview_size"
|
||||||
|
@change="change_preview_size"
|
||||||
|
v-model="preview_size"
|
||||||
|
type="range"
|
||||||
|
min="10" max="400"
|
||||||
|
step="5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="row border rounded p-2">
|
||||||
|
<div class="col">
|
||||||
|
<strong>Must include:</strong>
|
||||||
|
<div class="form-check" v-for="label in labels" :key="label.id">
|
||||||
|
<input class="form-check-input" :id="`include-${label.id}`" type="checkbox" @change="toggle_include_label(label)" :checked="include_labels.includes(label.id)">
|
||||||
|
<label class="form-check-label" :for="`include-${label.id}`">{{ label.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<strong>Must not include:</strong>
|
||||||
|
<div class="form-check" v-for="label in labels" :key="label.id">
|
||||||
|
<input class="form-check-input" :id="`exclude-${label.id}`" type="checkbox" @change="toggle_exclude_label(label)" :checked="exclude_labels.includes(label.id)">
|
||||||
|
<label class="form-check-label" :for="`exclude-${label.id}`">{{ label.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button class="btn btn-success" @click="search">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<GenericManager
|
||||||
|
:uri="resource_uri"
|
||||||
|
object_name="数据标注"
|
||||||
|
:visible_fields="visible_fields"
|
||||||
|
:actions="actions"
|
||||||
|
no_edit="1" no_details="1" no_delete="1" no_create="1"
|
||||||
|
ref="generic_manager"
|
||||||
|
:custom_fields="custom_fields"
|
||||||
|
>
|
||||||
|
</GenericManager>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GenericManager from '@/components/generic-manager.vue'
|
||||||
|
export default {
|
||||||
|
name: 'Labeling',
|
||||||
|
components: {
|
||||||
|
GenericManager,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
preview_size: 50,
|
||||||
|
labels: [],
|
||||||
|
visible_fields: ['id', 'labels', 'code', 'location', 'product__name'],
|
||||||
|
include_labels: [],
|
||||||
|
exclude_labels: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resource_uri() {
|
||||||
|
var url = '/api/v1/scan-data/?';
|
||||||
|
if (this.include_labels.length > 0) {
|
||||||
|
url += `include_labels=${this.include_labels.join(',')}&`;
|
||||||
|
}
|
||||||
|
if (this.exclude_labels.length > 0) {
|
||||||
|
url += `exclude_labels=${this.exclude_labels.join(',')}&`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
custom_fields() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '扫码图像',
|
||||||
|
get_html: (item) => {
|
||||||
|
var url = `/api/v1/oss-image/?name=${item.image}&token=${this.$root.token}`;
|
||||||
|
return `<a href="${url}" target="_blank"><img width="${this.preview_size}" src="${url}" /></a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
actions() {
|
||||||
|
if (!this.labels) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return this.labels.map(label => ({
|
||||||
|
handler: (item) => {
|
||||||
|
this.toggle_label(item, label);
|
||||||
|
},
|
||||||
|
name: `${label.name}`,
|
||||||
|
icon: 'tag',
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
search() {
|
||||||
|
this.$refs.generic_manager.reload();
|
||||||
|
},
|
||||||
|
toggle_include_label(label) {
|
||||||
|
var oldval = this.include_labels.includes(label.name);
|
||||||
|
if (oldval) {
|
||||||
|
this.include_labels = this.include_labels.filter(l => l !== label.name);
|
||||||
|
} else {
|
||||||
|
this.include_labels.push(label.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggle_exclude_label(label) {
|
||||||
|
var oldval = this.exclude_labels.includes(label.name);
|
||||||
|
if (oldval) {
|
||||||
|
this.exclude_labels = this.exclude_labels.filter(l => l !== label.name);
|
||||||
|
} else {
|
||||||
|
this.exclude_labels.push(label.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
change_preview_size() {
|
||||||
|
localStorage.setItem('labeling_preview_size', this.preview_size);
|
||||||
|
},
|
||||||
|
toggle_label(item, label) {
|
||||||
|
console.log(item, label);
|
||||||
|
var cur_labels = item.labels || "";
|
||||||
|
var new_labels = cur_labels.split(",");
|
||||||
|
if (new_labels.includes(label.name)) {
|
||||||
|
new_labels = new_labels.filter(l => l !== label.name);
|
||||||
|
} else {
|
||||||
|
new_labels.push(label.name);
|
||||||
|
}
|
||||||
|
item.labels = new_labels.filter(l => l).join(",");
|
||||||
|
this.$root.api_patch(`/api/v1/scan-data/${item.id}/`, {
|
||||||
|
labels: item.labels,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async reload() {
|
||||||
|
var url = '/api/v1/datalabel/';
|
||||||
|
var r = await this.$root.api_get(url);
|
||||||
|
this.labels = r.data.objects
|
||||||
|
this.preview_size = localStorage.getItem('labeling_preview_size') || 50;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.reload()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user