add labeling page

This commit is contained in:
Fam Zheng 2025-04-25 22:09:24 +01:00
parent 0b9035d5d4
commit 7e81451140
10 changed files with 283 additions and 4 deletions

View 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='描述')),
],
),
]

View 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='名称'),
),
]

View File

@ -433,3 +433,12 @@ class ABTestSample(models.Model):
def __str__(self):
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}"

View File

@ -419,6 +419,18 @@ class ScanDataResource(BaseResource):
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 Meta:
queryset = UserMessage.objects.all().order_by('-pk');
@ -485,6 +497,15 @@ class ABTestSampleResource(BaseResource):
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__():
v1_api.register(cls())
@ -1122,8 +1143,8 @@ class OssImageView(BaseView):
name = request.GET.get('name')
if not name:
return http404()
url = oss_sign_url(name)
return redirect(url)
img_bytes = oss_get(name)
return HttpResponse(img_bytes, content_type='image/jpeg')
class BackupView(BaseView):
name = 'backup'

View File

@ -57,6 +57,18 @@ export default [
to: '/scan-data',
icon: 'basket-shopping',
},
{
component: 'CNavItem',
name: '数据标注',
to: '/labeling',
icon: 'tag',
},
{
component: 'CNavItem',
name: '标签管理',
to: '/label-mgmt',
icon: 'tags',
},
{
component: 'CNavItem',
name: '系统日志',

View File

@ -90,6 +90,9 @@
<th v-for="f, i in list_fields_" :key="i">
{{ f.metadata.verbose_name }}
</th>
<th v-for="a, i in custom_fields" :key="i">
{{ a.title }}
</th>
<th v-if="!no_actions">操作</th>
</CTableRow>
</CTableHead>
@ -102,6 +105,9 @@
</span>
<CBadge class="public-badge" color="info" v-if="r.public">(公共)</CBadge>
</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">
<button class="btn btn-light btn-sm" title="查看详情" v-if="!no_details" @click="row_detail(r)">
<font-awesome-icon icon="eye" />
@ -128,6 +134,7 @@
<CTableRow v-for="x, i in filler_rows" :key="'filler-row-' + i">
<td v-if="!no_actions && !no_edit"></td>
<td v-for="j in x" :key="'filler-cell-' + j">&nbsp;</td>
<td v-for="a, i in custom_fields" :key="i"></td>
<td v-if="!no_actions"></td>
</CTableRow>
</CTableBody>
@ -181,7 +188,7 @@ export default {
props: ['uri', 'list_fields', 'visible_fields', 'editable_fields', 'object_name', 'show_search',
'actions', 'delete_confirm_field_name', 'no_details', 'no_edit', 'default_values',
'no_create', 'no_actions', 'no_delete',
'pre_save', 'hide_title', 'patch_schema'],
'pre_save', 'hide_title', 'patch_schema', 'custom_fields'],
components: {
GenericEditModal,
GenericDetailModal,

View File

@ -49,6 +49,18 @@ const routes = [
component: () =>
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',
name: 'ScanDataExport',

View 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
View 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>