feat: update appraisal return address and test packaging assets
This commit is contained in:
@@ -174,6 +174,7 @@ export interface AdminOrderListItem {
|
||||
id: number;
|
||||
order_no: string;
|
||||
appraisal_no: string;
|
||||
external_order_no?: string;
|
||||
product_name: string;
|
||||
category_name: string;
|
||||
brand_name: string;
|
||||
@@ -197,6 +198,7 @@ export interface AdminOrderDetail {
|
||||
id: number;
|
||||
order_no: string;
|
||||
appraisal_no: string;
|
||||
external_order_no?: string;
|
||||
service_provider: string;
|
||||
service_provider_text: string;
|
||||
price_package_name: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { CopyDocument } from "@element-plus/icons-vue";
|
||||
import {
|
||||
adminApi,
|
||||
type EnterpriseCustomer,
|
||||
@@ -210,6 +211,49 @@ async function resetSecret(row: EnterpriseCustomerApp) {
|
||||
}
|
||||
}
|
||||
|
||||
function copySecretFallback(value: string) {
|
||||
const input = document.createElement("textarea");
|
||||
input.value = value;
|
||||
input.setAttribute("readonly", "readonly");
|
||||
input.style.position = "fixed";
|
||||
input.style.opacity = "0";
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
try {
|
||||
const copied = document.execCommand("copy");
|
||||
if (!copied) {
|
||||
throw new Error("copy command failed");
|
||||
}
|
||||
} finally {
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
}
|
||||
|
||||
async function copySecret() {
|
||||
const value = oneTimeSecret.value;
|
||||
if (!value) {
|
||||
ElMessage.warning("Secret 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
} else {
|
||||
copySecretFallback(value);
|
||||
}
|
||||
ElMessage.success("Secret 已复制");
|
||||
} catch (error) {
|
||||
try {
|
||||
copySecretFallback(value);
|
||||
ElMessage.success("Secret 已复制");
|
||||
} catch (fallbackError) {
|
||||
console.error(fallbackError || error);
|
||||
ElMessage.error("Secret 复制失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resendEvent(row: EnterpriseOrderEvent) {
|
||||
try {
|
||||
const response = await adminApi.resendCustomerEvent(row.id);
|
||||
@@ -485,7 +529,11 @@ onMounted(fetchCustomers);
|
||||
|
||||
<el-dialog v-model="secretDialogVisible" title="应用 Secret" width="620px">
|
||||
<el-alert type="warning" show-icon :closable="false" title="Secret 只展示一次,关闭后无法再次查看。" />
|
||||
<el-input v-model="oneTimeSecret" readonly style="margin-top: 16px" />
|
||||
<el-input v-model="oneTimeSecret" readonly class="secret-input">
|
||||
<template #append>
|
||||
<el-button :icon="CopyDocument" @click="copySecret">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="secretDialogVisible = false">已保存</el-button>
|
||||
</template>
|
||||
@@ -509,4 +557,8 @@ onMounted(fetchCustomers);
|
||||
.detail-url {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.secret-input {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,6 +42,7 @@ const manualForm = ref<AdminManualOrderCreatePayload>(createManualOrderForm());
|
||||
const manualAddressRecognitionText = ref("");
|
||||
|
||||
const keyword = ref("");
|
||||
const externalOrderNo = ref("");
|
||||
const trackingNo = ref("");
|
||||
const userMobile = ref("");
|
||||
const serviceProvider = ref("");
|
||||
@@ -203,6 +204,7 @@ async function fetchOrders() {
|
||||
try {
|
||||
const response = await adminApi.getOrders({
|
||||
keyword: keyword.value,
|
||||
external_order_no: externalOrderNo.value,
|
||||
tracking_no: trackingNo.value,
|
||||
user_mobile: userMobile.value,
|
||||
service_provider: serviceProvider.value,
|
||||
@@ -535,6 +537,7 @@ onMounted(fetchOrders);
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<div class="filters-row">
|
||||
<el-input v-model="keyword" placeholder="搜索订单号 / 鉴定单号 / 商品名称" clearable style="width: 320px" />
|
||||
<el-input v-model="externalOrderNo" placeholder="搜索客户单号" clearable style="width: 180px" />
|
||||
<el-input v-model="trackingNo" placeholder="搜索快递单号" clearable style="width: 180px" />
|
||||
<el-input v-model="userMobile" placeholder="搜索用户电话" clearable style="width: 180px" />
|
||||
<el-select v-model="serviceProvider" placeholder="服务类型" style="width: 160px">
|
||||
@@ -563,6 +566,7 @@ onMounted(fetchOrders);
|
||||
<el-table-column label="下单渠道" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.source_channel_text }}</span>
|
||||
<div v-if="row.external_order_no" class="table-subtext">客户单号:{{ row.external_order_no }}</div>
|
||||
<div v-if="row.source_customer_id" class="table-subtext">客户ID:{{ row.source_customer_id }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -664,6 +668,10 @@ onMounted(fetchOrders);
|
||||
<div class="order-detail-item__label">大客户客户 ID</div>
|
||||
<div class="order-detail-item__value">{{ detail.order_info.source_customer_id }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item" v-if="detail.order_info.external_order_no">
|
||||
<div class="order-detail-item__label">客户单号</div>
|
||||
<div class="order-detail-item__value">{{ detail.order_info.external_order_no }}</div>
|
||||
</div>
|
||||
<div class="order-detail-item">
|
||||
<div class="order-detail-item__label">当前状态</div>
|
||||
<div class="order-detail-item__value"><OrderStatusTag :status="detail.order_info.display_status" /></div>
|
||||
|
||||
@@ -200,6 +200,37 @@ POST /api/open/v1/orders
|
||||
| `tracking_no` | string | 否 | 寄入运单号,可替代 `inbound_logistics.tracking_no` |
|
||||
| `extra_info` | object | 否 | 购买、成色、附件、备注等扩展信息 |
|
||||
|
||||
### 5.3 单独设置寄回地址
|
||||
|
||||
```text
|
||||
POST /api/open/v1/orders/return-address
|
||||
```
|
||||
|
||||
第三方可以在建单后单独补录或更新寄回地址。订单已生成回寄运单后,不允许再修改。
|
||||
|
||||
### 5.4 请求参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `external_order_no` | string | 是 | 第三方订单号 |
|
||||
| `return_address` | object | 是 | 寄回地址,字段要求同创建订单接口 |
|
||||
|
||||
### 5.5 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"external_order_no": "THIRD202605080001",
|
||||
"return_address": {
|
||||
"consignee": "张三",
|
||||
"mobile": "13800138000",
|
||||
"province": "浙江省",
|
||||
"city": "杭州市",
|
||||
"district": "西湖区",
|
||||
"detail_address": "文三路 1 号"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 可选对象字段
|
||||
|
||||
`product_info` 支持:
|
||||
@@ -452,6 +483,15 @@ GET /api/open/v1/orders?order_no=AXY20260508120000123
|
||||
"latest_desc": "客户已提交寄送运单:顺丰速运 SF1234567890,等待鉴定中心签收。",
|
||||
"latest_time": "2026-05-08 12:00:00"
|
||||
},
|
||||
"return_address": {
|
||||
"consignee": "张三",
|
||||
"mobile": "13800138000",
|
||||
"province": "浙江省",
|
||||
"city": "杭州市",
|
||||
"district": "西湖区",
|
||||
"detail_address": "文三路 1 号",
|
||||
"full_address": "浙江省杭州市西湖区文三路 1 号"
|
||||
},
|
||||
"return_logistics": null,
|
||||
"report_summary": {
|
||||
"report_no": "R202605080001",
|
||||
@@ -617,6 +657,7 @@ POST /api/open/v1/orders/shipping
|
||||
2. 第三方完成签名调试。
|
||||
3. 第三方调用套餐获取接口,确认可用套餐和 `price_package_code`。
|
||||
4. 第三方调用创建订单接口。最小只传 `external_order_no` 即可;如需要减少后续人工补录,建议同步传 `price_package_code`、`product_info`、`return_address`、`materials` 和 `inbound_logistics`。
|
||||
5. 商品实际寄出后,第三方调用发货通知接口提交 `express_company` 和 `tracking_no`。
|
||||
6. 第三方可通过查询接口主动查询订单状态。
|
||||
7. 如启用 webhook,平台在订单状态变化时主动通知第三方。
|
||||
5. 如建单时未提供寄回地址,或后续需要变更,可调用寄回地址接口补录或更新 `return_address`。
|
||||
6. 商品实际寄出后,第三方调用发货通知接口提交 `express_company` 和 `tracking_no`。
|
||||
7. 第三方可通过查询接口主动查询订单状态,并核对 `return_address`、物流和报告结果。
|
||||
8. 如启用 webhook,平台在订单状态变化时主动通知第三方。
|
||||
|
||||
@@ -18,6 +18,7 @@ class OrdersController
|
||||
public function index(Request $request)
|
||||
{
|
||||
$keyword = trim((string)$request->input('keyword', ''));
|
||||
$externalOrderNo = trim((string)$request->input('external_order_no', ''));
|
||||
$trackingNo = trim((string)$request->input('tracking_no', ''));
|
||||
$userMobile = trim((string)$request->input('user_mobile', ''));
|
||||
$status = trim((string)$request->input('status', ''));
|
||||
@@ -30,10 +31,12 @@ class OrdersController
|
||||
$query = Db::name('orders')
|
||||
->alias('o')
|
||||
->leftJoin('order_products p', 'p.order_id = o.id')
|
||||
->leftJoin('enterprise_customer_order_refs ecor', 'ecor.order_id = o.id')
|
||||
->field([
|
||||
'o.id',
|
||||
'o.order_no',
|
||||
'o.appraisal_no',
|
||||
'ecor.external_order_no',
|
||||
'o.service_provider',
|
||||
'o.order_status',
|
||||
'o.display_status',
|
||||
@@ -64,6 +67,12 @@ class OrdersController
|
||||
});
|
||||
}
|
||||
|
||||
if ($externalOrderNo !== '') {
|
||||
$query->whereRaw('ecor.external_order_no LIKE :external_order_no', [
|
||||
'external_order_no' => "%{$externalOrderNo}%",
|
||||
]);
|
||||
}
|
||||
|
||||
if ($trackingNo !== '') {
|
||||
$query->whereRaw(
|
||||
"EXISTS (SELECT 1 FROM order_logistics ol WHERE ol.order_id = o.id AND ol.logistics_type IN ('send_to_center', 'return_to_user') AND ol.tracking_no LIKE :tracking_no)",
|
||||
@@ -155,6 +164,7 @@ class OrdersController
|
||||
'id' => $orderId,
|
||||
'order_no' => $item['order_no'],
|
||||
'appraisal_no' => $item['appraisal_no'],
|
||||
'external_order_no' => (string)($item['external_order_no'] ?? ''),
|
||||
'product_name' => $item['product_name'] ?: '待完善物品信息',
|
||||
'category_name' => $item['category_name'] ?: '',
|
||||
'brand_name' => $item['brand_name'] ?: '',
|
||||
@@ -255,6 +265,7 @@ class OrdersController
|
||||
->where('order_id', $id)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
$enterpriseOrderRef = Db::name('enterprise_customer_order_refs')->where('order_id', $id)->find();
|
||||
$timeline = Db::name('order_timelines')
|
||||
->where('order_id', $id)
|
||||
->order('occurred_at', 'asc')
|
||||
@@ -343,6 +354,7 @@ class OrdersController
|
||||
'id' => (int)$order['id'],
|
||||
'order_no' => $order['order_no'],
|
||||
'appraisal_no' => $order['appraisal_no'],
|
||||
'external_order_no' => (string)($enterpriseOrderRef['external_order_no'] ?? ''),
|
||||
'service_provider' => $order['service_provider'],
|
||||
'service_provider_text' => $order['service_provider'] === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
||||
'price_package_name' => (string)($order['price_package_name'] ?? ''),
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace app\controller\app;
|
||||
|
||||
use app\support\ContentService;
|
||||
use app\support\FileStorageService;
|
||||
use support\Request;
|
||||
use support\think\Db;
|
||||
|
||||
@@ -9,11 +11,19 @@ class CatalogController
|
||||
{
|
||||
public function categories(Request $request)
|
||||
{
|
||||
$categoryVisuals = $this->categoryVisualMap($request);
|
||||
$list = Db::name('catalog_categories')
|
||||
->field(['id AS category_id', 'name AS category_name', 'code AS category_code'])
|
||||
->where('is_enabled', 1)
|
||||
->order('sort_order', 'asc')
|
||||
->select()
|
||||
->map(function ($item) use ($categoryVisuals) {
|
||||
$codeKey = $this->categoryMatchKey((string)$item['category_code']);
|
||||
$nameKey = $this->categoryMatchKey((string)$item['category_name']);
|
||||
$item['image_url'] = $categoryVisuals['code:' . $codeKey] ?? $categoryVisuals['name:' . $nameKey] ?? '';
|
||||
|
||||
return $item;
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return api_success(['list' => $list]);
|
||||
@@ -39,4 +49,45 @@ class CatalogController
|
||||
]);
|
||||
}
|
||||
|
||||
private function categoryVisualMap(Request $request): array
|
||||
{
|
||||
$items = (new ContentService())->getHomeConfig()['category_visuals'] ?? [];
|
||||
if (!is_array($items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$map = [];
|
||||
$storage = new FileStorageService();
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$imageUrl = trim((string)($item['image_url'] ?? ''));
|
||||
if ($imageUrl === '') {
|
||||
continue;
|
||||
}
|
||||
$imageUrl = $storage->normalizeUrl($imageUrl, $request);
|
||||
|
||||
$categoryCode = $this->categoryMatchKey((string)($item['category_code'] ?? ''));
|
||||
if ($categoryCode !== '') {
|
||||
$map['code:' . $categoryCode] = $imageUrl;
|
||||
}
|
||||
|
||||
$categoryName = $this->categoryMatchKey((string)($item['category_name'] ?? ''));
|
||||
if ($categoryName !== '') {
|
||||
$map['name:' . $categoryName] = $imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function categoryMatchKey(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
$normalized = preg_replace('/[\s\p{Cf}]+/u', '', $value);
|
||||
|
||||
return strtolower($normalized ?? $value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,34 @@ class OrdersController
|
||||
return api_success($result, '运单已提交');
|
||||
}
|
||||
|
||||
public function saveReturnAddress(Request $request)
|
||||
{
|
||||
try {
|
||||
$auth = (new EnterpriseOpenApiAuthService())->authenticate($request);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error($e->getMessage(), 401);
|
||||
}
|
||||
|
||||
$payload = json_decode($request->rawBody(), true);
|
||||
if (!is_array($payload)) {
|
||||
return api_error('请求体必须是合法 JSON 对象', 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = (new EnterpriseOrderService())->saveReturnAddress($auth['customer'], $payload);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), 422);
|
||||
} catch (\RuntimeException $e) {
|
||||
return api_error($e->getMessage(), 404);
|
||||
} catch (\Throwable $e) {
|
||||
return api_error('寄回地址保存失败', 500, [
|
||||
'detail' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return api_success($result, '寄回地址已保存');
|
||||
}
|
||||
|
||||
public function servicePricePackages(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -299,6 +299,93 @@ class EnterpriseOrderService
|
||||
];
|
||||
}
|
||||
|
||||
public function saveReturnAddress(array $customer, array $payload): array
|
||||
{
|
||||
$externalOrderNo = trim((string)($payload['external_order_no'] ?? ''));
|
||||
if ($externalOrderNo === '') {
|
||||
throw new \InvalidArgumentException('external_order_no 不能为空');
|
||||
}
|
||||
|
||||
$returnAddress = $this->normalizeReturnAddress((array)($payload['return_address'] ?? []));
|
||||
if (!$returnAddress) {
|
||||
throw new \InvalidArgumentException('return_address 不能为空');
|
||||
}
|
||||
|
||||
$ref = Db::name('enterprise_customer_order_refs')
|
||||
->where('customer_id', (int)$customer['id'])
|
||||
->where('external_order_no', $externalOrderNo)
|
||||
->find();
|
||||
if (!$ref) {
|
||||
throw new \RuntimeException('订单不存在');
|
||||
}
|
||||
|
||||
$order = Db::name('orders')->where('id', (int)$ref['order_id'])->find();
|
||||
if (!$order) {
|
||||
throw new \RuntimeException('订单不存在');
|
||||
}
|
||||
|
||||
$returnLogistics = Db::name('order_logistics')
|
||||
->where('order_id', (int)$order['id'])
|
||||
->where('logistics_type', 'return_to_user')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
if (!empty($returnLogistics['tracking_no'])) {
|
||||
throw new \InvalidArgumentException('回寄运单已生成,当前不可再修改寄回地址');
|
||||
}
|
||||
|
||||
$existing = Db::name('order_return_addresses')->where('order_id', (int)$order['id'])->find();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$updated = (bool)$existing;
|
||||
$snapshot = array_merge($returnAddress, [
|
||||
'user_address_id' => null,
|
||||
]);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
if ($existing) {
|
||||
Db::name('order_return_addresses')->where('order_id', (int)$order['id'])->update(array_merge($snapshot, [
|
||||
'updated_at' => $now,
|
||||
]));
|
||||
$nodeText = '已更新寄回地址';
|
||||
} else {
|
||||
Db::name('order_return_addresses')->insert(array_merge($snapshot, [
|
||||
'order_id' => (int)$order['id'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]));
|
||||
$nodeText = '已确认寄回地址';
|
||||
}
|
||||
|
||||
Db::name('order_timelines')->insert([
|
||||
'order_id' => (int)$order['id'],
|
||||
'node_code' => 'return_address_selected',
|
||||
'node_text' => $nodeText,
|
||||
'node_desc' => sprintf(
|
||||
'大客户已确认寄回地址:%s%s%s%s',
|
||||
$returnAddress['province'],
|
||||
$returnAddress['city'],
|
||||
$returnAddress['district'],
|
||||
$returnAddress['detail_address']
|
||||
),
|
||||
'operator_type' => 'system',
|
||||
'operator_id' => null,
|
||||
'occurred_at' => $now,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return [
|
||||
'updated' => $updated,
|
||||
'return_address' => $this->formatReturnAddress($snapshot),
|
||||
'order' => $this->buildOrderProgress((int)$customer['id'], $ref, (string)$customer['customer_code']),
|
||||
];
|
||||
}
|
||||
|
||||
public function buildOrderProgress(int $customerId, array $ref, string $customerCode = ''): array
|
||||
{
|
||||
$order = Db::name('orders')->where('id', (int)$ref['order_id'])->find();
|
||||
@@ -309,6 +396,7 @@ class EnterpriseOrderService
|
||||
$timeline = Db::name('order_timelines')->where('order_id', (int)$order['id'])->order('occurred_at', 'asc')->select()->toArray();
|
||||
$sendLogistics = Db::name('order_logistics')->where('order_id', (int)$order['id'])->where('logistics_type', 'send_to_center')->order('id', 'desc')->find();
|
||||
$returnLogistics = Db::name('order_logistics')->where('order_id', (int)$order['id'])->where('logistics_type', 'return_to_user')->order('id', 'desc')->find();
|
||||
$returnAddress = Db::name('order_return_addresses')->where('order_id', (int)$order['id'])->find();
|
||||
$report = Db::name('reports')
|
||||
->where('order_id', (int)$order['id'])
|
||||
->where('report_status', 'published')
|
||||
@@ -339,6 +427,7 @@ class EnterpriseOrderService
|
||||
'occurred_at' => (string)$item['occurred_at'],
|
||||
], $timeline),
|
||||
'inbound_logistics' => $this->formatLogistics($sendLogistics),
|
||||
'return_address' => $returnAddress ? $this->formatReturnAddress($returnAddress) : null,
|
||||
'return_logistics' => $this->formatLogistics($returnLogistics),
|
||||
'report_summary' => $report ? [
|
||||
'report_no' => (string)$report['report_no'],
|
||||
@@ -586,4 +675,23 @@ class EnterpriseOrderService
|
||||
'latest_time' => (string)($logistics['latest_time'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatReturnAddress(array $item): array
|
||||
{
|
||||
return [
|
||||
'consignee' => (string)($item['consignee'] ?? ''),
|
||||
'mobile' => (string)($item['mobile'] ?? ''),
|
||||
'province' => (string)($item['province'] ?? ''),
|
||||
'city' => (string)($item['city'] ?? ''),
|
||||
'district' => (string)($item['district'] ?? ''),
|
||||
'detail_address' => (string)($item['detail_address'] ?? ''),
|
||||
'full_address' => trim(sprintf(
|
||||
'%s%s%s%s',
|
||||
$item['province'] ?? '',
|
||||
$item['city'] ?? '',
|
||||
$item['district'] ?? '',
|
||||
$item['detail_address'] ?? ''
|
||||
)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ Route::post('/api/app/address/default', [AppAddressesController::class, 'setDefa
|
||||
Route::post('/api/app/address/delete', [AppAddressesController::class, 'delete']);
|
||||
|
||||
Route::post('/api/open/v1/orders', [OpenOrdersController::class, 'create']);
|
||||
Route::post('/api/open/v1/orders/return-address', [OpenOrdersController::class, 'saveReturnAddress']);
|
||||
Route::post('/api/open/v1/orders/shipping', [OpenOrdersController::class, 'shipping']);
|
||||
Route::get('/api/open/v1/orders', [OpenOrdersController::class, 'detail']);
|
||||
Route::get('/api/open/v1/orders/{external_order_no}', [OpenOrdersController::class, 'detail']);
|
||||
|
||||
699
server-api/resources/catalog/known_brands.php
Normal file
699
server-api/resources/catalog/known_brands.php
Normal file
@@ -0,0 +1,699 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
[
|
||||
'code' => 'lv',
|
||||
'name' => '路易威登',
|
||||
'en_name' => 'Louis Vuitton',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 10,
|
||||
'source_tags' => ['interbrand', 'lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'chanel',
|
||||
'name' => '香奈儿',
|
||||
'en_name' => 'Chanel',
|
||||
'category_codes' => ['luxury_bag', 'jewelry', 'beauty'],
|
||||
'category_names' => ['奢侈品箱包', '首饰配饰', '高端美妆'],
|
||||
'sort_order' => 20,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'hermes',
|
||||
'name' => '爱马仕',
|
||||
'en_name' => 'Hermes',
|
||||
'category_codes' => ['luxury_bag', 'jewelry'],
|
||||
'category_names' => ['奢侈品箱包', '首饰配饰'],
|
||||
'sort_order' => 30,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'gucci',
|
||||
'name' => '古驰',
|
||||
'en_name' => 'Gucci',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 40,
|
||||
'source_tags' => ['interbrand', 'kering'],
|
||||
],
|
||||
[
|
||||
'code' => 'dior',
|
||||
'name' => '迪奥',
|
||||
'en_name' => 'Dior',
|
||||
'category_codes' => ['luxury_bag', 'beauty'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰', '高端美妆'],
|
||||
'sort_order' => 50,
|
||||
'source_tags' => ['interbrand', 'lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'prada',
|
||||
'name' => '普拉达',
|
||||
'en_name' => 'Prada',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 60,
|
||||
'source_tags' => ['interbrand', 'lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'celine',
|
||||
'name' => '思琳',
|
||||
'en_name' => 'Celine',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 70,
|
||||
'source_tags' => ['lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'loewe',
|
||||
'name' => '罗意威',
|
||||
'en_name' => 'Loewe',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 80,
|
||||
'source_tags' => ['lvmh', 'lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'fendi',
|
||||
'name' => '芬迪',
|
||||
'en_name' => 'Fendi',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 90,
|
||||
'source_tags' => ['lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'balenciaga',
|
||||
'name' => '巴黎世家',
|
||||
'en_name' => 'Balenciaga',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 100,
|
||||
'source_tags' => ['kering', 'lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'bottega_veneta',
|
||||
'name' => '葆蝶家',
|
||||
'en_name' => 'Bottega Veneta',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 110,
|
||||
'source_tags' => ['kering'],
|
||||
],
|
||||
[
|
||||
'code' => 'saint_laurent',
|
||||
'name' => '圣罗兰',
|
||||
'en_name' => 'Saint Laurent',
|
||||
'category_codes' => ['luxury_bag', 'beauty'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰', '高端美妆'],
|
||||
'sort_order' => 120,
|
||||
'source_tags' => ['kering', 'loreal'],
|
||||
],
|
||||
[
|
||||
'code' => 'burberry',
|
||||
'name' => '博柏利',
|
||||
'en_name' => 'Burberry',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 130,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'coach',
|
||||
'name' => '蔻驰',
|
||||
'en_name' => 'Coach',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 140,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'michael_kors',
|
||||
'name' => '迈克高仕',
|
||||
'en_name' => 'Michael Kors',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 150,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'miu_miu',
|
||||
'name' => '缪缪',
|
||||
'en_name' => 'Miu Miu',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 160,
|
||||
'source_tags' => ['lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'givenchy',
|
||||
'name' => '纪梵希',
|
||||
'en_name' => 'Givenchy',
|
||||
'category_codes' => ['luxury_bag', 'beauty'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰', '高端美妆'],
|
||||
'sort_order' => 170,
|
||||
'source_tags' => ['lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'valentino',
|
||||
'name' => '华伦天奴',
|
||||
'en_name' => 'Valentino',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 180,
|
||||
'source_tags' => ['lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'versace',
|
||||
'name' => '范思哲',
|
||||
'en_name' => 'Versace',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 190,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'alexander_mcqueen',
|
||||
'name' => '亚历山大麦昆',
|
||||
'en_name' => 'Alexander McQueen',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 200,
|
||||
'source_tags' => ['kering'],
|
||||
],
|
||||
[
|
||||
'code' => 'marc_jacobs',
|
||||
'name' => '马克雅可布',
|
||||
'en_name' => 'Marc Jacobs',
|
||||
'category_codes' => ['luxury_bag'],
|
||||
'category_names' => ['奢侈品箱包', '潮流服饰'],
|
||||
'sort_order' => 210,
|
||||
'source_tags' => ['lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'nike',
|
||||
'name' => '耐克',
|
||||
'en_name' => 'Nike',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 300,
|
||||
'source_tags' => ['interbrand', 'stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'jordan',
|
||||
'name' => '乔丹',
|
||||
'en_name' => 'Jordan',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 310,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'adidas',
|
||||
'name' => '阿迪达斯',
|
||||
'en_name' => 'Adidas',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 320,
|
||||
'source_tags' => ['interbrand', 'stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'new_balance',
|
||||
'name' => '新百伦',
|
||||
'en_name' => 'New Balance',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 330,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'asics',
|
||||
'name' => '亚瑟士',
|
||||
'en_name' => 'ASICS',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 340,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'puma',
|
||||
'name' => '彪马',
|
||||
'en_name' => 'Puma',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 350,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'converse',
|
||||
'name' => '匡威',
|
||||
'en_name' => 'Converse',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 360,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'vans',
|
||||
'name' => '范斯',
|
||||
'en_name' => 'Vans',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 370,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'reebok',
|
||||
'name' => '锐步',
|
||||
'en_name' => 'Reebok',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 380,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'salomon',
|
||||
'name' => '萨洛蒙',
|
||||
'en_name' => 'Salomon',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 390,
|
||||
'source_tags' => ['lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'on_running',
|
||||
'name' => '昂跑',
|
||||
'en_name' => 'On',
|
||||
'category_codes' => ['sneaker'],
|
||||
'category_names' => ['潮流鞋类', '潮流服饰'],
|
||||
'sort_order' => 400,
|
||||
'source_tags' => ['lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'supreme',
|
||||
'name' => 'Supreme',
|
||||
'en_name' => 'Supreme',
|
||||
'category_codes' => [],
|
||||
'category_names' => ['潮流服饰'],
|
||||
'sort_order' => 430,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'stussy',
|
||||
'name' => 'Stussy',
|
||||
'en_name' => 'Stussy',
|
||||
'category_codes' => [],
|
||||
'category_names' => ['潮流服饰'],
|
||||
'sort_order' => 440,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'off_white',
|
||||
'name' => 'Off-White',
|
||||
'en_name' => 'Off-White',
|
||||
'category_codes' => [],
|
||||
'category_names' => ['潮流服饰'],
|
||||
'sort_order' => 450,
|
||||
'source_tags' => ['lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'fear_of_god',
|
||||
'name' => 'Fear of God',
|
||||
'en_name' => 'Fear of God',
|
||||
'category_codes' => [],
|
||||
'category_names' => ['潮流服饰'],
|
||||
'sort_order' => 460,
|
||||
'source_tags' => ['stockx'],
|
||||
],
|
||||
[
|
||||
'code' => 'arc_teryx',
|
||||
'name' => '始祖鸟',
|
||||
'en_name' => 'Arc\'teryx',
|
||||
'category_codes' => [],
|
||||
'category_names' => ['潮流服饰'],
|
||||
'sort_order' => 470,
|
||||
'source_tags' => ['lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'moncler',
|
||||
'name' => '盟可睐',
|
||||
'en_name' => 'Moncler',
|
||||
'category_codes' => [],
|
||||
'category_names' => ['潮流服饰'],
|
||||
'sort_order' => 480,
|
||||
'source_tags' => ['lyst'],
|
||||
],
|
||||
[
|
||||
'code' => 'rolex',
|
||||
'name' => '劳力士',
|
||||
'en_name' => 'Rolex',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 500,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'omega',
|
||||
'name' => '欧米茄',
|
||||
'en_name' => 'Omega',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 510,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'cartier',
|
||||
'name' => '卡地亚',
|
||||
'en_name' => 'Cartier',
|
||||
'category_codes' => ['watch', 'jewelry'],
|
||||
'category_names' => ['腕表', '首饰配饰'],
|
||||
'sort_order' => 520,
|
||||
'source_tags' => ['richemont', 'interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'patek_philippe',
|
||||
'name' => '百达翡丽',
|
||||
'en_name' => 'Patek Philippe',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 530,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'audemars_piguet',
|
||||
'name' => '爱彼',
|
||||
'en_name' => 'Audemars Piguet',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 540,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'iwc',
|
||||
'name' => '万国',
|
||||
'en_name' => 'IWC Schaffhausen',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 550,
|
||||
'source_tags' => ['richemont'],
|
||||
],
|
||||
[
|
||||
'code' => 'jaeger_lecoultre',
|
||||
'name' => '积家',
|
||||
'en_name' => 'Jaeger-LeCoultre',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 560,
|
||||
'source_tags' => ['richemont'],
|
||||
],
|
||||
[
|
||||
'code' => 'vacheron_constantin',
|
||||
'name' => '江诗丹顿',
|
||||
'en_name' => 'Vacheron Constantin',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 570,
|
||||
'source_tags' => ['richemont'],
|
||||
],
|
||||
[
|
||||
'code' => 'panerai',
|
||||
'name' => '沛纳海',
|
||||
'en_name' => 'Panerai',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 580,
|
||||
'source_tags' => ['richemont'],
|
||||
],
|
||||
[
|
||||
'code' => 'tag_heuer',
|
||||
'name' => '泰格豪雅',
|
||||
'en_name' => 'TAG Heuer',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 590,
|
||||
'source_tags' => ['lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'longines',
|
||||
'name' => '浪琴',
|
||||
'en_name' => 'Longines',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 600,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'tissot',
|
||||
'name' => '天梭',
|
||||
'en_name' => 'Tissot',
|
||||
'category_codes' => ['watch'],
|
||||
'category_names' => ['腕表'],
|
||||
'sort_order' => 610,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'tiffany_co',
|
||||
'name' => '蒂芙尼',
|
||||
'en_name' => 'Tiffany & Co.',
|
||||
'category_codes' => ['jewelry'],
|
||||
'category_names' => ['首饰配饰'],
|
||||
'sort_order' => 700,
|
||||
'source_tags' => ['interbrand', 'lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'van_cleef_arpels',
|
||||
'name' => '梵克雅宝',
|
||||
'en_name' => 'Van Cleef & Arpels',
|
||||
'category_codes' => ['jewelry'],
|
||||
'category_names' => ['首饰配饰'],
|
||||
'sort_order' => 710,
|
||||
'source_tags' => ['richemont'],
|
||||
],
|
||||
[
|
||||
'code' => 'bulgari',
|
||||
'name' => '宝格丽',
|
||||
'en_name' => 'Bulgari',
|
||||
'category_codes' => ['jewelry', 'watch'],
|
||||
'category_names' => ['首饰配饰', '腕表'],
|
||||
'sort_order' => 720,
|
||||
'source_tags' => ['lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'chaumet',
|
||||
'name' => '尚美巴黎',
|
||||
'en_name' => 'Chaumet',
|
||||
'category_codes' => ['jewelry'],
|
||||
'category_names' => ['首饰配饰'],
|
||||
'sort_order' => 730,
|
||||
'source_tags' => ['lvmh'],
|
||||
],
|
||||
[
|
||||
'code' => 'boucheron',
|
||||
'name' => '宝诗龙',
|
||||
'en_name' => 'Boucheron',
|
||||
'category_codes' => ['jewelry'],
|
||||
'category_names' => ['首饰配饰'],
|
||||
'sort_order' => 740,
|
||||
'source_tags' => ['kering'],
|
||||
],
|
||||
[
|
||||
'code' => 'pomellato',
|
||||
'name' => '宝曼兰朵',
|
||||
'en_name' => 'Pomellato',
|
||||
'category_codes' => ['jewelry'],
|
||||
'category_names' => ['首饰配饰'],
|
||||
'sort_order' => 750,
|
||||
'source_tags' => ['kering'],
|
||||
],
|
||||
[
|
||||
'code' => 'chopard',
|
||||
'name' => '萧邦',
|
||||
'en_name' => 'Chopard',
|
||||
'category_codes' => ['jewelry', 'watch'],
|
||||
'category_names' => ['首饰配饰', '腕表'],
|
||||
'sort_order' => 760,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'swarovski',
|
||||
'name' => '施华洛世奇',
|
||||
'en_name' => 'Swarovski',
|
||||
'category_codes' => ['jewelry'],
|
||||
'category_names' => ['首饰配饰'],
|
||||
'sort_order' => 770,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'pandora',
|
||||
'name' => '潘多拉',
|
||||
'en_name' => 'Pandora',
|
||||
'category_codes' => ['jewelry'],
|
||||
'category_names' => ['首饰配饰'],
|
||||
'sort_order' => 780,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'lancome',
|
||||
'name' => '兰蔻',
|
||||
'en_name' => 'Lancome',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 900,
|
||||
'source_tags' => ['loreal'],
|
||||
],
|
||||
[
|
||||
'code' => 'ysl_beauty',
|
||||
'name' => '圣罗兰美妆',
|
||||
'en_name' => 'Yves Saint Laurent Beauty',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 910,
|
||||
'source_tags' => ['loreal'],
|
||||
],
|
||||
[
|
||||
'code' => 'armani_beauty',
|
||||
'name' => '阿玛尼美妆',
|
||||
'en_name' => 'Armani Beauty',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 920,
|
||||
'source_tags' => ['loreal'],
|
||||
],
|
||||
[
|
||||
'code' => 'kiehls',
|
||||
'name' => '科颜氏',
|
||||
'en_name' => 'Kiehl\'s',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 930,
|
||||
'source_tags' => ['loreal'],
|
||||
],
|
||||
[
|
||||
'code' => 'shiseido',
|
||||
'name' => '资生堂',
|
||||
'en_name' => 'Shiseido',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 940,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'sk_ii',
|
||||
'name' => 'SK-II',
|
||||
'en_name' => 'SK-II',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 950,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'la_mer',
|
||||
'name' => '海蓝之谜',
|
||||
'en_name' => 'La Mer',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 960,
|
||||
'source_tags' => ['estee_lauder'],
|
||||
],
|
||||
[
|
||||
'code' => 'estee_lauder',
|
||||
'name' => '雅诗兰黛',
|
||||
'en_name' => 'Estee Lauder',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 970,
|
||||
'source_tags' => ['estee_lauder'],
|
||||
],
|
||||
[
|
||||
'code' => 'clinique',
|
||||
'name' => '倩碧',
|
||||
'en_name' => 'Clinique',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 980,
|
||||
'source_tags' => ['estee_lauder'],
|
||||
],
|
||||
[
|
||||
'code' => 'mac',
|
||||
'name' => '魅可',
|
||||
'en_name' => 'MAC',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 990,
|
||||
'source_tags' => ['estee_lauder'],
|
||||
],
|
||||
[
|
||||
'code' => 'bobbi_brown',
|
||||
'name' => '芭比波朗',
|
||||
'en_name' => 'Bobbi Brown',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 1000,
|
||||
'source_tags' => ['estee_lauder'],
|
||||
],
|
||||
[
|
||||
'code' => 'tom_ford_beauty',
|
||||
'name' => '汤姆福特美妆',
|
||||
'en_name' => 'Tom Ford Beauty',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 1010,
|
||||
'source_tags' => ['estee_lauder'],
|
||||
],
|
||||
[
|
||||
'code' => 'nars',
|
||||
'name' => 'NARS',
|
||||
'en_name' => 'NARS',
|
||||
'category_codes' => ['beauty'],
|
||||
'category_names' => ['高端美妆'],
|
||||
'sort_order' => 1020,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'apple',
|
||||
'name' => '苹果',
|
||||
'en_name' => 'Apple',
|
||||
'category_codes' => ['digital'],
|
||||
'category_names' => ['3C 数码'],
|
||||
'sort_order' => 1200,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'samsung',
|
||||
'name' => '三星',
|
||||
'en_name' => 'Samsung',
|
||||
'category_codes' => ['digital'],
|
||||
'category_names' => ['3C 数码'],
|
||||
'sort_order' => 1210,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'huawei',
|
||||
'name' => '华为',
|
||||
'en_name' => 'Huawei',
|
||||
'category_codes' => ['digital'],
|
||||
'category_names' => ['3C 数码'],
|
||||
'sort_order' => 1220,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'xiaomi',
|
||||
'name' => '小米',
|
||||
'en_name' => 'Xiaomi',
|
||||
'category_codes' => ['digital'],
|
||||
'category_names' => ['3C 数码'],
|
||||
'sort_order' => 1230,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
[
|
||||
'code' => 'sony',
|
||||
'name' => '索尼',
|
||||
'en_name' => 'Sony',
|
||||
'category_codes' => ['digital'],
|
||||
'category_names' => ['3C 数码'],
|
||||
'sort_order' => 1240,
|
||||
'source_tags' => ['interbrand'],
|
||||
],
|
||||
];
|
||||
296
server-api/tools/import_known_brands.php
Normal file
296
server-api/tools/import_known_brands.php
Normal file
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
const LEGACY_CODE_ALIASES = [
|
||||
'tiffany_co' => ['1'],
|
||||
];
|
||||
|
||||
$dryRun = in_array('--dry-run', $argv, true);
|
||||
$brandFile = dirname(__DIR__) . '/resources/catalog/known_brands.php';
|
||||
|
||||
if (!is_file($brandFile)) {
|
||||
fwrite(STDERR, "Known brand file not found: {$brandFile}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$brands = require $brandFile;
|
||||
if (!is_array($brands)) {
|
||||
fwrite(STDERR, "Known brand file must return an array.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
|
||||
$_ENV['DB_HOST'] ?? '127.0.0.1',
|
||||
$_ENV['DB_PORT'] ?? '3306',
|
||||
$_ENV['DB_DATABASE'] ?? '',
|
||||
$_ENV['DB_CHARSET'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$_ENV['DB_USERNAME'] ?? '',
|
||||
$_ENV['DB_PASSWORD'] ?? '',
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
function normalize_list(mixed $value): array
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($value as $item) {
|
||||
$text = trim((string)$item);
|
||||
if ($text !== '') {
|
||||
$items[] = $text;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($items));
|
||||
}
|
||||
|
||||
function decode_json_list(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return normalize_list($value);
|
||||
}
|
||||
if (!is_string($value) || trim($value) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
return normalize_list(is_array($decoded) ? $decoded : []);
|
||||
}
|
||||
|
||||
function stable_json_list(array $value): string
|
||||
{
|
||||
$items = normalize_list($value);
|
||||
sort($items, SORT_STRING);
|
||||
return json_encode($items, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
function load_enabled_categories(PDO $pdo): array
|
||||
{
|
||||
$rows = $pdo->query(
|
||||
"SELECT id, name, code, supported_service_types
|
||||
FROM catalog_categories
|
||||
WHERE is_enabled = 1
|
||||
ORDER BY sort_order ASC, id ASC"
|
||||
)->fetchAll();
|
||||
|
||||
$byCode = [];
|
||||
$byName = [];
|
||||
$byId = [];
|
||||
foreach ($rows as $row) {
|
||||
$category = [
|
||||
'id' => (int)$row['id'],
|
||||
'name' => (string)$row['name'],
|
||||
'code' => (string)$row['code'],
|
||||
'supported_service_types' => decode_json_list($row['supported_service_types'] ?? null),
|
||||
];
|
||||
$byId[$category['id']] = $category;
|
||||
$byCode[strtolower($category['code'])] = $category;
|
||||
$byName[$category['name']] = $category;
|
||||
}
|
||||
|
||||
return [$byCode, $byName, $byId];
|
||||
}
|
||||
|
||||
function match_categories(array $brand, array $categoriesByCode, array $categoriesByName): array
|
||||
{
|
||||
$matched = [];
|
||||
|
||||
foreach (normalize_list($brand['category_codes'] ?? []) as $code) {
|
||||
$key = strtolower($code);
|
||||
if (isset($categoriesByCode[$key])) {
|
||||
$matched[$categoriesByCode[$key]['id']] = $categoriesByCode[$key];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (normalize_list($brand['category_names'] ?? []) as $name) {
|
||||
if (isset($categoriesByName[$name])) {
|
||||
$matched[$categoriesByName[$name]['id']] = $categoriesByName[$name];
|
||||
}
|
||||
}
|
||||
|
||||
return $matched;
|
||||
}
|
||||
|
||||
function infer_supported_service_types(array $categories): array
|
||||
{
|
||||
$serviceTypes = [];
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category['supported_service_types'] as $serviceType) {
|
||||
$serviceTypes[$serviceType] = $serviceType;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$serviceTypes) {
|
||||
$serviceTypes['anxinyan'] = 'anxinyan';
|
||||
}
|
||||
|
||||
return array_values($serviceTypes);
|
||||
}
|
||||
|
||||
function find_existing_brand(PDO $pdo, string $code): ?array
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT * FROM catalog_brands WHERE code = ? LIMIT 1');
|
||||
$stmt->execute([$code]);
|
||||
$row = $stmt->fetch();
|
||||
if ($row) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
foreach (LEGACY_CODE_ALIASES[$code] ?? [] as $legacyCode) {
|
||||
$stmt->execute([$legacyCode]);
|
||||
$legacy = $stmt->fetch();
|
||||
if ($legacy) {
|
||||
return $legacy;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function existing_relation_ids(PDO $pdo, int $brandId): array
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT category_id FROM catalog_brand_categories WHERE brand_id = ?');
|
||||
$stmt->execute([$brandId]);
|
||||
|
||||
$ids = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$ids[(int)$row['category_id']] = true;
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
[$categoriesByCode, $categoriesByName, $categoriesById] = load_enabled_categories($pdo);
|
||||
|
||||
$stats = [
|
||||
'brands_total' => count($brands),
|
||||
'inserted' => 0,
|
||||
'updated' => 0,
|
||||
'enabled_existing' => 0,
|
||||
'legacy_code_repaired' => 0,
|
||||
'relations_inserted' => 0,
|
||||
'skipped_no_enabled_category' => 0,
|
||||
'skipped_invalid' => 0,
|
||||
];
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
$insertBrand = $pdo->prepare(
|
||||
'INSERT INTO catalog_brands (name, en_name, code, logo, sort_order, is_enabled, supported_service_types, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?)'
|
||||
);
|
||||
$updateBrand = $pdo->prepare(
|
||||
'UPDATE catalog_brands
|
||||
SET name = ?, en_name = ?, code = ?, sort_order = ?, is_enabled = 1, supported_service_types = ?, updated_at = ?
|
||||
WHERE id = ?'
|
||||
);
|
||||
$insertRelation = $pdo->prepare(
|
||||
'INSERT INTO catalog_brand_categories (brand_id, category_id, created_at)
|
||||
VALUES (?, ?, ?)'
|
||||
);
|
||||
|
||||
if (!$dryRun) {
|
||||
$pdo->beginTransaction();
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($brands as $brand) {
|
||||
$code = trim((string)($brand['code'] ?? ''));
|
||||
$name = trim((string)($brand['name'] ?? ''));
|
||||
$enName = trim((string)($brand['en_name'] ?? ''));
|
||||
if ($code === '' || $name === '' || $enName === '') {
|
||||
$stats['skipped_invalid']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchedCategories = match_categories($brand, $categoriesByCode, $categoriesByName);
|
||||
if (!$matchedCategories) {
|
||||
$stats['skipped_no_enabled_category']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$serviceTypes = infer_supported_service_types($matchedCategories);
|
||||
$serviceTypesJson = stable_json_list($serviceTypes);
|
||||
$sortOrder = (int)($brand['sort_order'] ?? 0);
|
||||
$existing = find_existing_brand($pdo, $code);
|
||||
|
||||
if ($existing) {
|
||||
$brandId = (int)$existing['id'];
|
||||
$wasDisabled = (int)$existing['is_enabled'] !== 1;
|
||||
$wasLegacyCode = (string)$existing['code'] !== $code;
|
||||
|
||||
$existingJson = stable_json_list(decode_json_list($existing['supported_service_types'] ?? null));
|
||||
$needsUpdate = $wasDisabled
|
||||
|| $wasLegacyCode
|
||||
|| (string)$existing['name'] !== $name
|
||||
|| (string)$existing['en_name'] !== $enName
|
||||
|| (int)$existing['sort_order'] !== $sortOrder
|
||||
|| $existingJson !== $serviceTypesJson;
|
||||
|
||||
if ($needsUpdate) {
|
||||
$stats['updated']++;
|
||||
if ($wasDisabled) {
|
||||
$stats['enabled_existing']++;
|
||||
}
|
||||
if ($wasLegacyCode) {
|
||||
$stats['legacy_code_repaired']++;
|
||||
}
|
||||
if (!$dryRun) {
|
||||
$updateBrand->execute([$name, $enName, $code, $sortOrder, $serviceTypesJson, $now, $brandId]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$stats['inserted']++;
|
||||
if ($dryRun) {
|
||||
$brandId = 0;
|
||||
} else {
|
||||
$insertBrand->execute([$name, $enName, $code, '', $sortOrder, $serviceTypesJson, $now, $now]);
|
||||
$brandId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
$existingCategoryIds = $brandId > 0 ? existing_relation_ids($pdo, $brandId) : [];
|
||||
foreach (array_keys($matchedCategories) as $categoryId) {
|
||||
if (isset($existingCategoryIds[(int)$categoryId])) {
|
||||
continue;
|
||||
}
|
||||
$stats['relations_inserted']++;
|
||||
if (!$dryRun && $brandId > 0) {
|
||||
$insertRelation->execute([$brandId, (int)$categoryId, $now]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$pdo->commit();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
if (!$dryRun && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
fwrite(STDERR, "IMPORT_KNOWN_BRANDS_FAILED\n");
|
||||
fwrite(STDERR, $e->getMessage() . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo $dryRun ? "DRY_RUN\n" : "IMPORT_KNOWN_BRANDS_OK\n";
|
||||
echo "ENABLED_CATEGORIES=" . count($categoriesById) . PHP_EOL;
|
||||
foreach ($stats as $key => $value) {
|
||||
echo strtoupper($key) . '=' . $value . PHP_EOL;
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
VITE_API_BASE_URL=https://test.api.anxinjianyan.com
|
||||
VITE_APP_ENV=test
|
||||
VITE_API_BASE_URL=http://127.0.0.1:8788
|
||||
VITE_APP_ENV=development
|
||||
VITE_APP_TITLE=安心验
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface CategoryOption {
|
||||
category_id: number;
|
||||
category_name: string;
|
||||
category_code: string;
|
||||
image_url?: string;
|
||||
}
|
||||
|
||||
export interface UploadItem {
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
{
|
||||
"path": "pages/order/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "订单详情"
|
||||
"navigationBarTitleText": "订单详情",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { appraisalApi } from "../../api/appraisal";
|
||||
import { appraisalApi, type AppraisalServicePackage, type PreviewData } from "../../api/appraisal";
|
||||
import { appApi, type UserAddressItem } from "../../api/app";
|
||||
import { useAppraisalStore } from "../../stores/appraisal";
|
||||
import { isMissingDraftError, rebuildDraftFromStore } from "../../utils/appraisal-flow";
|
||||
@@ -15,18 +15,24 @@ const submitting = ref(false);
|
||||
const addressSheetVisible = ref(false);
|
||||
const addressesLoading = ref(false);
|
||||
const addressOptions = ref<UserAddressItem[]>([]);
|
||||
const packageOptions = ref<AppraisalServicePackage[]>([]);
|
||||
const packageOptionsLoading = ref(false);
|
||||
const packageOptionsError = ref("");
|
||||
const packageUpdating = ref(false);
|
||||
const selectedReturnAddress = computed(() => store.returnAddress);
|
||||
const recentCreatedAddressStorageKey = "anxinyan_recent_created_address_id";
|
||||
|
||||
const serviceProviderText = computed(() => preview.value?.service_summary.service_provider_text || "安心验鉴定");
|
||||
const currentServiceProvider = computed(() => preview.value?.service_summary.service_provider || store.serviceProvider || "anxinyan");
|
||||
const packageNameText = computed(() => preview.value?.service_summary.price_package_name || store.pricePackageName || "");
|
||||
const selectedPackageId = computed(() => Number(preview.value?.service_summary.price_package_id || store.pricePackageId || 0));
|
||||
const categoryText = computed(() => preview.value?.product_summary.category_name || store.product.categoryName || "-");
|
||||
const brandText = computed(() => preview.value?.product_summary.brand_name || store.product.brandName || "未填写");
|
||||
const productNameText = computed(() => preview.value?.product_summary.product_name || `${categoryText.value} ${brandText.value === "未填写" ? "" : brandText.value}`.trim());
|
||||
const serviceFeeText = computed(() => formatMoney(preview.value?.fee_detail.service_fee || 0));
|
||||
const discountFeeText = computed(() => formatMoney(preview.value?.fee_detail.discount_fee || 0));
|
||||
const payAmountText = computed(() => formatMoney(preview.value?.fee_detail.pay_amount || 0));
|
||||
const canSubmit = computed(() => Boolean(store.draftId && store.returnAddress.id && !loading.value && !submitting.value));
|
||||
const canSubmit = computed(() => Boolean(store.draftId && store.returnAddress.id && !loading.value && !submitting.value && !packageUpdating.value));
|
||||
|
||||
function formatMoney(value: number | string) {
|
||||
const amount = Number(value || 0);
|
||||
@@ -34,6 +40,20 @@ function formatMoney(value: number | string) {
|
||||
return amount % 1 === 0 ? String(amount) : amount.toFixed(2);
|
||||
}
|
||||
|
||||
function normalizeProvider(value = "") {
|
||||
return value === "zhongjian" ? "zhongjian" : "anxinyan";
|
||||
}
|
||||
|
||||
function syncPackageFromPreview(data: PreviewData) {
|
||||
store.setServiceProvider(data.service_summary.service_provider || store.serviceProvider);
|
||||
store.setPricePackage({
|
||||
id: Number(data.service_summary.price_package_id || 0),
|
||||
packageName: data.service_summary.price_package_name || "",
|
||||
packageCode: data.service_summary.price_package_code || "",
|
||||
price: Number(data.fee_detail.service_fee || 0),
|
||||
});
|
||||
}
|
||||
|
||||
function applySelectedAddress(item: UserAddressItem) {
|
||||
store.setReturnAddress({
|
||||
id: item.id,
|
||||
@@ -125,11 +145,70 @@ async function loadPreview() {
|
||||
showInfoToast("草稿已自动恢复,请确认订单信息后继续提交。");
|
||||
}
|
||||
store.setPreview(data);
|
||||
syncPackageFromPreview(data);
|
||||
} catch (error) {
|
||||
showErrorToast(error, "订单预览加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPackageOptions() {
|
||||
if (packageOptionsLoading.value) return;
|
||||
packageOptionsLoading.value = true;
|
||||
packageOptionsError.value = "";
|
||||
try {
|
||||
const data = await appraisalApi.getServiceConfigs();
|
||||
const provider = normalizeProvider(currentServiceProvider.value);
|
||||
const config = data.list.find((item) => normalizeProvider(item.service_provider) === provider);
|
||||
packageOptions.value = config?.packages || [];
|
||||
} catch (error) {
|
||||
packageOptions.value = [];
|
||||
packageOptionsError.value = "套餐列表加载失败,当前订单仍可按已选套餐继续支付。";
|
||||
showErrorToast(error, "套餐列表加载失败");
|
||||
} finally {
|
||||
packageOptionsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectPricePackage(item: AppraisalServicePackage) {
|
||||
if (packageUpdating.value || submitting.value || loading.value) return;
|
||||
if (!item.is_enabled) {
|
||||
showInfoToast("所选价格套餐已停用,请选择其他套餐");
|
||||
return;
|
||||
}
|
||||
if (item.id === selectedPackageId.value) return;
|
||||
if (!store.draftId) {
|
||||
showInfoToast("订单信息未准备完成,请返回上一步检查");
|
||||
return;
|
||||
}
|
||||
|
||||
packageUpdating.value = true;
|
||||
try {
|
||||
await withLoading("正在更新套餐", async () => {
|
||||
const serviceProvider = normalizeProvider(currentServiceProvider.value);
|
||||
await appraisalApi.saveDraft({
|
||||
draft_id: store.draftId,
|
||||
current_step: store.currentStep || 4,
|
||||
service_provider: serviceProvider,
|
||||
price_package_id: item.id,
|
||||
price_package_code: item.package_code,
|
||||
});
|
||||
store.setServiceProvider(serviceProvider);
|
||||
store.setPricePackage({
|
||||
id: item.id,
|
||||
packageName: item.package_name,
|
||||
packageCode: item.package_code,
|
||||
price: item.price,
|
||||
});
|
||||
store.setPreview(null);
|
||||
await loadPreview();
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error, "套餐更新失败");
|
||||
} finally {
|
||||
packageUpdating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function goSuccess() {
|
||||
if (submitting.value) return;
|
||||
if (!store.draftId) {
|
||||
@@ -187,6 +266,7 @@ onLoad(async () => {
|
||||
store.hydrate();
|
||||
await fetchAddresses();
|
||||
await loadPreview();
|
||||
await loadPackageOptions();
|
||||
});
|
||||
|
||||
onShow(fetchAddresses);
|
||||
@@ -195,16 +275,8 @@ onShow(fetchAddresses);
|
||||
<template>
|
||||
<view class="confirm-page">
|
||||
<view class="confirm-nav">
|
||||
<view class="confirm-nav__home" @click="goBack">
|
||||
<view class="confirm-nav__home-roof"></view>
|
||||
<view class="confirm-nav__home-body"></view>
|
||||
</view>
|
||||
<view class="confirm-nav__back" @click="goBack"></view>
|
||||
<view class="confirm-nav__title">确认订单</view>
|
||||
<view class="confirm-nav__capsule">
|
||||
<text class="confirm-nav__dots">•••</text>
|
||||
<view class="confirm-nav__divider"></view>
|
||||
<view class="confirm-nav__circle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="confirm-state">
|
||||
@@ -234,16 +306,51 @@ onShow(fetchAddresses);
|
||||
<view class="product-summary__thumb">
|
||||
<view class="product-summary__mark">{{ brandText === "未填写" ? "AXY" : brandText.slice(0, 2) }}</view>
|
||||
</view>
|
||||
<view class="product-summary__info">
|
||||
<view class="product-summary__row">服务:{{ serviceProviderText }}</view>
|
||||
<view v-if="packageNameText" class="product-summary__row">套餐:{{ packageNameText }}</view>
|
||||
<view class="product-summary__row">品类:{{ categoryText }}</view>
|
||||
<view class="product-summary__row">品牌:{{ brandText }}</view>
|
||||
<view class="product-summary__info">
|
||||
<view class="product-summary__row">服务:{{ serviceProviderText }}</view>
|
||||
<view class="product-summary__row">品类:{{ categoryText }}</view>
|
||||
<view class="product-summary__row">品牌:{{ brandText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="confirm-card__muted confirm-card__muted--top">{{ productNameText }}</view>
|
||||
</view>
|
||||
|
||||
<view class="confirm-card package-section">
|
||||
<view class="package-section__head">
|
||||
<view>
|
||||
<view class="confirm-card__title">价格套餐</view>
|
||||
<view class="package-section__desc">选择本次鉴定服务使用的价格套餐。</view>
|
||||
</view>
|
||||
<view v-if="packageUpdating" class="package-section__status">更新中...</view>
|
||||
</view>
|
||||
|
||||
<view v-if="packageOptionsLoading" class="package-empty">套餐加载中...</view>
|
||||
<view v-else-if="packageOptionsError" class="package-empty">{{ packageOptionsError }}</view>
|
||||
<view v-else-if="packageOptions.length" class="package-list">
|
||||
<view
|
||||
v-for="item in packageOptions"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'package-card',
|
||||
selectedPackageId === item.id ? 'package-card--selected' : '',
|
||||
packageUpdating ? 'package-card--disabled' : '',
|
||||
]"
|
||||
@click="selectPricePackage(item)"
|
||||
>
|
||||
<view class="package-card__main">
|
||||
<view class="package-card__name">{{ item.package_name }}</view>
|
||||
<view v-if="item.description" class="package-card__desc">{{ item.description }}</view>
|
||||
</view>
|
||||
<view class="package-card__side">
|
||||
<view class="package-card__price">¥{{ formatMoney(item.price) }}</view>
|
||||
<view v-if="selectedPackageId === item.id" class="package-card__tag">已选</view>
|
||||
<view v-else-if="item.is_default" class="package-card__tag package-card__tag--muted">默认</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="package-empty">当前服务暂无可切换套餐</view>
|
||||
</view>
|
||||
|
||||
<view class="confirm-card">
|
||||
<view class="fee-head">
|
||||
<view class="confirm-card__title">费用明细</view>
|
||||
@@ -340,51 +447,34 @@ onShow(fetchAddresses);
|
||||
}
|
||||
|
||||
.confirm-nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
min-height: 72rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.confirm-nav__home {
|
||||
position: relative;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
}
|
||||
|
||||
.confirm-nav__home-roof {
|
||||
.confirm-nav__back {
|
||||
position: absolute;
|
||||
left: 10rpx;
|
||||
top: 4rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-left: 5rpx solid #252527;
|
||||
border-top: 5rpx solid #252527;
|
||||
transform: rotate(45deg);
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.confirm-nav__home-body {
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
bottom: 6rpx;
|
||||
width: 30rpx;
|
||||
height: 28rpx;
|
||||
border: 5rpx solid #252527;
|
||||
border-top: 0;
|
||||
border-radius: 4rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.confirm-nav__home-body::after {
|
||||
.confirm-nav__back::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 2rpx;
|
||||
bottom: 2rpx;
|
||||
width: 18rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 12rpx 12rpx 12rpx 4rpx;
|
||||
background: #edbd00;
|
||||
left: 20rpx;
|
||||
top: 17rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-left: 5rpx solid #252527;
|
||||
border-bottom: 5rpx solid #252527;
|
||||
border-radius: 2rpx;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.confirm-nav__title {
|
||||
@@ -394,38 +484,6 @@ onShow(fetchAddresses);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.confirm-nav__capsule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150rpx;
|
||||
height: 64rpx;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.confirm-nav__dots {
|
||||
color: #111111;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.confirm-nav__divider {
|
||||
width: 1px;
|
||||
height: 36rpx;
|
||||
margin: 0 18rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.confirm-nav__circle {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border: 7rpx solid #111111;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.confirm-state,
|
||||
.confirm-card,
|
||||
.agreement-item {
|
||||
@@ -595,6 +653,116 @@ onShow(fetchAddresses);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.package-section__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.package-section__desc {
|
||||
margin-top: 6rpx;
|
||||
color: #a0a0a4;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.package-section__status {
|
||||
flex-shrink: 0;
|
||||
color: #a0a0a4;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.package-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.package-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
min-height: 128rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border: 2rpx solid #e6e6e8;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.package-card--selected {
|
||||
border-color: #edbd00;
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
.package-card--disabled {
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.package-card__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.package-card__name {
|
||||
color: #252527;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.package-card__desc {
|
||||
margin-top: 8rpx;
|
||||
color: #8c8c90;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.package-card__side {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.package-card__price {
|
||||
color: #edbd00;
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.package-card__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 64rpx;
|
||||
height: 34rpx;
|
||||
margin-top: 8rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(237, 189, 0, 0.13);
|
||||
color: #9a7700;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.package-card__tag--muted {
|
||||
background: #f0f0f2;
|
||||
color: #8c8c90;
|
||||
}
|
||||
|
||||
.package-empty {
|
||||
margin-top: 20rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f7f7f8;
|
||||
color: #9a9a9d;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fee-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -175,7 +175,7 @@ async function saveProductAndGoConfirm(payload: { brandId: number; brandName: st
|
||||
function selectBrand(item: BrandOption) {
|
||||
void saveProductAndGoConfirm({
|
||||
brandId: item.id,
|
||||
brandName: item.name || item.enName || item.displayName,
|
||||
brandName: item.displayName,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { appraisalApi, type AppraisalServiceConfig, type AppraisalServicePackage, type CategoryOption } from "../../api/appraisal";
|
||||
import { appraisalApi, type CategoryOption } from "../../api/appraisal";
|
||||
import { useAppraisalStore } from "../../stores/appraisal";
|
||||
import { isLoggedIn, redirectToLogin } from "../../utils/auth";
|
||||
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||
@@ -12,6 +12,7 @@ type CategoryPickerItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
imageUrl: string;
|
||||
visual: string;
|
||||
};
|
||||
|
||||
@@ -19,7 +20,6 @@ const providerIntro: Record<ServiceProvider, {
|
||||
navTitle: string;
|
||||
logoText: string;
|
||||
intro: string;
|
||||
priceText: string;
|
||||
highlights: string[];
|
||||
steps: Array<{
|
||||
title: string;
|
||||
@@ -31,7 +31,6 @@ const providerIntro: Record<ServiceProvider, {
|
||||
navTitle: "安心验鉴定",
|
||||
logoText: "安心验 鉴定",
|
||||
intro: "安心验(深圳)商品检验鉴定有限责任公司立足深圳核心产业服务区,是一家专业从事商品检验、鉴定、测试及技术咨询的第三方服务机构。公司依托粤港澳大湾区雄厚的产业基础与国际贸易枢纽优势,致力于为 C 端消费者及 B 端电商平台、商家提供网购商品真伪鉴定、成色评级、价值评估及争议仲裁等一站式解决方案。",
|
||||
priceText: "¥99 起",
|
||||
highlights: ["独立第三方", "报告可验真", "流程可追踪"],
|
||||
steps: [
|
||||
{ title: "下单付款", desc: "商品寄至鉴定中心,录像验收。", visual: "pay" },
|
||||
@@ -44,7 +43,6 @@ const providerIntro: Record<ServiceProvider, {
|
||||
navTitle: "中检鉴定",
|
||||
logoText: "中检 鉴定",
|
||||
intro: "中检鉴定服务面向更高规格出具需求,沿用安心验标准化下单、寄送与进度追踪流程,由合作机构完成对应服务交付,适用于对报告出具方有明确要求的鉴定场景。",
|
||||
priceText: "¥199 起",
|
||||
highlights: ["合作机构", "报告出具方不同", "流程一致"],
|
||||
steps: [
|
||||
{ title: "选择中检服务", desc: "首页选定中检鉴定后,确认品类、品牌和费用。", visual: "pay" },
|
||||
@@ -75,24 +73,8 @@ const categorySheetVisible = ref(false);
|
||||
const submitting = ref(false);
|
||||
const loadError = ref("");
|
||||
const selectedCategoryId = ref(0);
|
||||
const selectedPackageId = ref(0);
|
||||
const serviceConfigsLoading = ref(false);
|
||||
const serviceConfigs = ref<Record<ServiceProvider, AppraisalServiceConfig | undefined>>({
|
||||
anxinyan: undefined,
|
||||
zhongjian: undefined,
|
||||
});
|
||||
|
||||
const currentIntro = computed(() => providerIntro[providerCode.value]);
|
||||
const currentPackages = computed<AppraisalServicePackage[]>(() => serviceConfigs.value[providerCode.value]?.packages || []);
|
||||
const currentPackage = computed(() => currentPackages.value.find((item) => item.id === selectedPackageId.value) || currentPackages.value.find((item) => item.is_default) || currentPackages.value[0]);
|
||||
const currentPriceText = computed(() => {
|
||||
if (currentPackage.value) {
|
||||
return `¥${formatPrice(Number(currentPackage.value.price))}`;
|
||||
}
|
||||
const price = serviceConfigs.value[providerCode.value]?.price;
|
||||
return Number(price || 0) > 0 ? `¥${formatPrice(Number(price))} 起` : currentIntro.value.priceText;
|
||||
});
|
||||
const currentPriceDesc = computed(() => currentPackage.value?.package_name || "请选择价格套餐");
|
||||
const providerThemeClass = computed(() => `service-intro--${providerCode.value}`);
|
||||
|
||||
function normalizeProvider(value?: string): ServiceProvider {
|
||||
@@ -116,44 +98,6 @@ function resolveCategoryVisual(item: CategoryOption) {
|
||||
return matched?.visual || "default";
|
||||
}
|
||||
|
||||
function formatPrice(price: number) {
|
||||
return Number.isInteger(price) ? String(price) : price.toFixed(2);
|
||||
}
|
||||
|
||||
async function loadServiceConfigs() {
|
||||
if (serviceConfigsLoading.value) return;
|
||||
serviceConfigsLoading.value = true;
|
||||
try {
|
||||
const data = await appraisalApi.getServiceConfigs();
|
||||
const nextConfigs: Record<ServiceProvider, AppraisalServiceConfig | undefined> = {
|
||||
anxinyan: undefined,
|
||||
zhongjian: undefined,
|
||||
};
|
||||
data.list.forEach((item) => {
|
||||
const provider = normalizeProvider(item.service_provider);
|
||||
nextConfigs[provider] = item;
|
||||
});
|
||||
serviceConfigs.value = nextConfigs;
|
||||
applyDefaultPackage();
|
||||
} catch (error) {
|
||||
console.warn("service config fallback", error);
|
||||
} finally {
|
||||
serviceConfigsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyDefaultPackage() {
|
||||
const packages = currentPackages.value;
|
||||
if (packages.some((item) => item.id === selectedPackageId.value)) return;
|
||||
const target = packages.find((item) => item.is_default) || packages[0];
|
||||
selectedPackageId.value = target?.id || 0;
|
||||
}
|
||||
|
||||
function selectPackage(item: AppraisalServicePackage) {
|
||||
if (!item.is_enabled) return;
|
||||
selectedPackageId.value = item.id;
|
||||
}
|
||||
|
||||
function buildServicePageUrl(options: ServicePageOptions = {}) {
|
||||
const params: string[] = [];
|
||||
const provider = options.provider || providerCode.value;
|
||||
@@ -189,6 +133,7 @@ async function loadCategories() {
|
||||
id: item.category_id,
|
||||
name: item.category_name,
|
||||
code: item.category_code,
|
||||
imageUrl: item.image_url || "",
|
||||
visual: resolveCategoryVisual(item),
|
||||
}));
|
||||
categoriesLoaded.value = true;
|
||||
@@ -209,14 +154,6 @@ async function openCategorySheet() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentPackage.value) {
|
||||
await loadServiceConfigs();
|
||||
}
|
||||
if (!currentPackage.value) {
|
||||
showInfoToast("当前服务暂无可用价格套餐");
|
||||
return;
|
||||
}
|
||||
|
||||
categorySheetVisible.value = true;
|
||||
await loadCategories();
|
||||
}
|
||||
@@ -228,11 +165,6 @@ function closeCategorySheet() {
|
||||
|
||||
async function selectCategory(item: CategoryPickerItem) {
|
||||
if (submitting.value) return;
|
||||
const selectedPackage = currentPackage.value;
|
||||
if (!selectedPackage) {
|
||||
showInfoToast("请先选择价格套餐");
|
||||
return;
|
||||
}
|
||||
selectedCategoryId.value = item.id;
|
||||
submitting.value = true;
|
||||
try {
|
||||
@@ -241,13 +173,7 @@ async function selectCategory(item: CategoryPickerItem) {
|
||||
store.resetForNewFlow();
|
||||
store.clearLegacyExtraDefaults();
|
||||
store.setServiceProvider(providerCode.value);
|
||||
store.setPricePackage({
|
||||
id: selectedPackage.id,
|
||||
packageName: selectedPackage.package_name,
|
||||
packageCode: selectedPackage.package_code,
|
||||
price: selectedPackage.price,
|
||||
});
|
||||
const draft = await appraisalApi.createDraft(providerCode.value, selectedPackage.id, selectedPackage.package_code);
|
||||
const draft = await appraisalApi.createDraft(providerCode.value);
|
||||
draftId = draft.draft_id;
|
||||
store.setDraft(draftId);
|
||||
store.setPricePackage({
|
||||
@@ -293,21 +219,18 @@ function categoryKey(item: CategoryPickerItem) {
|
||||
return `${item.id}-${item.name}`;
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack();
|
||||
function goHome() {
|
||||
uni.switchTab({ url: "/pages/home/index" });
|
||||
}
|
||||
|
||||
function applyProviderFromOptions(options: ServicePageOptions = {}) {
|
||||
providerCode.value = normalizeProvider(options.provider);
|
||||
selectedPackageId.value = 0;
|
||||
applyDefaultPackage();
|
||||
uni.setNavigationBarTitle({ title: currentIntro.value.navTitle });
|
||||
}
|
||||
|
||||
onLoad((options: ServicePageOptions = {}) => {
|
||||
const resolvedOptions = resolveServicePageOptions(options);
|
||||
applyProviderFromOptions(resolvedOptions);
|
||||
void loadServiceConfigs();
|
||||
if (resolvedOptions.start === "1" && isLoggedIn()) {
|
||||
setTimeout(() => {
|
||||
void openCategorySheet();
|
||||
@@ -322,16 +245,11 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
<view :class="['service-intro', providerThemeClass]">
|
||||
<view class="service-intro__hero">
|
||||
<view class="service-intro__nav">
|
||||
<view class="service-intro__home" @click="goBack">
|
||||
<view class="service-intro__home" @click="goHome">
|
||||
<view class="service-intro__home-roof"></view>
|
||||
<view class="service-intro__home-body"></view>
|
||||
</view>
|
||||
<view class="service-intro__title">鉴定服务</view>
|
||||
<view class="service-intro__capsule">
|
||||
<text class="service-intro__dots">•••</text>
|
||||
<view class="service-intro__divider"></view>
|
||||
<view class="service-intro__circle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="service-intro__brand">
|
||||
@@ -363,32 +281,9 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="service-intro__package-title">价格套餐</view>
|
||||
<view v-if="currentPackages.length" class="package-list">
|
||||
<view
|
||||
v-for="item in currentPackages"
|
||||
:key="item.id"
|
||||
:class="['package-card', selectedPackageId === item.id ? 'package-card--selected' : '']"
|
||||
@click="selectPackage(item)"
|
||||
>
|
||||
<view class="package-card__main">
|
||||
<view class="package-card__name">{{ item.package_name }}</view>
|
||||
<view v-if="item.description" class="package-card__desc">{{ item.description }}</view>
|
||||
</view>
|
||||
<view class="package-card__side">
|
||||
<view class="package-card__price">¥{{ formatPrice(item.price) }}</view>
|
||||
<view v-if="item.is_default" class="package-card__tag">默认</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="package-empty">{{ serviceConfigsLoading ? "套餐加载中..." : "当前服务暂无可用套餐" }}</view>
|
||||
</view>
|
||||
|
||||
<view class="service-intro__action-bar">
|
||||
<view>
|
||||
<view class="service-intro__price">{{ currentPriceText }}</view>
|
||||
<view class="service-intro__price-desc">{{ currentPriceDesc }}</view>
|
||||
</view>
|
||||
<view :class="['service-intro__primary', submitting ? 'service-intro__primary--disabled' : '']" @click="openCategorySheet">
|
||||
{{ submitting ? "处理中..." : "发起鉴定" }}
|
||||
</view>
|
||||
@@ -412,7 +307,13 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
@click="selectCategory(item)"
|
||||
>
|
||||
<view class="category-card__name">{{ item.name }}</view>
|
||||
<view :class="['category-card__visual', `category-card__visual--${item.visual}`]"></view>
|
||||
<image
|
||||
v-if="item.imageUrl"
|
||||
class="category-card__image"
|
||||
:src="item.imageUrl"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view v-else :class="['category-card__visual', `category-card__visual--${item.visual}`]"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -495,22 +396,25 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
min-height: 64rpx;
|
||||
}
|
||||
|
||||
.service-intro__home {
|
||||
position: relative;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.service-intro__home-roof {
|
||||
position: absolute;
|
||||
left: 10rpx;
|
||||
top: 4rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
left: 15rpx;
|
||||
top: 9rpx;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border-left: 5rpx solid #252527;
|
||||
border-top: 5rpx solid #252527;
|
||||
transform: rotate(45deg);
|
||||
@@ -518,10 +422,10 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
|
||||
.service-intro__home-body {
|
||||
position: absolute;
|
||||
left: 12rpx;
|
||||
bottom: 6rpx;
|
||||
width: 30rpx;
|
||||
height: 28rpx;
|
||||
left: 17rpx;
|
||||
bottom: 10rpx;
|
||||
width: 31rpx;
|
||||
height: 29rpx;
|
||||
border: 5rpx solid #252527;
|
||||
border-top: 0;
|
||||
border-radius: 4rpx;
|
||||
@@ -546,38 +450,6 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.service-intro__capsule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 146rpx;
|
||||
height: 62rpx;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.service-intro__dots {
|
||||
color: #111111;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.service-intro__divider {
|
||||
width: 1px;
|
||||
height: 36rpx;
|
||||
margin: 0 18rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.service-intro__circle {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
border: 7rpx solid #111111;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.service-intro__brand {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -839,109 +711,6 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
width: 232rpx;
|
||||
}
|
||||
|
||||
.service-intro__package-title {
|
||||
margin-top: 6rpx;
|
||||
color: #252527;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.package-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.package-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
min-height: 128rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
border: 2rpx solid #e6e6e8;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.package-card--selected {
|
||||
border-color: #edbd00;
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
.service-intro--zhongjian .package-card--selected {
|
||||
border-color: #416f9e;
|
||||
background: #f3f7fb;
|
||||
}
|
||||
|
||||
.package-card__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.package-card__name {
|
||||
color: #252527;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.package-card__desc {
|
||||
margin-top: 8rpx;
|
||||
color: #8c8c90;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.package-card__side {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.package-card__price {
|
||||
color: #edbd00;
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.service-intro--zhongjian .package-card__price {
|
||||
color: #416f9e;
|
||||
}
|
||||
|
||||
.package-card__tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 64rpx;
|
||||
height: 34rpx;
|
||||
margin-top: 8rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(237, 189, 0, 0.13);
|
||||
color: #9a7700;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.service-intro--zhongjian .package-card__tag {
|
||||
background: rgba(65, 111, 158, 0.12);
|
||||
color: #416f9e;
|
||||
}
|
||||
|
||||
.package-empty {
|
||||
margin-top: 20rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
color: #9a9a9d;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.service-intro__action-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -950,33 +719,18 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
z-index: 90;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 28rpx;
|
||||
justify-content: center;
|
||||
padding: 22rpx 32rpx calc(22rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #e9e9eb;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.service-intro__price {
|
||||
color: #edbd00;
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.service-intro__price-desc {
|
||||
margin-top: 4rpx;
|
||||
color: #9a9a9d;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.service-intro__primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
max-width: 360rpx;
|
||||
max-width: 640rpx;
|
||||
height: 86rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #edbd00;
|
||||
@@ -1095,6 +849,14 @@ onLoad((options: ServicePageOptions = {}) => {
|
||||
background: #f7f7f8;
|
||||
}
|
||||
|
||||
.category-card__image {
|
||||
position: absolute;
|
||||
right: 18rpx;
|
||||
bottom: 18rpx;
|
||||
width: 128rpx;
|
||||
height: 104rpx;
|
||||
}
|
||||
|
||||
.category-card__visual::before,
|
||||
.category-card__visual::after {
|
||||
content: "";
|
||||
|
||||
@@ -12,7 +12,7 @@ const pageLoading = ref(false);
|
||||
const pageReady = ref(false);
|
||||
const categoryDataLoaded = ref(false);
|
||||
const loadError = ref("");
|
||||
const defaultHeroBackground = "/static/home/home-reference.jpg";
|
||||
const defaultHeroBackground = "/static/home/home-hero-bg.png";
|
||||
|
||||
const categoryFallbackVisuals = [
|
||||
{ visual: "bag", keys: ["luxury_bag", "奢侈品箱包", "箱包"] },
|
||||
@@ -239,7 +239,7 @@ onShow(fetchHome);
|
||||
height: 470rpx;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-position: center 44%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +209,10 @@ const secondaryActionText = computed(() =>
|
||||
isPendingPayment.value ? (cancelSubmitting.value ? "取消中..." : "取消订单") : detail.value.available_actions.secondary_action,
|
||||
);
|
||||
|
||||
function goOrderList() {
|
||||
uni.switchTab({ url: "/pages/order/index" });
|
||||
}
|
||||
|
||||
async function fetchDetail() {
|
||||
if (!orderId.value) return;
|
||||
loading.value = true;
|
||||
@@ -424,6 +428,11 @@ onShow(async () => {
|
||||
|
||||
<template>
|
||||
<view class="app-page app-page--tight">
|
||||
<view class="order-detail-nav">
|
||||
<view class="order-detail-nav__back" @click="goOrderList"></view>
|
||||
<view class="order-detail-nav__title">订单详情</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!pageReady && loading" class="section notice-card">
|
||||
<view class="notice-card__title">正在加载订单详情</view>
|
||||
<view class="notice-card__desc">请稍候,我们正在同步订单状态、资料、寄回地址和处理记录。</view>
|
||||
@@ -710,6 +719,44 @@ onShow(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.order-detail-nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 72rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.order-detail-nav__back {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.order-detail-nav__back::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
top: 17rpx;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-left: 5rpx solid #252527;
|
||||
border-bottom: 5rpx solid #252527;
|
||||
border-radius: 2rpx;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.order-detail-nav__title {
|
||||
color: var(--color-heading);
|
||||
font-size: 38rpx;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.order-detail-hero {
|
||||
padding-bottom: 34rpx;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ onShow(async () => {
|
||||
{{ item.service_provider === "zhongjian" ? "中检鉴定" : "安心验鉴定" }}
|
||||
<text v-if="item.price_package_name"> / {{ item.price_package_name }}</text>
|
||||
</view>
|
||||
<view class="order-card__action">{{ item.primary_action }}</view>
|
||||
<view v-if="item.primary_action" class="order-card__action">{{ item.primary_action }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
BIN
user-app/src/static/home/home-hero-bg.png
Executable file
BIN
user-app/src/static/home/home-hero-bg.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
@@ -199,6 +199,12 @@ export interface AdminManualOrderMeta {
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AdminCatalogCategoryOption {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface AdminOrderDetail {
|
||||
order_info: AdminOrderListItem & {
|
||||
can_mark_received: boolean;
|
||||
@@ -541,6 +547,25 @@ export const adminApi = {
|
||||
getManualOrderMeta() {
|
||||
return request<AdminManualOrderMeta>("/api/admin/manual-order/meta");
|
||||
},
|
||||
async getAppCatalogCategories() {
|
||||
const data = await request<{
|
||||
category_entries: Array<{
|
||||
category_id: number;
|
||||
category_name: string;
|
||||
category_code: string;
|
||||
}>;
|
||||
}>("/api/app/home/index");
|
||||
|
||||
const list: AdminCatalogCategoryOption[] = data.category_entries.map((item) => ({
|
||||
id: item.category_id,
|
||||
name: item.category_name,
|
||||
code: item.category_code,
|
||||
}));
|
||||
|
||||
return {
|
||||
list,
|
||||
};
|
||||
},
|
||||
getExpressCompanies(params: { enabled_only?: 0 | 1 } = { enabled_only: 1 }) {
|
||||
return request<{ list: AdminExpressCompanyItem[]; default_company: string }>("/api/admin/express-companies", { params });
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { adminApi, type AdminAppraisalTaskDetail, type AdminFileAsset } from "../../api/admin";
|
||||
import { adminApi, type AdminAppraisalTaskDetail, type AdminCatalogCategoryOption, type AdminFileAsset } from "../../api/admin";
|
||||
import { showErrorToast, showInfoToast, withLoading } from "../../utils/feedback";
|
||||
|
||||
const loading = ref(false);
|
||||
@@ -25,6 +25,7 @@ const externalRemark = ref("");
|
||||
const internalRemark = ref("");
|
||||
const zhongjianReportNo = ref("");
|
||||
const productName = ref("");
|
||||
const categoryId = ref(0);
|
||||
const categoryName = ref("");
|
||||
const brandName = ref("");
|
||||
const color = ref("");
|
||||
@@ -33,6 +34,8 @@ const serialNo = ref("");
|
||||
const zhongjianFiles = ref<AdminFileAsset[]>([]);
|
||||
const evidenceFiles = ref<AdminFileAsset[]>([]);
|
||||
const activePreviewVideo = ref<AdminFileAsset | null>(null);
|
||||
const catalogCategories = ref<AdminCatalogCategoryOption[]>([]);
|
||||
const categoryLoading = ref(false);
|
||||
const supplementForm = reactive({
|
||||
reason: "",
|
||||
deadline: "",
|
||||
@@ -56,6 +59,13 @@ const internalTagNo = computed(() => detail.value?.task_info.internal_tag_no ||
|
||||
const resultSummary = computed(() => detail.value?.result_info.result_text || "暂未填写");
|
||||
const reportSummary = computed(() => detail.value?.report_summary?.report_no || "");
|
||||
const hasBoundMaterialTag = computed(() => Boolean(detail.value?.material_tag?.id));
|
||||
const categoryOptions = computed(() => catalogCategories.value);
|
||||
const categoryPickerIndex = computed(() => Math.max(0, categoryOptions.value.findIndex((item) => item.id === categoryId.value)));
|
||||
const categoryPickerLabel = computed(() => selectedCategoryName(categoryId.value) || categoryName.value.trim());
|
||||
const categoryPickerPlaceholder = computed(() => {
|
||||
if (categoryLoading.value) return "正在加载品类";
|
||||
return categoryOptions.value.length ? "请选择品类" : "暂无可选品类";
|
||||
});
|
||||
type AppraisalTemplate = NonNullable<AdminAppraisalTaskDetail["appraisal_template"]>;
|
||||
|
||||
function hasConditionFields(template?: AppraisalTemplate | null) {
|
||||
@@ -74,6 +84,58 @@ function formatMoneyInput(value: string | number) {
|
||||
return Number.isFinite(num) ? num : 0;
|
||||
}
|
||||
|
||||
function selectedCategoryName(selectedCategoryId: number) {
|
||||
return categoryOptions.value.find((item) => item.id === selectedCategoryId)?.name || "";
|
||||
}
|
||||
|
||||
function resolveCategoryId(selectedCategoryId: number, selectedCategoryNameText: string) {
|
||||
if (selectedCategoryId) return selectedCategoryId;
|
||||
const categoryNameText = selectedCategoryNameText.trim();
|
||||
return categoryOptions.value.find((item) => item.name === categoryNameText)?.id || 0;
|
||||
}
|
||||
|
||||
function syncCurrentCategory() {
|
||||
const resolvedCategoryId = resolveCategoryId(categoryId.value, categoryName.value);
|
||||
categoryId.value = resolvedCategoryId;
|
||||
categoryName.value = selectedCategoryName(resolvedCategoryId) || categoryName.value;
|
||||
}
|
||||
|
||||
async function fetchCatalogMeta() {
|
||||
if (catalogCategories.value.length || categoryLoading.value) return;
|
||||
categoryLoading.value = true;
|
||||
try {
|
||||
const data = await adminApi.getAppCatalogCategories();
|
||||
catalogCategories.value = data.list;
|
||||
syncCurrentCategory();
|
||||
} catch (error) {
|
||||
showErrorToast(error, "品类列表加载失败");
|
||||
} finally {
|
||||
categoryLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onCategoryChange(event: any) {
|
||||
if (isTaskReadonly.value) return;
|
||||
const index = Number(event.detail?.value || 0);
|
||||
const category = categoryOptions.value[index];
|
||||
if (!category) return;
|
||||
categoryId.value = category.id;
|
||||
categoryName.value = category.name;
|
||||
}
|
||||
|
||||
function productInfoPayload() {
|
||||
const resolvedCategoryId = resolveCategoryId(categoryId.value, categoryName.value);
|
||||
return {
|
||||
category_id: resolvedCategoryId,
|
||||
product_name: productName.value.trim(),
|
||||
category_name: selectedCategoryName(resolvedCategoryId) || categoryName.value.trim(),
|
||||
brand_name: brandName.value.trim(),
|
||||
color: color.value.trim(),
|
||||
size_spec: sizeSpec.value.trim(),
|
||||
serial_no: serialNo.value.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function hydrate(detailData: AdminAppraisalTaskDetail) {
|
||||
detail.value = detailData;
|
||||
activeSection.value = detailData.task_info.service_provider === "zhongjian"
|
||||
@@ -102,7 +164,9 @@ function hydrate(detailData: AdminAppraisalTaskDetail) {
|
||||
internalRemark.value = detailData.result_info.internal_remark || "";
|
||||
zhongjianReportNo.value = detailData.zhongjian_report?.report_no || "";
|
||||
productName.value = detailData.product_info.product_name || "";
|
||||
categoryId.value = resolveCategoryId(detailData.product_info.category_id, detailData.product_info.category_name || "");
|
||||
categoryName.value = detailData.product_info.category_name || "";
|
||||
syncCurrentCategory();
|
||||
brandName.value = detailData.product_info.brand_name || "";
|
||||
color.value = detailData.product_info.color || "";
|
||||
sizeSpec.value = detailData.product_info.size_spec || "";
|
||||
@@ -556,15 +620,7 @@ async function submitResult(action: "save" | "submit") {
|
||||
adminApi.saveAppraisalTaskResult({
|
||||
id: detail.value!.task_info.id,
|
||||
action,
|
||||
product_info: {
|
||||
category_id: detail.value!.product_info.category_id,
|
||||
product_name: productName.value.trim(),
|
||||
category_name: categoryName.value.trim(),
|
||||
brand_name: brandName.value.trim(),
|
||||
color: color.value.trim(),
|
||||
size_spec: sizeSpec.value.trim(),
|
||||
serial_no: serialNo.value.trim(),
|
||||
},
|
||||
product_info: productInfoPayload(),
|
||||
result_text: resultText.value.trim(),
|
||||
result_desc: resultDesc.value.trim(),
|
||||
...conditionPayload,
|
||||
@@ -662,15 +718,7 @@ async function submitZhongjianReport() {
|
||||
await adminApi.saveZhongjianAppraisalReport({
|
||||
id: detail.value.task_info.id,
|
||||
zhongjian_report_no: zhongjianReportNo.value.trim(),
|
||||
product_info: {
|
||||
category_id: detail.value.product_info.category_id,
|
||||
product_name: productName.value.trim(),
|
||||
category_name: categoryName.value.trim(),
|
||||
brand_name: brandName.value.trim(),
|
||||
color: color.value.trim(),
|
||||
size_spec: sizeSpec.value.trim(),
|
||||
serial_no: serialNo.value.trim(),
|
||||
},
|
||||
product_info: productInfoPayload(),
|
||||
result_text: resultText.value.trim(),
|
||||
result_desc: resultDesc.value.trim(),
|
||||
attachments: evidenceFiles.value,
|
||||
@@ -700,8 +748,11 @@ onLoad((options) => {
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (taskId.value && !pageReady.value) {
|
||||
void fetchDetail();
|
||||
if (taskId.value) {
|
||||
void fetchCatalogMeta();
|
||||
if (!pageReady.value) {
|
||||
void fetchDetail();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -767,7 +818,19 @@ onShow(() => {
|
||||
<view class="stack" style="margin-top: 18rpx">
|
||||
<view class="card-desc">报告展示信息</view>
|
||||
<input v-model="productName" class="field" :disabled="isTaskReadonly" placeholder="产品名称" />
|
||||
<input v-model="categoryName" class="field" :disabled="isTaskReadonly" placeholder="品类" />
|
||||
<picker
|
||||
:range="categoryOptions"
|
||||
range-key="name"
|
||||
:value="categoryPickerIndex"
|
||||
:disabled="isTaskReadonly || categoryLoading || !categoryOptions.length"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<view class="field picker-field" :class="{ 'picker-field--disabled': isTaskReadonly }">
|
||||
<text v-if="categoryPickerLabel" class="picker-field__value">{{ categoryPickerLabel }}</text>
|
||||
<text v-else class="picker-field__placeholder">{{ categoryPickerPlaceholder }}</text>
|
||||
<text v-if="!isTaskReadonly" class="picker-field__arrow"></text>
|
||||
</view>
|
||||
</picker>
|
||||
<input v-model="brandName" class="field" :disabled="isTaskReadonly" placeholder="品牌" />
|
||||
<view class="meta-grid">
|
||||
<input v-model="color" class="field" :disabled="isTaskReadonly" placeholder="颜色" />
|
||||
@@ -876,7 +939,19 @@ onShow(() => {
|
||||
<view class="stack" style="margin-top: 18rpx">
|
||||
<view class="card-desc">报告展示信息</view>
|
||||
<input v-model="productName" class="field" :disabled="isTaskReadonly" placeholder="产品名称" />
|
||||
<input v-model="categoryName" class="field" :disabled="isTaskReadonly" placeholder="品类" />
|
||||
<picker
|
||||
:range="categoryOptions"
|
||||
range-key="name"
|
||||
:value="categoryPickerIndex"
|
||||
:disabled="isTaskReadonly || categoryLoading || !categoryOptions.length"
|
||||
@change="onCategoryChange"
|
||||
>
|
||||
<view class="field picker-field" :class="{ 'picker-field--disabled': isTaskReadonly }">
|
||||
<text v-if="categoryPickerLabel" class="picker-field__value">{{ categoryPickerLabel }}</text>
|
||||
<text v-else class="picker-field__placeholder">{{ categoryPickerPlaceholder }}</text>
|
||||
<text v-if="!isTaskReadonly" class="picker-field__arrow"></text>
|
||||
</view>
|
||||
</picker>
|
||||
<input v-model="brandName" class="field" :disabled="isTaskReadonly" placeholder="品牌" />
|
||||
<view class="meta-grid">
|
||||
<input v-model="color" class="field" :disabled="isTaskReadonly" placeholder="颜色" />
|
||||
@@ -1051,6 +1126,42 @@ onShow(() => {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.picker-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.picker-field--disabled {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.picker-field__value,
|
||||
.picker-field__placeholder {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-field__value {
|
||||
color: var(--work-text);
|
||||
}
|
||||
|
||||
.picker-field__placeholder {
|
||||
color: var(--work-text-muted);
|
||||
}
|
||||
|
||||
.picker-field__arrow {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
flex: 0 0 14rpx;
|
||||
border-right: 3rpx solid var(--work-text-soft);
|
||||
border-bottom: 3rpx solid var(--work-text-soft);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.attachment-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user