diff --git a/cmd/migrate-member-mobile/main.go b/cmd/migrate-member-mobile/main.go new file mode 100644 index 0000000..6923968 --- /dev/null +++ b/cmd/migrate-member-mobile/main.go @@ -0,0 +1,268 @@ +package main + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "log" + "os" + "regexp" + "strings" + + "git.118im.com/im_team/im_workbench/internal/config" + _ "github.com/go-sql-driver/mysql" +) + +var phonePattern = regexp.MustCompile(`^1[3-9][0-9]{9}$`) + +type memberPhone struct { + id int64 + tenantID int64 + ciphertext string + hash string + mask string + phone string +} + +func main() { + cfg, err := config.Load() + if err != nil { + log.Fatal(err) + } + db, err := sql.Open("mysql", strings.TrimPrefix(cfg.MySQL.DSN, "mysql://")) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + ctx := context.Background() + hasMobile, err := columnExists(ctx, db, "mobile") + if err != nil { + log.Fatal(err) + } + hasCipher, err := columnExists(ctx, db, "mobile_cipher") + if err != nil { + log.Fatal(err) + } + if hasMobile && !hasCipher { + log.Print("hg_app_member already uses the mobile column") + return + } + if !hasCipher { + log.Fatal("mobile_cipher is missing before migration") + } + + aeads := newAEADs(append(splitKeys(os.Getenv("WORKBENCH_MEMBER_MIGRATION_DATA_KEYS")), cfg.Auth.DataEncryptionKey)) + lookupKeys := decodeKeys(append(splitKeys(os.Getenv("WORKBENCH_MEMBER_MIGRATION_LOOKUP_KEYS")), cfg.Auth.LookupHMACKey)) + overrides, err := parseOverrides(os.Getenv("WORKBENCH_MEMBER_MIGRATION_OVERRIDES")) + if err != nil { + log.Fatal(err) + } + if len(aeads) == 0 && len(lookupKeys) == 0 { + log.Fatal("no valid historical phone keys were provided") + } + phones, err := loadPhones(ctx, db, aeads, lookupKeys, overrides) + if err != nil { + log.Fatal(err) + } + if err := validateUnique(phones); err != nil { + log.Fatal(err) + } + if !hasMobile { + if _, err := db.ExecContext(ctx, `ALTER TABLE hg_app_member ADD COLUMN mobile varchar(20) NULL AFTER username`); err != nil { + log.Fatal(err) + } + } + if err := updatePhones(ctx, db, phones); err != nil { + log.Fatal(err) + } + if _, err := db.ExecContext(ctx, `ALTER TABLE hg_app_member + MODIFY COLUMN mobile varchar(20) NOT NULL, + DROP INDEX uk_app_member_mobile_hash, + ADD UNIQUE KEY uk_app_member_mobile (tenant_id,mobile), + DROP COLUMN mobile_cipher, + DROP COLUMN mobile_hash, + DROP COLUMN mobile_mask`); err != nil { + log.Fatal(err) + } + log.Printf("migrated %d member phone numbers", len(phones)) +} + +func columnExists(ctx context.Context, db *sql.DB, column string) (bool, error) { + var count int + err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='hg_app_member' AND COLUMN_NAME=?`, column).Scan(&count) + return count == 1, err +} + +func loadPhones(ctx context.Context, db *sql.DB, aeads []cipher.AEAD, lookupKeys [][]byte, overrides map[int64]string) ([]memberPhone, error) { + rows, err := db.QueryContext(ctx, `SELECT id,tenant_id,mobile_cipher,mobile_hash,mobile_mask FROM hg_app_member ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + phones := make([]memberPhone, 0) + failed := make([]string, 0) + for rows.Next() { + var item memberPhone + if err := rows.Scan(&item.id, &item.tenantID, &item.ciphertext, &item.hash, &item.mask); err != nil { + return nil, err + } + item.phone = recoverPhone(item, aeads, lookupKeys) + if item.phone == "" { + item.phone = overrides[item.id] + } + if !phonePattern.MatchString(item.phone) { + failed = append(failed, fmt.Sprintf("%d", item.id)) + } else if len(item.mask) == 11 && item.phone[:3]+"****"+item.phone[7:] != item.mask { + return nil, fmt.Errorf("member %d override does not match its phone mask", item.id) + } + phones = append(phones, item) + } + if err := rows.Err(); err != nil { + return nil, err + } + if len(failed) > 0 { + return nil, fmt.Errorf("phone recovery failed for member IDs: %s", strings.Join(failed, ",")) + } + return phones, nil +} + +func validateUnique(phones []memberPhone) error { + seen := make(map[string]int64, len(phones)) + for _, item := range phones { + key := fmt.Sprintf("%d:%s", item.tenantID, item.phone) + if otherID, ok := seen[key]; ok { + return fmt.Errorf("members %d and %d have duplicate phone numbers", otherID, item.id) + } + seen[key] = item.id + } + return nil +} + +func updatePhones(ctx context.Context, db *sql.DB, phones []memberPhone) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + for _, item := range phones { + result, err := tx.ExecContext(ctx, `UPDATE hg_app_member SET mobile=? WHERE id=? AND tenant_id=?`, item.phone, item.id, item.tenantID) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected != 1 { + return fmt.Errorf("member %d update affected %d rows", item.id, affected) + } + } + return tx.Commit() +} + +func splitKeys(value string) []string { + return strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == '\n' }) +} + +func parseOverrides(value string) (map[int64]string, error) { + result := make(map[int64]string) + for _, pair := range splitKeys(value) { + var id int64 + var phone string + if _, err := fmt.Sscanf(pair, "%d=%s", &id, &phone); err != nil || id <= 0 || !phonePattern.MatchString(phone) { + return nil, fmt.Errorf("invalid member phone override") + } + result[id] = phone + } + return result, nil +} + +func newAEADs(encodedKeys []string) []cipher.AEAD { + result := make([]cipher.AEAD, 0, len(encodedKeys)) + for _, encodedKey := range encodedKeys { + key, err := decodeKey(encodedKey) + if err != nil || len(key) != 32 { + continue + } + block, err := aes.NewCipher(key) + if err != nil { + continue + } + aead, err := cipher.NewGCM(block) + if err == nil { + result = append(result, aead) + } + } + return result +} + +func decodeKeys(encodedKeys []string) [][]byte { + result := make([][]byte, 0, len(encodedKeys)) + for _, encodedKey := range encodedKeys { + key, err := decodeKey(encodedKey) + if err == nil && len(key) >= 32 { + result = append(result, key) + } + } + return result +} + +func decodeKey(value string) ([]byte, error) { + value = strings.TrimSpace(value) + if decoded, err := base64.StdEncoding.DecodeString(value); err == nil { + return decoded, nil + } + return base64.RawStdEncoding.DecodeString(value) +} + +func recoverPhone(item memberPhone, aeads []cipher.AEAD, lookupKeys [][]byte) string { + for _, aead := range aeads { + for _, encoding := range []*base64.Encoding{base64.RawStdEncoding, base64.RawURLEncoding} { + plain, err := decrypt(aead, encoding, item.ciphertext) + phone := strings.TrimPrefix(plain, "+86") + if err == nil && phonePattern.MatchString(phone) { + return phone + } + } + } + if len(item.mask) != 11 || item.mask[3:7] != "****" { + return "" + } + for middle := 0; middle < 10000; middle++ { + phone := fmt.Sprintf("%s%04d%s", item.mask[:3], middle, item.mask[7:]) + for _, value := range []string{phone, "+86" + phone} { + digest := sha256.Sum256([]byte(value)) + if hmac.Equal([]byte(strings.ToLower(item.hash)), []byte(hex.EncodeToString(digest[:]))) { + return phone + } + } + for _, key := range lookupKeys { + for _, value := range []string{phone, "+86" + phone} { + mac := hmac.New(sha256.New, key) + _, _ = mac.Write([]byte(value)) + if hmac.Equal([]byte(strings.ToLower(item.hash)), []byte(hex.EncodeToString(mac.Sum(nil)))) { + return phone + } + } + } + } + return "" +} + +func decrypt(aead cipher.AEAD, encoding *base64.Encoding, value string) (string, error) { + raw, err := encoding.DecodeString(value) + if err != nil || len(raw) < aead.NonceSize() { + return "", errors.New("invalid ciphertext") + } + plain, err := aead.Open(nil, raw[:aead.NonceSize()], raw[aead.NonceSize():], nil) + return string(plain), err +} diff --git a/internal/portal/service.go b/internal/portal/service.go index 8aff228..3cc6b45 100644 --- a/internal/portal/service.go +++ b/internal/portal/service.go @@ -256,14 +256,14 @@ func parseFeatures(raw json.RawMessage) Features { return Features{} } return Features{ - TaskEnabled: findBool(config, "task_enabled", "taskEnabled", "TASK"), - RechargeEnabled: findBool(config, "recharge_enabled", "rechargeEnabled", "RECHARGE"), - WithdrawalEnabled: findBool(config, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"), - ManualMallEnabled: findBool(config, "manual_mall_enabled", "manualMallEnabled", "SHOP_STATUS"), + TaskEnabled: findBool(config, true, "task_enabled", "taskEnabled", "TASK"), + RechargeEnabled: findBool(config, true, "recharge_enabled", "rechargeEnabled", "RECHARGE"), + WithdrawalEnabled: findBool(config, true, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"), + ManualMallEnabled: findBool(config, true, "manual_mall_enabled", "manualMallEnabled", "SHOP_STATUS"), } } -func findBool(config map[string]any, keys ...string) bool { +func findBool(config map[string]any, fallback bool, keys ...string) bool { for _, key := range keys { if value, ok := config[key]; ok { return boolValue(value) @@ -276,7 +276,7 @@ func findBool(config map[string]any, keys ...string) bool { } } } - return false + return fallback } func boolValue(value any) bool { diff --git a/internal/portal/service_test.go b/internal/portal/service_test.go index 8b8fc1f..d2aa286 100644 --- a/internal/portal/service_test.go +++ b/internal/portal/service_test.go @@ -61,6 +61,13 @@ func TestParseFeaturesSupportsNestedAndLegacyKeys(t *testing.T) { } } +func TestParseFeaturesDefaultsToEnabled(t *testing.T) { + features := parseFeatures(json.RawMessage(`{"siteName":"demo"}`)) + if !features.TaskEnabled || !features.RechargeEnabled || !features.WithdrawalEnabled || !features.ManualMallEnabled { + t.Fatalf("unexpected feature defaults: %+v", features) + } +} + func TestBannersSanitizeTargets(t *testing.T) { service := New(fakeRepository{banners: []store.Banner{ {ID: 1, TargetType: 2, TargetValue: "/receive"}, diff --git a/internal/store/recharges.go b/internal/store/recharges.go new file mode 100644 index 0000000..dc92409 --- /dev/null +++ b/internal/store/recharges.go @@ -0,0 +1,167 @@ +package store + +import ( + "context" + "database/sql" + "encoding/json" + "errors" +) + +type RechargeChannel struct { + ID int64 `db:"id"` + Code string `db:"channel_code"` + Name string `db:"name"` + ChannelType int `db:"channel_type"` + MinAmount int64 `db:"min_amount"` + MaxAmount int64 `db:"max_amount"` + FeeType int `db:"fee_type"` + FeeFixedAmount int64 `db:"fee_fixed_amount"` + FeeRate int `db:"fee_rate"` +} + +type RechargeCreation struct { + TenantID int64 + MemberID int64 + RechargeNo string + ChannelID int64 + RequestAmount int64 + Currency string + VoucherAttachmentIDs []int64 + Remark string +} + +type RechargeOrder struct { + ID int64 `db:"id"` + RechargeNo string `db:"recharge_no"` + ChannelID int64 `db:"channel_id"` + ChannelName string `db:"channel_name"` + RequestAmount int64 `db:"request_amount"` + PaidAmount int64 `db:"paid_amount"` + Currency string `db:"currency"` + VoucherAttachmentIDs json.RawMessage `db:"voucher_attachment_ids"` + Status int `db:"recharge_status"` + Remark string `db:"remark"` + ReviewReasonCode string `db:"review_reason_code"` + ReviewReason string `db:"review_reason"` + ExternalNo string `db:"external_no"` + PaidAt string `db:"paid_at"` + ReviewedAt string `db:"reviewed_at"` + CreditedAt string `db:"credited_at"` + CreatedAt string `db:"created_at"` + UpdatedAt string `db:"updated_at"` +} + +type RechargePage struct { + Items []RechargeOrder + Total int64 +} + +func (s *Store) RechargeChannels(ctx context.Context, tenantID int64) ([]RechargeChannel, error) { + items := make([]RechargeChannel, 0) + err := s.db.SelectContext(ctx, &items, `SELECT id,channel_code,name,channel_type,min_amount,max_amount, + fee_type,fee_fixed_amount,fee_rate FROM hg_payment_channel + WHERE tenant_id=? AND direction IN (1,3) AND status=1 AND deleted_at IS NULL ORDER BY sort,id`, tenantID) + return items, err +} + +func (s *Store) CreateRecharge(ctx context.Context, input RechargeCreation) (*RechargeOrder, error) { + tx, err := s.db.BeginTxx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + var memberID int64 + err = tx.GetContext(ctx, &memberID, `SELECT id FROM hg_app_member WHERE tenant_id=? AND id=? + AND member_status=1 AND deleted_at IS NULL LIMIT 1 FOR UPDATE`, input.TenantID, input.MemberID) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAccountRestricted + } + if err != nil { + return nil, err + } + var channel RechargeChannel + err = tx.GetContext(ctx, &channel, `SELECT id,channel_code,name,channel_type,min_amount,max_amount, + fee_type,fee_fixed_amount,fee_rate FROM hg_payment_channel WHERE tenant_id=? AND id=? + AND direction IN (1,3) AND status=1 AND deleted_at IS NULL LIMIT 1 FOR UPDATE`, input.TenantID, input.ChannelID) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrRechargeChannel + } + if err != nil { + return nil, err + } + if input.RequestAmount < maxInt64(1, channel.MinAmount) || (channel.MaxAmount > 0 && input.RequestAmount > channel.MaxAmount) { + return nil, ErrRechargeAmount + } + for _, attachmentID := range input.VoucherAttachmentIDs { + var count int + if err = tx.GetContext(ctx, &count, `SELECT COUNT(*) FROM hg_sys_attachment WHERE tenant_id=? AND id=? + AND app_id='workbench' AND member_id=? AND status=1`, input.TenantID, attachmentID, input.MemberID); err != nil { + return nil, err + } + if count != 1 { + return nil, ErrRechargeVoucher + } + } + vouchers, err := json.Marshal(input.VoucherAttachmentIDs) + if err != nil { + return nil, err + } + result, err := tx.ExecContext(ctx, `INSERT INTO hg_recharge_order + (tenant_id,recharge_no,member_id,channel_id,request_amount,paid_amount,currency, + voucher_attachment_ids,recharge_type,recharge_status,remark,created_by,updated_by) + VALUES(?,?,?,?,?,0,?,?,1,1,?,?,?)`, input.TenantID, input.RechargeNo, input.MemberID, + input.ChannelID, input.RequestAmount, input.Currency, vouchers, input.Remark, input.MemberID, input.MemberID) + if err != nil { + return nil, err + } + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + if err = tx.Commit(); err != nil { + return nil, err + } + return &RechargeOrder{ID: id, RechargeNo: input.RechargeNo, ChannelID: channel.ID, + ChannelName: channel.Name, RequestAmount: input.RequestAmount, Currency: input.Currency, + VoucherAttachmentIDs: vouchers, Status: 1, Remark: input.Remark}, nil +} + +func (s *Store) RechargeOrders(ctx context.Context, tenantID, memberID int64, status, offset, limit int) (*RechargePage, error) { + where := ` WHERE o.tenant_id=? AND o.member_id=?` + args := []any{tenantID, memberID} + if status > 0 { + where += ` AND o.recharge_status=?` + args = append(args, status) + } + var total int64 + if err := s.db.GetContext(ctx, &total, `SELECT COUNT(*) FROM hg_recharge_order o`+where, args...); err != nil { + return nil, err + } + args = append(args, limit, offset) + items := make([]RechargeOrder, 0) + err := s.db.SelectContext(ctx, &items, rechargeSelectSQL()+where+ + ` ORDER BY o.created_at DESC,o.id DESC LIMIT ? OFFSET ?`, args...) + return &RechargePage{Items: items, Total: total}, err +} + +func (s *Store) RechargeOrder(ctx context.Context, tenantID, memberID, orderID int64) (*RechargeOrder, error) { + var row RechargeOrder + err := s.db.GetContext(ctx, &row, rechargeSelectSQL()+ + ` WHERE o.tenant_id=? AND o.member_id=? AND o.id=? LIMIT 1`, tenantID, memberID, orderID) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrRechargeNotFound + } + return &row, err +} + +func rechargeSelectSQL() string { + return `SELECT o.id,o.recharge_no,o.channel_id,COALESCE(c.name,'') AS channel_name,o.request_amount, + o.paid_amount,o.currency,COALESCE(o.voucher_attachment_ids,JSON_ARRAY()) AS voucher_attachment_ids, + o.recharge_status,o.remark,o.review_reason_code,o.review_reason,o.external_no, + COALESCE(DATE_FORMAT(o.paid_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS paid_at, + COALESCE(DATE_FORMAT(o.reviewed_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS reviewed_at, + COALESCE(DATE_FORMAT(o.credited_at,'%Y-%m-%dT%H:%i:%sZ'),'') AS credited_at, + DATE_FORMAT(o.created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at, + DATE_FORMAT(o.updated_at,'%Y-%m-%dT%H:%i:%sZ') AS updated_at + FROM hg_recharge_order o LEFT JOIN hg_payment_channel c ON c.tenant_id=o.tenant_id AND c.id=o.channel_id` +} diff --git a/web/src/env.d.ts b/web/src/env.d.ts new file mode 100644 index 0000000..578fd66 --- /dev/null +++ b/web/src/env.d.ts @@ -0,0 +1,11 @@ +/// + +import type { MessageApi } from 'naive-ui'; + +declare global { + interface Window { + $message: MessageApi; + } +} + +export {}; diff --git a/web/src/mobile/MobileShell.vue b/web/src/mobile/MobileShell.vue new file mode 100644 index 0000000..bd91d9f --- /dev/null +++ b/web/src/mobile/MobileShell.vue @@ -0,0 +1,73 @@ + + + diff --git a/web/src/mobile/api.ts b/web/src/mobile/api.ts new file mode 100644 index 0000000..a815d22 --- /dev/null +++ b/web/src/mobile/api.ts @@ -0,0 +1,188 @@ +import axios, { AxiosError, AxiosRequestConfig } from 'axios'; + +const TOKEN_KEY = 'olons_web_session'; +const DEVICE_KEY = 'olons_web_device'; + +const reasonMessages: Record = { + AUTH_REQUIRED: '登录已失效,请重新登录', + FORBIDDEN: '当前账户无权执行此操作', + INVALID_CREDENTIALS: '手机号或密码错误', + ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册', + RATE_LIMITED: '操作过于频繁,请稍后再试', + TASK_DISABLED: '领取功能暂未开放', + OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内', + INSUFFICIENT_BALANCE: '可用余额不足', + DAILY_LIMIT_REACHED: '今日领取次数已用完', + PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理', + NO_TASK_AVAILABLE: '当前暂无可领取任务', + ORDER_EXPIRED: '该任务已超时', + ORDER_STATE_INVALID: '当前任务状态不允许此操作', + TRADE_PASSWORD_NOT_SET: '请先设置支付密码', + TRADE_PASSWORD_INVALID: '支付密码不正确', + PAYMENT_ACCOUNT_INVALID: '收款账户未核验或不可用', + WITHDRAWAL_DISABLED: '取出功能暂未开放', + WITHDRAWAL_AMOUNT_INVALID: '取出金额不符合当前规则', + WITHDRAWAL_DAILY_LIMIT_REACHED: '今日取出次数已用完', + WITHDRAWAL_CHANNEL_UNAVAILABLE: '当前没有可用的取出通道', + RECHARGE_AMOUNT_INVALID: '储入金额不符合当前通道规则', + RECHARGE_CHANNEL_UNAVAILABLE: '当前没有可用的储入通道', + RECHARGE_VOUCHER_INVALID: '储入凭证无效', + SERVICE_UNAVAILABLE: '服务暂不可用,请稍后重试', + INTERNAL: '服务异常,请稍后重试', +}; + +export interface ApiEnvelope { + code: number; + data: T; + message?: string; + reason?: string; + request_id?: string; + server_time?: string; +} + +export class MobileApiError extends Error { + reason: string; + status: number; + + constructor(message: string, reason = '', status = 0) { + super(message); + this.reason = reason; + this.status = status; + } +} + +const client = axios.create({ + baseURL: '', + timeout: 15000, + headers: { Accept: 'application/json' }, +}); + +client.interceptors.request.use((config) => { + const token = sessionStorage.getItem(TOKEN_KEY); + if (token) config.headers.Authorization = `Bearer ${token}`; + config.headers['X-Request-ID'] = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`; + config.headers['X-Device-ID'] = getDeviceID(); + return config; +}); + +client.interceptors.response.use( + (response) => response, + (error: AxiosError>) => { + const status = error.response?.status || 0; + const payload = error.response?.data; + if (status === 401) { + clearSession(); + const currentPath = window.location.hash.replace(/^#/, '') || '/app/home'; + if (!currentPath.startsWith('/app/auth')) { + window.location.hash = `/app/auth?redirect=${encodeURIComponent(currentPath)}`; + } + } + const reason = payload?.reason || ''; + return Promise.reject( + new MobileApiError( + reasonMessages[reason] || + payload?.message || + (status ? '请求失败,请稍后重试' : '网络连接失败'), + reason, + status + ) + ); + } +); + +export async function request(config: AxiosRequestConfig): Promise { + const method = String(config.method || 'GET').toUpperCase(); + const attempts = method === 'GET' ? 3 : 1; + let lastError: unknown; + for (let attempt = 0; attempt < attempts; attempt += 1) { + try { + const response = await client.request>(config); + if (response.data.code !== 0) { + const reason = response.data.reason || ''; + throw new MobileApiError( + reasonMessages[reason] || response.data.message || '请求失败', + reason, + response.status + ); + } + return response.data.data; + } catch (error) { + lastError = error; + if (!shouldRetry(error, attempt, attempts)) throw error; + await new Promise((resolve) => window.setTimeout(resolve, attempt === 0 ? 250 : 600)); + } + } + throw lastError; +} + +function shouldRetry(error: unknown, attempt: number, attempts: number) { + if (attempt >= attempts - 1) return false; + if (axios.isCancel(error)) return false; + return error instanceof MobileApiError && (error.status === 0 || error.status === 503); +} + +function getDeviceID() { + let value = localStorage.getItem(DEVICE_KEY); + if (!value) { + value = crypto.randomUUID?.() || `web-${Date.now()}-${Math.random().toString(36).slice(2)}`; + localStorage.setItem(DEVICE_KEY, value); + } + return value; +} + +export function saveSession(token: string) { + sessionStorage.setItem(TOKEN_KEY, token); +} + +export function clearSession() { + sessionStorage.removeItem(TOKEN_KEY); +} + +export function hasSession() { + return Boolean(sessionStorage.getItem(TOKEN_KEY)); +} + +export function idempotencyKey() { + return crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`; +} + +export function money(value?: number, currency = 'CNY') { + const amount = Number.isFinite(value) ? Number(value) / 100 : 0; + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency, + minimumFractionDigits: 2, + }).format(amount); +} + +export function dateTime(value?: string) { + if (!value) return '--'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return '--'; + return new Intl.DateTimeFormat('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }) + .format(date) + .replaceAll('/', '-'); +} + +export function safeImage(url?: string) { + if (!url) return ''; + try { + const parsed = new URL(url, window.location.origin); + return parsed.protocol === 'https:' || parsed.origin === window.location.origin + ? parsed.href + : ''; + } catch { + return ''; + } +} + +export function errorText(error: unknown) { + return error instanceof Error ? error.message : '操作失败,请稍后重试'; +} diff --git a/web/src/mobile/assets.ts b/web/src/mobile/assets.ts new file mode 100644 index 0000000..c7d79ff --- /dev/null +++ b/web/src/mobile/assets.ts @@ -0,0 +1,153 @@ +import homeHero from './assets/home-hero.png'; +import homeBalance from './assets/home-balance.png'; +import shortcutClaim from './assets/shortcut-claim.png'; +import shortcutRecharge from './assets/shortcut-recharge.png'; +import shortcutWithdraw from './assets/shortcut-withdraw.png'; +import shortcutMall from './assets/shortcut-mall.png'; +import navHome from './assets/nav-home.png'; +import navHomeActive from './assets/nav-home-active.png'; +import navOrders from './assets/nav-orders.png'; +import navOrdersActive from './assets/nav-orders-active.png'; +import navClaimActive from './assets/nav-claim-active.png'; +import navSupport from './assets/nav-support.png'; +import navSupportActive from './assets/nav-support-active.png'; +import navProfile from './assets/nav-profile.png'; +import navProfileActive from './assets/nav-profile-active.png'; +import profileBackground from './assets/profile-bg.png'; +import profileWallet from './assets/profile-wallet.png'; +import menuProfile from './assets/menu-profile.png'; +import menuRecharge from './assets/menu-recharge.png'; +import menuWithdraw from './assets/menu-withdraw.png'; +import menuLedger from './assets/menu-ledger.png'; +import menuNotices from './assets/menu-notices.png'; +import menuLogout from './assets/menu-logout.png'; +import copy from './assets/copy.png'; +import supportHero from './assets/support-hero.png'; +import back from './assets/back.png'; +import claimOrbit from './assets/claim-orbit.png'; +import claimGift from './assets/claim-gift.png'; +import claimButton from './assets/claim-button.png'; +import processSimple from './assets/process-simple.png'; +import processMaterial from './assets/process-material.png'; +import processTime from './assets/process-time.png'; +import processReturn from './assets/process-return.png'; +import chatVoice from './assets/chat-voice.png'; +import chatMore from './assets/chat-more.png'; +import companyPlatform from './assets/company-platform.png'; +import companyRules from './assets/company-rules.png'; +import companyQualification from './assets/company-qualification.png'; +import companyAgency from './assets/company-agency.png'; +import avatarEdit from './assets/avatar-edit.png'; +import rechargeRecord from './assets/recharge-record.png'; +import withdrawalRecord from './assets/withdrawal-record.png'; +import loadingOrbit from './assets/loading-orbit.png'; +import agencyHero from './assets/agency-hero.png'; +import agencyBackground from './assets/agency-bg.png'; +import agencyPersonPrimary from './assets/agency-person-primary.png'; +import agencyPersonSecondary from './assets/agency-person-secondary.png'; +import agencyPersonTertiary from './assets/agency-person-tertiary.png'; +import rulesBackground from './assets/rules-bg.png'; +import rulesBanner from './assets/rules-banner.png'; +import levelSilver from './assets/level-silver.png'; +import levelGold from './assets/level-gold.png'; +import levelPlatinum from './assets/level-platinum.png'; +import levelDiamond from './assets/level-diamond.png'; +import levelStar from './assets/level-star.png'; +import levelCrown from './assets/level-crown.png'; +import claimMachine from './assets/claim-machine.png'; +import claimAutoButton from './assets/claim-auto-button.png'; +import claimTotal from './assets/claim-total.png'; +import claimYesterday from './assets/claim-yesterday.png'; +import claimToday from './assets/claim-today.png'; +import claimCount from './assets/claim-count.png'; +import claimSpinner from './assets/claim-spinner.png'; +import claimMatchingGift from './assets/claim-matching-gift.png'; +import claimSuccess from './assets/claim-success.png'; +import claimFailed from './assets/claim-failed.png'; +import claimCancelled from './assets/claim-cancelled.png'; +import mallBanner from './assets/mall-banner.png'; +import mallBackground from './assets/mall-bg.png'; +import rankFirst from './assets/rank-first.png'; +import rankSecond from './assets/rank-second.png'; +import rankThird from './assets/rank-third.png'; +import rankBackground from './assets/rank-bg.png'; +import qualificationOperation from './assets/qualification-operation.png'; +import qualificationDevelopment from './assets/qualification-development.png'; + +export const mobileAssets = { + homeHero, + homeBalance, + shortcutClaim, + shortcutRecharge, + shortcutWithdraw, + shortcutMall, + navHome, + navHomeActive, + navOrders, + navOrdersActive, + navClaimActive, + navSupport, + navSupportActive, + navProfile, + navProfileActive, + profileBackground, + profileWallet, + menuProfile, + menuRecharge, + menuWithdraw, + menuLedger, + menuNotices, + menuLogout, + copy, + supportHero, + back, + claimOrbit, + claimGift, + claimButton, + processSimple, + processMaterial, + processTime, + processReturn, + chatVoice, + chatMore, + companyPlatform, + companyRules, + companyQualification, + companyAgency, + avatarEdit, + rechargeRecord, + withdrawalRecord, + loadingOrbit, + agencyHero, + agencyBackground, + agencyPersonPrimary, + agencyPersonSecondary, + agencyPersonTertiary, + rulesBackground, + rulesBanner, + levelSilver, + levelGold, + levelPlatinum, + levelDiamond, + levelStar, + levelCrown, + claimMachine, + claimAutoButton, + claimTotal, + claimYesterday, + claimToday, + claimCount, + claimSpinner, + claimMatchingGift, + claimSuccess, + claimFailed, + claimCancelled, + mallBanner, + mallBackground, + rankFirst, + rankSecond, + rankThird, + rankBackground, + qualificationOperation, + qualificationDevelopment, +}; diff --git a/web/src/mobile/assets/agency-bg.png b/web/src/mobile/assets/agency-bg.png new file mode 100644 index 0000000..72b6a66 Binary files /dev/null and b/web/src/mobile/assets/agency-bg.png differ diff --git a/web/src/mobile/assets/agency-hero.png b/web/src/mobile/assets/agency-hero.png new file mode 100644 index 0000000..4f600e9 Binary files /dev/null and b/web/src/mobile/assets/agency-hero.png differ diff --git a/web/src/mobile/assets/agency-person-primary.png b/web/src/mobile/assets/agency-person-primary.png new file mode 100644 index 0000000..4283fbc Binary files /dev/null and b/web/src/mobile/assets/agency-person-primary.png differ diff --git a/web/src/mobile/assets/agency-person-secondary.png b/web/src/mobile/assets/agency-person-secondary.png new file mode 100644 index 0000000..b85124f Binary files /dev/null and b/web/src/mobile/assets/agency-person-secondary.png differ diff --git a/web/src/mobile/assets/agency-person-tertiary.png b/web/src/mobile/assets/agency-person-tertiary.png new file mode 100644 index 0000000..085c44f Binary files /dev/null and b/web/src/mobile/assets/agency-person-tertiary.png differ diff --git a/web/src/mobile/assets/avatar-edit.png b/web/src/mobile/assets/avatar-edit.png new file mode 100644 index 0000000..573c375 Binary files /dev/null and b/web/src/mobile/assets/avatar-edit.png differ diff --git a/web/src/mobile/assets/back.png b/web/src/mobile/assets/back.png new file mode 100644 index 0000000..c93058f Binary files /dev/null and b/web/src/mobile/assets/back.png differ diff --git a/web/src/mobile/assets/chat-more.png b/web/src/mobile/assets/chat-more.png new file mode 100644 index 0000000..fd981ec Binary files /dev/null and b/web/src/mobile/assets/chat-more.png differ diff --git a/web/src/mobile/assets/chat-voice.png b/web/src/mobile/assets/chat-voice.png new file mode 100644 index 0000000..0ebcefc Binary files /dev/null and b/web/src/mobile/assets/chat-voice.png differ diff --git a/web/src/mobile/assets/claim-auto-button.png b/web/src/mobile/assets/claim-auto-button.png new file mode 100644 index 0000000..9e6a584 Binary files /dev/null and b/web/src/mobile/assets/claim-auto-button.png differ diff --git a/web/src/mobile/assets/claim-button.png b/web/src/mobile/assets/claim-button.png new file mode 100644 index 0000000..9e6a584 Binary files /dev/null and b/web/src/mobile/assets/claim-button.png differ diff --git a/web/src/mobile/assets/claim-cancelled.png b/web/src/mobile/assets/claim-cancelled.png new file mode 100644 index 0000000..529decd Binary files /dev/null and b/web/src/mobile/assets/claim-cancelled.png differ diff --git a/web/src/mobile/assets/claim-count.png b/web/src/mobile/assets/claim-count.png new file mode 100644 index 0000000..41f80c3 Binary files /dev/null and b/web/src/mobile/assets/claim-count.png differ diff --git a/web/src/mobile/assets/claim-failed.png b/web/src/mobile/assets/claim-failed.png new file mode 100644 index 0000000..9b3a710 Binary files /dev/null and b/web/src/mobile/assets/claim-failed.png differ diff --git a/web/src/mobile/assets/claim-gift.png b/web/src/mobile/assets/claim-gift.png new file mode 100644 index 0000000..1633edd Binary files /dev/null and b/web/src/mobile/assets/claim-gift.png differ diff --git a/web/src/mobile/assets/claim-machine.png b/web/src/mobile/assets/claim-machine.png new file mode 100644 index 0000000..2fdb070 Binary files /dev/null and b/web/src/mobile/assets/claim-machine.png differ diff --git a/web/src/mobile/assets/claim-matching-gift.png b/web/src/mobile/assets/claim-matching-gift.png new file mode 100644 index 0000000..1633edd Binary files /dev/null and b/web/src/mobile/assets/claim-matching-gift.png differ diff --git a/web/src/mobile/assets/claim-orbit.png b/web/src/mobile/assets/claim-orbit.png new file mode 100644 index 0000000..f508b63 Binary files /dev/null and b/web/src/mobile/assets/claim-orbit.png differ diff --git a/web/src/mobile/assets/claim-spinner.png b/web/src/mobile/assets/claim-spinner.png new file mode 100644 index 0000000..f508b63 Binary files /dev/null and b/web/src/mobile/assets/claim-spinner.png differ diff --git a/web/src/mobile/assets/claim-success.png b/web/src/mobile/assets/claim-success.png new file mode 100644 index 0000000..1ef3d06 Binary files /dev/null and b/web/src/mobile/assets/claim-success.png differ diff --git a/web/src/mobile/assets/claim-today.png b/web/src/mobile/assets/claim-today.png new file mode 100644 index 0000000..6d01a52 Binary files /dev/null and b/web/src/mobile/assets/claim-today.png differ diff --git a/web/src/mobile/assets/claim-total.png b/web/src/mobile/assets/claim-total.png new file mode 100644 index 0000000..05acef3 Binary files /dev/null and b/web/src/mobile/assets/claim-total.png differ diff --git a/web/src/mobile/assets/claim-yesterday.png b/web/src/mobile/assets/claim-yesterday.png new file mode 100644 index 0000000..5fc7e4d Binary files /dev/null and b/web/src/mobile/assets/claim-yesterday.png differ diff --git a/web/src/mobile/assets/company-agency.png b/web/src/mobile/assets/company-agency.png new file mode 100644 index 0000000..511bf0d Binary files /dev/null and b/web/src/mobile/assets/company-agency.png differ diff --git a/web/src/mobile/assets/company-platform.png b/web/src/mobile/assets/company-platform.png new file mode 100644 index 0000000..ecbf0c9 Binary files /dev/null and b/web/src/mobile/assets/company-platform.png differ diff --git a/web/src/mobile/assets/company-qualification.png b/web/src/mobile/assets/company-qualification.png new file mode 100644 index 0000000..474d9e1 Binary files /dev/null and b/web/src/mobile/assets/company-qualification.png differ diff --git a/web/src/mobile/assets/company-rules.png b/web/src/mobile/assets/company-rules.png new file mode 100644 index 0000000..56d2a98 Binary files /dev/null and b/web/src/mobile/assets/company-rules.png differ diff --git a/web/src/mobile/assets/copy.png b/web/src/mobile/assets/copy.png new file mode 100644 index 0000000..e36ef45 Binary files /dev/null and b/web/src/mobile/assets/copy.png differ diff --git a/web/src/mobile/assets/home-balance.png b/web/src/mobile/assets/home-balance.png new file mode 100644 index 0000000..d685d58 Binary files /dev/null and b/web/src/mobile/assets/home-balance.png differ diff --git a/web/src/mobile/assets/home-hero.png b/web/src/mobile/assets/home-hero.png new file mode 100644 index 0000000..52bb672 Binary files /dev/null and b/web/src/mobile/assets/home-hero.png differ diff --git a/web/src/mobile/assets/level-crown.png b/web/src/mobile/assets/level-crown.png new file mode 100644 index 0000000..ce177bf Binary files /dev/null and b/web/src/mobile/assets/level-crown.png differ diff --git a/web/src/mobile/assets/level-diamond.png b/web/src/mobile/assets/level-diamond.png new file mode 100644 index 0000000..99709c0 Binary files /dev/null and b/web/src/mobile/assets/level-diamond.png differ diff --git a/web/src/mobile/assets/level-gold.png b/web/src/mobile/assets/level-gold.png new file mode 100644 index 0000000..8b90b61 Binary files /dev/null and b/web/src/mobile/assets/level-gold.png differ diff --git a/web/src/mobile/assets/level-platinum.png b/web/src/mobile/assets/level-platinum.png new file mode 100644 index 0000000..8279c73 Binary files /dev/null and b/web/src/mobile/assets/level-platinum.png differ diff --git a/web/src/mobile/assets/level-silver.png b/web/src/mobile/assets/level-silver.png new file mode 100644 index 0000000..ba84150 Binary files /dev/null and b/web/src/mobile/assets/level-silver.png differ diff --git a/web/src/mobile/assets/level-star.png b/web/src/mobile/assets/level-star.png new file mode 100644 index 0000000..c8588eb Binary files /dev/null and b/web/src/mobile/assets/level-star.png differ diff --git a/web/src/mobile/assets/loading-orbit.png b/web/src/mobile/assets/loading-orbit.png new file mode 100644 index 0000000..f508b63 Binary files /dev/null and b/web/src/mobile/assets/loading-orbit.png differ diff --git a/web/src/mobile/assets/mall-banner.png b/web/src/mobile/assets/mall-banner.png new file mode 100644 index 0000000..1a6de22 Binary files /dev/null and b/web/src/mobile/assets/mall-banner.png differ diff --git a/web/src/mobile/assets/mall-bg.png b/web/src/mobile/assets/mall-bg.png new file mode 100644 index 0000000..0ac1258 Binary files /dev/null and b/web/src/mobile/assets/mall-bg.png differ diff --git a/web/src/mobile/assets/menu-ledger.png b/web/src/mobile/assets/menu-ledger.png new file mode 100644 index 0000000..15b60f6 Binary files /dev/null and b/web/src/mobile/assets/menu-ledger.png differ diff --git a/web/src/mobile/assets/menu-logout.png b/web/src/mobile/assets/menu-logout.png new file mode 100644 index 0000000..82d6e28 Binary files /dev/null and b/web/src/mobile/assets/menu-logout.png differ diff --git a/web/src/mobile/assets/menu-notices.png b/web/src/mobile/assets/menu-notices.png new file mode 100644 index 0000000..b7e7c7b Binary files /dev/null and b/web/src/mobile/assets/menu-notices.png differ diff --git a/web/src/mobile/assets/menu-profile.png b/web/src/mobile/assets/menu-profile.png new file mode 100644 index 0000000..d76c57a Binary files /dev/null and b/web/src/mobile/assets/menu-profile.png differ diff --git a/web/src/mobile/assets/menu-recharge.png b/web/src/mobile/assets/menu-recharge.png new file mode 100644 index 0000000..f1be9cd Binary files /dev/null and b/web/src/mobile/assets/menu-recharge.png differ diff --git a/web/src/mobile/assets/menu-withdraw.png b/web/src/mobile/assets/menu-withdraw.png new file mode 100644 index 0000000..ff26835 Binary files /dev/null and b/web/src/mobile/assets/menu-withdraw.png differ diff --git a/web/src/mobile/assets/nav-claim-active.png b/web/src/mobile/assets/nav-claim-active.png new file mode 100644 index 0000000..36ef849 Binary files /dev/null and b/web/src/mobile/assets/nav-claim-active.png differ diff --git a/web/src/mobile/assets/nav-home-active.png b/web/src/mobile/assets/nav-home-active.png new file mode 100644 index 0000000..26514bf Binary files /dev/null and b/web/src/mobile/assets/nav-home-active.png differ diff --git a/web/src/mobile/assets/nav-home.png b/web/src/mobile/assets/nav-home.png new file mode 100644 index 0000000..a6f59ee Binary files /dev/null and b/web/src/mobile/assets/nav-home.png differ diff --git a/web/src/mobile/assets/nav-orders-active.png b/web/src/mobile/assets/nav-orders-active.png new file mode 100644 index 0000000..2ade094 Binary files /dev/null and b/web/src/mobile/assets/nav-orders-active.png differ diff --git a/web/src/mobile/assets/nav-orders.png b/web/src/mobile/assets/nav-orders.png new file mode 100644 index 0000000..f822d89 Binary files /dev/null and b/web/src/mobile/assets/nav-orders.png differ diff --git a/web/src/mobile/assets/nav-profile-active.png b/web/src/mobile/assets/nav-profile-active.png new file mode 100644 index 0000000..5a43359 Binary files /dev/null and b/web/src/mobile/assets/nav-profile-active.png differ diff --git a/web/src/mobile/assets/nav-profile.png b/web/src/mobile/assets/nav-profile.png new file mode 100644 index 0000000..da49138 Binary files /dev/null and b/web/src/mobile/assets/nav-profile.png differ diff --git a/web/src/mobile/assets/nav-support-active.png b/web/src/mobile/assets/nav-support-active.png new file mode 100644 index 0000000..8805b0c Binary files /dev/null and b/web/src/mobile/assets/nav-support-active.png differ diff --git a/web/src/mobile/assets/nav-support.png b/web/src/mobile/assets/nav-support.png new file mode 100644 index 0000000..81fc3e8 Binary files /dev/null and b/web/src/mobile/assets/nav-support.png differ diff --git a/web/src/mobile/assets/process-material.png b/web/src/mobile/assets/process-material.png new file mode 100644 index 0000000..9db89a4 Binary files /dev/null and b/web/src/mobile/assets/process-material.png differ diff --git a/web/src/mobile/assets/process-return.png b/web/src/mobile/assets/process-return.png new file mode 100644 index 0000000..6928819 Binary files /dev/null and b/web/src/mobile/assets/process-return.png differ diff --git a/web/src/mobile/assets/process-simple.png b/web/src/mobile/assets/process-simple.png new file mode 100644 index 0000000..495a5ae Binary files /dev/null and b/web/src/mobile/assets/process-simple.png differ diff --git a/web/src/mobile/assets/process-time.png b/web/src/mobile/assets/process-time.png new file mode 100644 index 0000000..6925f59 Binary files /dev/null and b/web/src/mobile/assets/process-time.png differ diff --git a/web/src/mobile/assets/profile-bg.png b/web/src/mobile/assets/profile-bg.png new file mode 100644 index 0000000..036ddaf Binary files /dev/null and b/web/src/mobile/assets/profile-bg.png differ diff --git a/web/src/mobile/assets/profile-wallet.png b/web/src/mobile/assets/profile-wallet.png new file mode 100644 index 0000000..ba9f8b0 Binary files /dev/null and b/web/src/mobile/assets/profile-wallet.png differ diff --git a/web/src/mobile/assets/qualification-development.png b/web/src/mobile/assets/qualification-development.png new file mode 100644 index 0000000..3950502 Binary files /dev/null and b/web/src/mobile/assets/qualification-development.png differ diff --git a/web/src/mobile/assets/qualification-operation.png b/web/src/mobile/assets/qualification-operation.png new file mode 100644 index 0000000..a19e18f Binary files /dev/null and b/web/src/mobile/assets/qualification-operation.png differ diff --git a/web/src/mobile/assets/rank-bg.png b/web/src/mobile/assets/rank-bg.png new file mode 100644 index 0000000..72b6a66 Binary files /dev/null and b/web/src/mobile/assets/rank-bg.png differ diff --git a/web/src/mobile/assets/rank-first.png b/web/src/mobile/assets/rank-first.png new file mode 100644 index 0000000..05a4bc0 Binary files /dev/null and b/web/src/mobile/assets/rank-first.png differ diff --git a/web/src/mobile/assets/rank-second.png b/web/src/mobile/assets/rank-second.png new file mode 100644 index 0000000..904f1d7 Binary files /dev/null and b/web/src/mobile/assets/rank-second.png differ diff --git a/web/src/mobile/assets/rank-third.png b/web/src/mobile/assets/rank-third.png new file mode 100644 index 0000000..604af5b Binary files /dev/null and b/web/src/mobile/assets/rank-third.png differ diff --git a/web/src/mobile/assets/recharge-record.png b/web/src/mobile/assets/recharge-record.png new file mode 100644 index 0000000..05acef3 Binary files /dev/null and b/web/src/mobile/assets/recharge-record.png differ diff --git a/web/src/mobile/assets/rules-banner.png b/web/src/mobile/assets/rules-banner.png new file mode 100644 index 0000000..f16ed3a Binary files /dev/null and b/web/src/mobile/assets/rules-banner.png differ diff --git a/web/src/mobile/assets/rules-bg.png b/web/src/mobile/assets/rules-bg.png new file mode 100644 index 0000000..036ddaf Binary files /dev/null and b/web/src/mobile/assets/rules-bg.png differ diff --git a/web/src/mobile/assets/shortcut-claim.png b/web/src/mobile/assets/shortcut-claim.png new file mode 100644 index 0000000..1633edd Binary files /dev/null and b/web/src/mobile/assets/shortcut-claim.png differ diff --git a/web/src/mobile/assets/shortcut-mall.png b/web/src/mobile/assets/shortcut-mall.png new file mode 100644 index 0000000..c2c574b Binary files /dev/null and b/web/src/mobile/assets/shortcut-mall.png differ diff --git a/web/src/mobile/assets/shortcut-recharge.png b/web/src/mobile/assets/shortcut-recharge.png new file mode 100644 index 0000000..55148f7 Binary files /dev/null and b/web/src/mobile/assets/shortcut-recharge.png differ diff --git a/web/src/mobile/assets/shortcut-withdraw.png b/web/src/mobile/assets/shortcut-withdraw.png new file mode 100644 index 0000000..0b9ce9c Binary files /dev/null and b/web/src/mobile/assets/shortcut-withdraw.png differ diff --git a/web/src/mobile/assets/support-hero.png b/web/src/mobile/assets/support-hero.png new file mode 100644 index 0000000..5717901 Binary files /dev/null and b/web/src/mobile/assets/support-hero.png differ diff --git a/web/src/mobile/assets/withdrawal-record.png b/web/src/mobile/assets/withdrawal-record.png new file mode 100644 index 0000000..9db89a4 Binary files /dev/null and b/web/src/mobile/assets/withdrawal-record.png differ diff --git a/web/src/mobile/components/AsyncState.vue b/web/src/mobile/components/AsyncState.vue new file mode 100644 index 0000000..835892c --- /dev/null +++ b/web/src/mobile/components/AsyncState.vue @@ -0,0 +1,27 @@ + + + diff --git a/web/src/mobile/components/PageHeader.vue b/web/src/mobile/components/PageHeader.vue new file mode 100644 index 0000000..2a7d037 --- /dev/null +++ b/web/src/mobile/components/PageHeader.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/mobile/styles.css b/web/src/mobile/styles.css new file mode 100644 index 0000000..17ca571 --- /dev/null +++ b/web/src/mobile/styles.css @@ -0,0 +1,3263 @@ +:root { + --m-accent: #d58d2b; + --m-accent-strong: #f45b20; + --m-ink: #2f3033; + --m-muted: #8b929d; + --m-line: #ececec; + --m-bg: #f7f7f7; + --m-gold-soft: #fff2d8; + --m-success: #3f9d68; + --m-danger: #d94b4b; +} + +html, +body, +#app { + min-height: 100%; + margin: 0; +} + +body { + background: var(--m-bg); +} + +.m-app, +.m-app * { + box-sizing: border-box; +} +.m-app { + width: 100%; + min-height: 100dvh; + max-width: 750px; + margin: 0 auto; + color: var(--m-ink); + background: var(--m-bg); + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-size: 16px; + letter-spacing: 0; + position: relative; +} +.m-app button, +.m-app input, +.m-app textarea { + font: inherit; + letter-spacing: 0; +} +.m-app button { + cursor: pointer; +} +.m-app--with-nav { + padding-bottom: calc(76px + env(safe-area-inset-bottom)); +} +.m-page { + min-height: 100dvh; + padding: 0 20px 28px; +} +.m-page--white { + background: #fff; +} +.m-page-header { + height: 82px; + padding-top: env(safe-area-inset-top); + display: grid; + grid-template-columns: 44px 1fr 44px; + align-items: center; + background: transparent; + position: relative; + z-index: 2; +} +.m-page-header h1 { + margin: 0; + text-align: center; + font-size: 22px; + line-height: 1.2; + font-weight: 600; +} +.m-header-space { + width: 44px; +} +.m-icon-btn { + width: 44px; + height: 44px; + border: 0; + background: transparent; + padding: 8px; + display: grid; + place-items: center; + color: var(--m-ink); +} +.m-icon-btn img { + width: 9px; + height: 18px; + object-fit: contain; + transform: rotate(180deg); +} +.m-bottom-nav { + position: fixed; + left: 50%; + bottom: 0; + transform: translateX(-50%); + z-index: 20; + width: min(100%, 750px); + height: calc(76px + env(safe-area-inset-bottom)); + padding: 8px 18px env(safe-area-inset-bottom); + background: rgba(255, 255, 255, 0.98); + border-top: 1px solid #eee; + display: grid; + grid-template-columns: repeat(5, 1fr); +} +.m-bottom-nav a { + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + color: #89919c; + text-decoration: none; + font-size: 13px; +} +.m-bottom-nav img { + width: 23px; + height: 23px; + object-fit: contain; +} +.m-bottom-nav a:not(.router-link-active) img { + filter: grayscale(1); + opacity: 0.9; +} +.m-bottom-nav a.router-link-active { + color: #87500b; +} +.m-card { + background: #fff; + border-radius: 8px; +} +.m-primary-btn, +.m-outline-btn { + width: 100%; + min-height: 48px; + border-radius: 24px; + border: 0; + font-weight: 600; + transition: + opacity 0.2s, + transform 0.2s; +} +.m-primary-btn { + color: #fff; + background: linear-gradient(90deg, #ff9c2a, #f2571c); +} +.m-outline-btn { + color: var(--m-accent-strong); + border: 1px solid var(--m-accent-strong); + background: #fff; +} +.m-primary-btn:disabled { + opacity: 0.55; + cursor: not-allowed; +} +.m-primary-btn:active, +.m-outline-btn:active { + transform: scale(0.99); +} +.m-link-btn { + min-height: 44px; + border: 0; + background: transparent; + color: var(--m-accent); + padding: 0 16px; +} +.m-state { + min-height: 240px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: var(--m-muted); + gap: 10px; + text-align: center; +} +.m-state .n-icon { + font-size: 42px; +} +.m-state p { + margin: 0; +} +.m-state--error .n-icon { + color: #c1a477; +} +.m-form { + display: grid; + gap: 12px; +} +.m-input-row { + min-height: 56px; + border-radius: 28px; + background: #f3f3f3; + display: flex; + align-items: center; + gap: 14px; + padding: 0 20px; +} +.m-input-row .n-icon { + font-size: 23px; + color: #858a91; + flex: 0 0 auto; +} +.m-input-row input { + width: 100%; + min-width: 0; + border: 0; + outline: none; + background: transparent; + color: var(--m-ink); +} +.m-input-row input::placeholder { + color: #b9bbc0; +} +.m-form-error { + color: var(--m-danger); + font-size: 14px; + margin: 0 6px; +} + +.m-auth { + min-height: 100dvh; + background: #fff9ed; + padding: 0 30px 34px; + display: flex; + flex-direction: column; +} +.m-auth-hero { + height: 310px; + padding: 84px 18px 0; + position: relative; + overflow: hidden; +} +.m-auth-hero::before { + content: ''; + position: absolute; + width: 310px; + height: 310px; + right: -105px; + top: -30px; + border-radius: 50%; + background: #ffedc9; +} +.m-auth-hero p, +.m-auth-hero h1 { + margin: 0; + position: relative; + z-index: 1; + font-weight: 700; +} +.m-auth-hero p { + font-size: 28px; + margin-top: 66px; +} +.m-auth-hero h1 { + font-size: 30px; + margin-top: 8px; +} +.m-auth-mark { + position: absolute; + right: 32px; + top: 90px; + width: 104px; + height: 104px; + z-index: 1; +} +.m-auth-mark span { + position: absolute; + display: block; + border-radius: 50%; + background: linear-gradient(145deg, #ff9b30, #f05a19); + box-shadow: + inset -8px -10px 16px rgba(185, 59, 5, 0.2), + inset 8px 8px 13px rgba(255, 255, 255, 0.65); +} +.m-auth-mark span:nth-child(1) { + width: 58px; + height: 58px; + left: 25px; + top: 0; +} +.m-auth-mark span:nth-child(2) { + width: 72px; + height: 72px; + left: 20px; + top: 47px; +} +.m-auth-mark span:nth-child(3) { + width: 18px; + height: 18px; + left: 0; + top: 49px; +} +.m-auth-panel { + background: #fff; + border-radius: 28px 28px 8px 8px; + padding: 0 30px 28px; + position: relative; +} +.m-auth-tabs { + height: 74px; + margin: 0 -30px 22px; + display: grid; + grid-template-columns: 1fr 1fr; +} +.m-auth-tabs button { + border: 0; + background: #fff7e8; + color: #a0a1a5; + font-size: 19px; + position: relative; +} +.m-auth-tabs button:first-child { + border-radius: 28px 0 28px 0; +} +.m-auth-tabs button:last-child { + border-radius: 0 28px 0 28px; +} +.m-auth-tabs button.active { + background: #fff; + color: var(--m-accent-strong); +} +.m-auth-tabs button.active::after { + content: ''; + position: absolute; + left: 50%; + bottom: 9px; + width: 20px; + height: 3px; + border-radius: 2px; + transform: translateX(-50%); + background: var(--m-accent-strong); +} +.m-auth .m-form { + gap: 12px; +} +.m-auth .m-primary-btn { + margin-top: 8px; +} +.m-forgot-link { + justify-self: end; + min-height: 32px; + color: #9b7451; + text-decoration: none; + font-size: 14px; +} +.m-auth-agreement { + margin-top: auto; + padding-top: 28px; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 4px; + font-size: 13px; + line-height: 1.7; +} +.m-auth-agreement input { + width: 18px; + height: 18px; + accent-color: var(--m-accent-strong); + margin-right: 5px; +} +.m-auth-agreement a { + color: var(--m-accent-strong); + text-decoration: none; +} + +.m-home { + min-height: 100dvh; + background: var(--m-bg); +} +.m-home-hero { + min-height: 305px; + padding: calc(34px + env(safe-area-inset-top)) 30px 26px; + color: #fff; + background-color: #1b1711; + background-size: cover; + background-position: center; +} +.m-search { + height: 48px; + border-radius: 24px; + background: #fff; + color: #aa8a78; + display: flex; + align-items: center; + gap: 10px; + padding: 0 18px; +} +.m-search .n-icon { + font-size: 23px; +} +.m-search input { + width: 100%; + border: 0; + outline: 0; + color: inherit; + background: transparent; +} +.m-greeting { + margin: 28px 8px 22px; +} +.m-greeting h1 { + margin: 0; + font-size: 26px; +} +.m-greeting p { + margin: 5px 0 0; + font-size: 17px; + color: #eadbc4; +} +.m-balance-card { + min-height: 124px; + padding: 22px 22px; + border-radius: 8px; + background-color: #1b1b1b; + background-image: url('./assets/home-balance.png'); + background-size: 100% 100%; + background-repeat: no-repeat; + display: flex; + align-items: center; + justify-content: space-between; +} +.m-balance-card span, +.m-balance-card strong { + display: block; +} +.m-balance-card span { + color: #eee4d2; +} +.m-balance-card strong { + font-size: 32px; + line-height: 1.25; + margin-top: 10px; +} +.m-coin { + width: 78px; + height: 78px; + border-radius: 50%; + border: 6px double #ffe087; + display: none; + place-items: center; + color: #7c4b0c; + background: #efb536; + font-size: 34px; + font-weight: 700; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.3); + transform: rotate(-8deg); +} +.m-home-body { + padding: 14px 20px 25px; +} +.m-shortcuts { + display: grid; + grid-template-columns: repeat(4, 1fr); + padding: 14px 5px; +} +.m-shortcuts button { + min-width: 0; + min-height: 80px; + border: 0; + background: transparent; + display: flex; + align-items: center; + flex-direction: column; + gap: 7px; + color: var(--m-ink); +} +.m-shortcuts button:disabled { + opacity: 0.35; +} +.m-shortcuts button > span { + width: 42px; + height: 42px; + border-radius: 50%; + display: grid; + place-items: center; + background: transparent; +} +.m-shortcuts img { + width: 38px; + height: 38px; + object-fit: contain; +} +.m-shortcuts b { + font-weight: 500; +} +.m-notice { + margin-top: 12px; + padding: 12px 16px; + display: flex; + align-items: center; + gap: 9px; + color: #6c5a45; +} +.m-notice p { + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.m-section { + margin-top: 12px; + padding: 17px 16px; +} +.m-section-title { + display: flex; + justify-content: space-between; + align-items: center; +} +.m-section-title h2, +.m-rule-box h2 { + margin: 0; + font-size: 19px; +} +.m-section-title span { + color: var(--m-accent); + font-size: 14px; +} +.m-summary-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + margin-top: 16px; +} +.m-summary-grid > div { + min-width: 0; + padding: 4px; + text-align: center; + border-right: 1px solid var(--m-line); +} +.m-summary-grid > div:last-child { + border-right: 0; +} +.m-summary-grid strong, +.m-summary-grid span { + display: block; +} +.m-summary-grid strong { + color: #70430f; + font-size: 17px; + overflow: hidden; + text-overflow: ellipsis; +} +.m-summary-grid span { + margin-top: 6px; + color: var(--m-muted); + font-size: 13px; +} +.m-company-links { + display: grid; + grid-template-columns: repeat(4, 1fr); + margin-top: 20px; +} +.m-company-links a { + min-width: 0; + color: #555; + text-decoration: none; + display: flex; + flex-direction: column; + align-items: center; + gap: 7px; + text-align: center; + font-size: 14px; +} +.m-company-links img { + width: 24px; + height: 28px; + object-fit: contain; +} + +.m-claim { + background: #fffaf2; +} +.m-claim-level { + padding: 20px; + background: #272522; + color: #fff; +} +.m-claim-level span { + color: #d8cbb9; + font-size: 14px; +} +.m-claim-level strong { + display: block; + font-size: 26px; + margin-top: 5px; +} +.m-claim-level p { + margin: 9px 0 0; + color: #d8cbb9; +} +.m-claim-orbit { + height: 330px; + display: grid; + place-items: center; + position: relative; +} +.m-claim-orbit::before, +.m-claim-orbit::after { + content: none; +} +.m-claim-orbit::before { + width: 260px; + height: 260px; +} +.m-claim-orbit::after { + width: 300px; + height: 190px; + transform: rotate(25deg); +} +.m-claim-disc { + position: relative; + z-index: 1; + width: 190px; + min-height: 220px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #fff; +} +.m-claim-orbit-image { + position: absolute; + width: 260px; + height: 252px; + object-fit: contain; +} +.m-claim-gift { + width: 116px; + height: 116px; + object-fit: contain; +} +.m-claim-disc button { + width: 190px; + min-height: 44px; + border: 0; + background: transparent; + position: relative; + padding: 0; + color: #8b3e00; + font-weight: 700; +} +.m-claim-disc button img { + width: 190px; + height: 42px; + object-fit: contain; +} +.m-claim-disc button span { + position: absolute; + inset: 0; + display: grid; + place-items: center; + border-radius: 22px; + background: rgba(255, 238, 150, 0.94); +} +.m-claim > .m-summary-grid { + padding: 18px 4px; + margin-top: 0; +} +.m-rule-box { + margin-top: 14px; + padding: 20px; +} +.m-rule-box p { + margin: 12px 0 0; + color: #747474; + line-height: 1.7; +} +.m-eligibility { + min-height: 46px; + margin-top: 12px; + border-radius: 8px; + padding: 10px 14px; + display: flex; + align-items: center; + gap: 9px; + background: #fff0ec; + color: #b75636; +} +.m-eligibility.allowed { + background: #e9f7ee; + color: var(--m-success); +} +.m-eligibility .n-icon { + font-size: 22px; +} +.m-level-list { + margin-top: 14px; + padding: 18px; +} +.m-level-list h2 { + margin: 0 0 8px; + font-size: 18px; +} +.m-level-list > div { + min-height: 48px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + border-top: 1px solid var(--m-line); +} +.m-level-list span { + color: var(--m-muted); + font-size: 13px; + text-align: right; +} + +.m-profile { + min-height: 100dvh; + padding: 0 20px 28px; + background-color: #f8f8f7; + background-size: 100% auto; + background-repeat: no-repeat; +} +.m-profile-head { + padding: 28px 14px 24px; + display: flex; + align-items: center; + gap: 14px; +} +.m-avatar { + width: 76px; + height: 76px; + flex: 0 0 auto; + border-radius: 50%; + overflow: hidden; + border: 4px solid #fff; + background: #f0f0f0; + display: grid; + place-items: center; +} +.m-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-avatar .n-icon { + font-size: 76px; + color: #98a1ab; +} +.m-profile-head h2 { + margin: 0; + font-size: 24px; +} +.m-profile-head button { + border: 0; + border-radius: 16px; + background: #fff0d4; + padding: 5px 10px; + margin-top: 7px; + color: #6c401b; +} +.m-profile-head button img { + width: 12px; + height: 12px; + margin-left: 4px; + vertical-align: -2px; +} +.m-wallet-card { + padding: 18px; + min-height: 140px; + border-radius: 8px; + background-color: #ffd46f; + background-size: 100% 100%; + background-repeat: no-repeat; + color: #502b13; +} +.m-wallet-card span, +.m-wallet-card strong { + display: block; +} +.m-wallet-card strong { + font-size: 31px; + margin: 6px 0 15px; +} +.m-wallet-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} +.m-wallet-actions a { + height: 42px; + border-radius: 22px; + display: grid; + place-items: center; + text-decoration: none; + color: #442c1b; + background: rgba(255, 255, 255, 0.78); + font-weight: 600; +} +.m-wallet-actions a:first-child { + background: #f7bd4e; +} +.m-menu { + margin-top: 18px; + padding: 8px 18px; +} +.m-menu a, +.m-menu button { + width: 100%; + min-height: 58px; + border: 0; + border-bottom: 1px solid #f1f1f1; + background: transparent; + color: var(--m-ink); + text-decoration: none; + display: grid; + grid-template-columns: 30px 1fr 24px; + align-items: center; + text-align: left; +} +.m-menu a:last-child, +.m-menu button:last-child { + border-bottom: 0; +} +.m-menu > * > img:first-child { + width: 23px; + height: 23px; + object-fit: contain; +} +.m-menu-arrow { + color: #90969e; +} +.m-confirm { + width: min(84vw, 490px); + background: #fff; + border-radius: 8px; + text-align: center; + overflow: hidden; +} +.m-confirm h3 { + margin: 24px 0 5px; +} +.m-confirm p { + margin: 0 0 20px; +} +.m-confirm > div { + display: grid; + grid-template-columns: 1fr 1fr; + border-top: 1px solid var(--m-line); +} +.m-confirm button { + min-height: 54px; + border: 0; + background: #fff; +} +.m-confirm button + button { + border-left: 1px solid var(--m-line); +} +.m-confirm .danger { + color: #ef3f3f; +} + +.m-orders { + background: #fff; +} +.m-asset-banner { + min-height: 124px; + border-radius: 8px; + padding: 18px 22px; + background: radial-gradient(circle at 77% 18%, #fff7cf, transparent 44%), + linear-gradient(135deg, #ffe8a9, #ffc74e); + color: #5b2f13; + display: flex; + justify-content: space-between; + align-items: center; +} +.m-asset-banner span, +.m-asset-banner strong, +.m-asset-banner small { + display: block; +} +.m-asset-banner strong { + font-size: 29px; + margin: 5px 0; +} +.m-asset-banner small { + font-size: 13px; +} +.m-yuan { + width: 72px; + height: 72px; + border-radius: 50%; + background: #f6a919; + border: 6px double #fff0a8; + display: grid; + place-items: center; + font-size: 34px; + color: #fff; + box-shadow: 0 8px 20px rgba(205, 128, 17, 0.25); +} +.m-tabs { + height: 52px; + border-radius: 26px; + background: #fafafa; + display: flex; + overflow-x: auto; + scrollbar-width: none; + margin: 15px 0; +} +.m-tabs::-webkit-scrollbar { + display: none; +} +.m-tabs button { + min-width: 82px; + flex: 1 0 auto; + border: 0; + border-radius: 26px; + background: transparent; + color: var(--m-muted); +} +.m-tabs button.active { + background: #d9932f; + color: #fff; +} +.m-order-list { + display: grid; + gap: 12px; +} +.m-order-card { + width: 100%; + border: 0; + background: #fff; + border-radius: 8px; + padding: 14px; + text-align: left; + box-shadow: 0 4px 18px rgba(73, 54, 31, 0.07); + color: var(--m-ink); +} +.m-order-meta { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + color: #6f7074; +} +.m-order-meta span { + border-radius: 20px; + padding: 6px 13px; + background: #fff1d9; + color: #815522; +} +.m-order-meta .status-completed { + background: #e5f6ea; + color: var(--m-success); +} +.m-order-main { + display: grid; + grid-template-columns: 96px 1fr; + gap: 13px; + align-items: center; +} +.m-product-image { + width: 96px; + aspect-ratio: 1; + border-radius: 5px; + overflow: hidden; + background: #f2f2f2; + display: grid; + place-items: center; +} +.m-product-image img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-product-image .n-icon { + font-size: 36px; + color: #bbb; +} +.m-order-main h2 { + margin: 0; + font-size: 17px; + line-height: 1.4; +} +.m-order-main p { + margin: 5px 0; + color: #754400; + font-size: 17px; +} +.m-order-main strong { + color: #ce8d3d; + font-weight: 500; +} +.m-detail { + display: grid; + gap: 12px; +} +.m-detail-fields { + padding: 17px; +} +.m-detail-fields div { + min-height: 49px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; +} +.m-detail-fields b { + font-weight: 400; + color: var(--m-muted); + text-align: right; + overflow-wrap: anywhere; +} +.m-btn-link { + display: grid; + place-items: center; + text-decoration: none; + margin-top: 8px; +} +.m-submit-form { + padding: 18px; + border-radius: 8px; + background: #fff; + box-shadow: 0 4px 18px rgba(73, 54, 31, 0.07); +} +.m-submit-form h2 { + margin: 0 0 12px; + font-size: 18px; +} +.m-submit-form textarea { + width: 100%; + min-height: 112px; + padding: 12px; + border: 1px solid var(--m-line); + border-radius: 6px; + outline: none; + resize: vertical; +} +.m-submit-form small { + display: block; + margin: 7px 0 14px; + color: var(--m-muted); +} + +.m-product-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} +.m-product-card { + min-width: 0; + border: 0; + border-radius: 8px; + padding: 0 0 13px; + overflow: hidden; + background: #fff; + box-shadow: 0 4px 18px rgba(55, 45, 30, 0.08); + text-align: left; +} +.m-product-cover { + width: 100%; + aspect-ratio: 1.2; + background: #f2f2f2; + display: grid; + place-items: center; +} +.m-product-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-product-cover .n-icon { + font-size: 48px; + color: #bbb; +} +.m-product-card h2 { + font-size: 16px; + margin: 11px 12px 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.m-product-card p { + margin: 0 12px; + color: #e4602a; + font-weight: 700; +} +.m-product-card span { + display: block; + margin: 6px 12px 0; + color: var(--m-muted); + font-size: 13px; +} +.m-product-detail { + margin: 0 -20px; + padding-bottom: 82px; +} +.m-product-hero { + width: 100%; + aspect-ratio: 1.36; + background: #eee; + display: grid; + place-items: center; +} +.m-product-hero img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-product-hero .n-icon { + font-size: 70px; + color: #bbb; +} +.m-product-detail > section { + padding: 17px 20px; + border-bottom: 10px solid #faf7f2; +} +.m-product-detail h1 { + margin: 0; + font-size: 20px; +} +.m-price { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 11px; +} +.m-price strong { + color: #ed602a; + font-size: 25px; +} +.m-price span { + padding: 5px 8px; + border: 1px solid #ffc3b1; + border-radius: 4px; + color: #e26034; +} +.m-product-intro h2, +.m-process h2 { + font-size: 18px; + margin: 0 0 10px; +} +.m-product-intro p { + margin: 0; + line-height: 1.7; +} +.m-process > div { + display: grid; + grid-template-columns: repeat(4, 1fr); +} +.m-process span { + display: flex; + flex-direction: column; + align-items: center; + gap: 7px; + text-align: center; +} +.m-process img { + width: 29px; + height: 29px; + object-fit: contain; +} +.m-buy-bar { + position: fixed; + left: 50%; + bottom: 0; + transform: translateX(-50%); + width: min(100%, 750px); + height: 72px; + padding: 10px 20px calc(10px + env(safe-area-inset-bottom)); + background: #fff; + border-top: 1px solid var(--m-line); + display: flex; + align-items: center; + justify-content: space-between; +} +.m-buy-bar b { + color: #ff6038; + font-size: 23px; +} +.m-buy-bar a { + height: 48px; + padding: 0 26px; + border-radius: 24px; + display: grid; + place-items: center; + background: linear-gradient(90deg, #ff9c2a, #f2571c); + color: #fff; + text-decoration: none; +} + +.m-support { + min-height: 100dvh; + background: #f7f7f7; +} +.m-support-hero { + height: 310px; + padding: 0 20px; + background-color: #f7c98e; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + position: relative; + overflow: hidden; +} +.m-support-hero > .m-page-header { + position: relative; + z-index: 2; +} +.m-support-hero > div:last-child { + margin-left: 42%; + position: relative; + z-index: 1; +} +.m-support-hero h1 { + margin: 25px 0 5px; + font-size: 27px; +} +.m-support-hero p { + margin: 0; + line-height: 1.6; + color: #606064; +} +.m-support-visual { + position: absolute; + left: 18px; + bottom: -18px; + width: 145px; + height: 190px; + border-radius: 70px 70px 0 0; + background: #2f343b; + display: grid; + place-items: center; + color: #ffd39d; +} +.m-support-visual .n-icon { + font-size: 84px; +} +.m-support-body { + margin-top: -18px; + position: relative; + z-index: 2; + border-radius: 28px 28px 0 0; + background: #f7f7f7; + padding: 38px 20px; +} +.m-service-card { + width: 100%; + min-height: 100px; + border: 0; + border-radius: 8px; + background: #fff; + margin-bottom: 12px; + padding: 16px; + display: grid; + grid-template-columns: 62px 1fr auto; + gap: 12px; + align-items: center; + text-align: left; +} +.m-service-avatar { + width: 62px; + height: 62px; + border-radius: 50%; + overflow: hidden; + background: #fff0d2; + display: grid; + place-items: center; + color: #71471c; +} +.m-service-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-service-avatar .n-icon { + font-size: 35px; +} +.m-service-card h2 { + margin: 0; + font-size: 20px; +} +.m-service-card p { + margin: 5px 0 0; + color: #666; +} +.m-service-card small { + display: block; + margin-top: 5px; + color: #9a9289; + font-size: 12px; + overflow-wrap: anywhere; +} +.m-service-card > span { + border-radius: 20px; + padding: 9px 13px; + background: #d9902b; + color: #fff; + white-space: nowrap; +} +.m-service-card > span.is-offline { + background: #8f9498; +} +.m-service-card > span.is-busy { + background: #c86d45; +} +.m-chat { + min-height: 100dvh; + background: #f4f5f6; + padding-bottom: 80px; +} +.m-chat .m-page-header { + padding-inline: 20px; + background: #fff; +} +.m-chat-time { + text-align: center; + color: #92969b; + padding: 8px; +} +.m-chat-empty { + min-height: 530px; + padding: 100px 30px 20px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + color: #7c838a; +} +.m-chat-empty > .n-icon { + font-size: 64px; +} +.m-chat-empty h2 { + margin: 12px 0 4px; + color: var(--m-ink); +} +.m-chat-empty p { + line-height: 1.6; +} +.m-chat-empty .m-btn-link { + width: min(280px, 100%); + margin-top: 16px; +} +.m-chat-input { + position: fixed; + left: 50%; + bottom: 0; + transform: translateX(-50%); + width: min(100%, 750px); + height: 72px; + background: #f1f1f1; + padding: 10px 15px calc(10px + env(safe-area-inset-bottom)); + display: grid; + grid-template-columns: 44px 1fr 44px; + gap: 8px; +} +.m-chat-input button { + border: 0; + background: transparent; + font-size: 27px; +} +.m-chat-input button img { + width: 24px; + height: 24px; + object-fit: contain; +} +.m-chat-input input { + border: 0; + border-radius: 8px; + background: #fff; + padding: 0 12px; +} + +.m-subpage { + background: #fff; +} +.m-subpage--profile { + background-color: #fffaf5; + background-size: 100% auto; + background-repeat: no-repeat; +} +.m-profile-edit { + text-align: center; +} +.m-avatar--large { + width: 92px; + height: 92px; + margin: 34px auto; + position: relative; + overflow: visible; +} +.m-avatar--large > img:not(.m-avatar-edit), +.m-avatar--large > .n-icon { + border-radius: 50%; +} +.m-avatar--large .m-avatar-edit { + position: absolute; + right: -2px; + bottom: 1px; + width: 18px; + height: 18px; + object-fit: contain; +} +.m-info-card { + padding: 8px 18px; + text-align: left; + box-shadow: 0 5px 24px rgba(90, 60, 25, 0.06); +} +.m-info-card > div, +.m-info-card > a { + min-height: 62px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #f0f0f0; + color: var(--m-ink); + text-decoration: none; +} +.m-info-card b { + font-weight: 400; + color: #afb1b5; +} +.m-info-card > a .n-icon { + color: #aaa; + font-size: 24px; +} +.m-plain-form { + margin: 12px -20px 0; +} +.m-plain-form > label { + min-height: 64px; + padding: 0 30px; + display: grid; + grid-template-columns: 125px 1fr; + align-items: center; + border-bottom: 1px solid var(--m-line); +} +.m-plain-form input { + width: 100%; + min-width: 0; + border: 0; + outline: 0; +} +.m-plain-form input::placeholder { + color: #c4c6ca; +} +.m-plain-form > .m-primary-btn { + width: calc(100% - 40px); + margin: 36px 20px 0; +} +.m-form-hint { + margin: 20px 30px; + color: var(--m-muted); + line-height: 1.6; +} +.m-plain-form > .m-form-error { + margin: 15px 30px; +} +.m-verify label:nth-child(2) { + grid-template-columns: 125px 1fr auto; +} +.m-verify label button { + border: 0; + border-radius: 20px; + background: linear-gradient(90deg, #ff9c2a, #f2571c); + color: #fff; + padding: 9px 14px; + white-space: nowrap; +} +.m-password-form { + max-width: 580px; + margin: 65px auto 0; + display: grid; + gap: 15px; + text-align: center; +} +.m-password-form h2 { + margin: 0; + font-size: 27px; +} +.m-password-form > p { + margin: 0 0 25px; + color: var(--m-muted); +} +.m-code-input input { + width: 100%; + height: 58px; + border: 1px solid var(--m-line); + border-radius: 8px; + text-align: center; + letter-spacing: 12px !important; + outline: 0; +} +.m-password-form .m-primary-btn { + margin-top: 20px; +} +.m-content { + padding: 16px 4px 40px; +} +.m-content h1 { + font-size: 25px; +} +.m-content-summary { + color: var(--m-muted); + line-height: 1.7; +} +.m-content-body { + white-space: pre-wrap; + line-height: 1.85; + font-size: 16px; +} +.m-content time { + display: block; + margin-top: 28px; + color: var(--m-muted); + font-size: 13px; +} + +.m-recharge-page { + background: #fffdf9; +} +.m-level-table { + overflow: hidden; + border-radius: 12px; + background: #fff; + box-shadow: 0 6px 18px rgba(64, 49, 28, 0.08); +} +.m-level-row { + min-height: 75px; + display: grid; + grid-template-columns: 1.2fr 1fr 1fr 0.72fr 0.9fr; + align-items: center; + background: #fff; +} +.m-level-row:nth-child(odd):not(.m-level-head) { + background: #fffaf3; +} +.m-level-row > * { + min-width: 0; + padding: 0 4px; + text-align: center; + white-space: nowrap; +} +.m-level-row:not(.m-level-head) { + font-size: 14px; +} +.m-level-head { + min-height: 43px; + background: #ffdc62; + font-size: 14px; +} +.m-level-empty { + min-height: 92px; + display: grid; + place-items: center; + color: var(--m-muted); +} +.m-recharge-form { + margin-top: 20px; + display: grid; + gap: 16px; +} +.m-recharge-form label { + display: grid; + gap: 8px; + font-size: 17px; +} +.m-recharge-form input, +.m-recharge-form select { + width: 100%; + height: 52px; + border: 0; + border-radius: 26px; + padding: 0 23px; + background: #fbf7f2; + outline: 0; +} +.m-recharge-form select { + appearance: auto; + color: var(--m-text); +} +.m-recharge-form input::placeholder { + color: #d9d4ce; +} +.m-recharge-form .m-primary-btn { + margin-top: 18px; +} + +.m-rules-page, +.m-agency-page { + min-height: 100dvh; + background-color: #fdfaf5; + background-position: top center; + background-repeat: no-repeat; + background-size: 100% auto; +} + +/* Platform introduction */ +.m-platform-intro { + min-height: 100dvh; + padding-bottom: 42px; + background: #f6f4ef; + color: #332d27; +} +.m-platform-intro .m-page-header { + background: #fff; +} +.m-platform-intro-head { + padding: 32px 24px 28px; + background: #fff; + border-bottom: 1px solid #e8e2d9; +} +.m-platform-intro-head > span { + color: #178c78; + font-size: 12px; + font-weight: 700; + letter-spacing: 0; +} +.m-platform-intro-head h2 { + margin: 8px 0 12px; + color: #2d2925; + font-size: 32px; + line-height: 1.2; +} +.m-platform-intro-head p { + max-width: 680px; + margin: 0; + color: #766d64; + font-size: 15px; + line-height: 1.8; +} +.m-platform-intro-head div { + display: flex; + align-items: baseline; + gap: 6px; + margin-top: 22px; + color: #8b8177; +} +.m-platform-intro-head b { + color: #e97635; + font-size: 22px; +} +.m-platform-intro-head small { + font-size: 13px; +} +.m-platform-intro-head i { + width: 1px; + height: 18px; + margin: 0 10px; + background: #ddd5ca; +} +.m-platform-intro-nav { + position: sticky; + top: 0; + z-index: 3; + display: grid; + grid-template-columns: repeat(2, 1fr); + padding: 0 20px; + background: rgba(255, 255, 255, 0.96); + border-bottom: 1px solid #e8e2d9; + backdrop-filter: blur(12px); +} +.m-platform-intro-nav button { + min-height: 48px; + display: grid; + place-items: center; + border: 0; + border-bottom: 2px solid transparent; + background: transparent; + color: #665e56; + font-size: 15px; +} +.m-platform-intro-nav button.active { + color: #e26f30; + border-bottom: 2px solid #e26f30; +} +.m-platform-intro main { + width: min(100%, 760px); + margin: 0 auto; +} +.m-platform-document { + padding: 30px 22px 4px; + scroll-margin-top: 58px; +} +.m-platform-section-title { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 22px; +} +.m-platform-section-title > span { + color: #e97635; + font-size: 30px; + font-weight: 700; +} +.m-platform-section-title div { + padding-left: 12px; + border-left: 1px solid #d9d1c7; +} +.m-platform-section-title small { + color: #178c78; + font-size: 10px; + font-weight: 700; + letter-spacing: 0; +} +.m-platform-section-title h2 { + margin: 1px 0 0; + font-size: 20px; +} +.m-platform-document article { + padding: 19px 0 17px; + border-top: 1px solid #ded8cf; +} +.m-platform-document article h3 { + margin: 0 0 11px; + color: #3b342e; + font-size: 17px; + line-height: 1.45; +} +.m-platform-document article p { + margin: 0 0 9px; + color: #6e665e; + font-size: 14px; + line-height: 1.9; + text-align: justify; + overflow-wrap: anywhere; +} +.m-platform-document article p:last-child { + margin-bottom: 0; +} +.m-platform-faq { + margin-top: 18px; + padding-bottom: 24px; + background: #fff; +} +.m-platform-faq article h3 { + color: #177d6d; +} +@media (max-width: 520px) { + .m-platform-intro-head { + padding: 26px 20px 24px; + } + .m-platform-intro-head h2 { + font-size: 28px; + } + .m-platform-document { + padding-right: 20px; + padding-left: 20px; + } +} +.m-rules-banner { + min-height: 162px; + padding: 24px 28px; + border-radius: 22px; + background-position: center; + background-repeat: no-repeat; + background-size: 100% 100%; + color: #4b2518; +} +.m-rules-banner > span { + font-size: 25px; +} +.m-rules-banner p { + display: flex; + gap: 36px; + margin: 23px 0 0; + font-size: 22px; +} +.m-rules-banner b { + white-space: nowrap; +} +.m-rules-banner strong { + color: #ff5b16; + font-size: 37px; + font-weight: 700; +} +.m-rules-table { + margin: 36px 6px 0; + overflow: hidden; + border: 1px solid #d8d8d8; + border-radius: 8px; + background: rgba(255, 255, 255, 0.78); +} +.m-rules-row { + display: grid; + grid-template-columns: 1.02fr 1.04fr repeat(4, 1fr); + min-height: 78px; + border-bottom: 1px solid #d8d8d8; +} +.m-rules-row:last-child { + border-bottom: 0; +} +.m-rules-row > * { + min-width: 0; + padding: 5px 3px; + border-right: 1px solid #d8d8d8; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 14px; + line-height: 1.25; +} +.m-rules-row > *:last-child { + border-right: 0; +} +.m-rules-head { + min-height: 62px; + background: rgba(255, 248, 237, 0.88); +} +.m-rules-head b { + font-size: 14px; + font-weight: 500; +} +.m-rule-level { + gap: 4px; + white-space: nowrap; +} +.m-rule-level img { + width: 28px; + height: 28px; + flex: 0 0 28px; + object-fit: contain; +} +.m-rules-notes { + margin: 20px 6px 0; + padding: 18px 22px 20px; + border: 1px solid #ecd9c0; + border-radius: 8px; + background: rgba(255, 255, 255, 0.72); + color: #735f55; +} +.m-rules-notes h2 { + margin: 0 0 10px; + color: #4d2a20; + font-size: 21px; + font-weight: 500; +} +.m-rules-notes h2 span { + margin-right: 4px; +} +.m-rules-notes ul { + margin: 0; + padding-left: 20px; + font-size: 17px; + line-height: 1.65; +} + +.m-agency-page { + padding: 0 18px 32px; +} +.m-agency-page .m-page-header { + height: 70px; +} +.m-agency-hero { + display: block; + width: calc(100% + 36px); + margin: -14px -18px -3px; +} +.m-agency-block { + margin-top: 10px; + padding: 18px 20px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.94); +} +.m-agency-block h2 { + display: flex; + align-items: center; + gap: 9px; + margin: 0 0 9px; + color: #303033; + font-size: 20px; + font-weight: 600; +} +.m-agency-block h2 i { + width: 29px; + height: 29px; + flex: 0 0 29px; + border-radius: 50%; + display: grid; + place-items: center; + color: #fff; + background: #ff9026; + font-size: 17px; + font-style: normal; + font-weight: 500; +} +.m-agency-block > p { + margin: 0; + color: #77716d; + font-size: 17px; + line-height: 1.52; +} +.m-agency-rewards { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} +.m-agency-rewards article { + min-width: 0; + padding: 12px 8px 10px; + border-radius: 8px; + display: grid; + grid-template-columns: 34px minmax(0, 1fr); + align-items: center; +} +.m-agency-rewards article.is-primary { + color: #ff741f; + background: #fff0e6; +} +.m-agency-rewards article.is-secondary { + color: #3487f4; + background: #eaf3ff; +} +.m-agency-rewards article.is-tertiary { + color: #39b54a; + background: #e9f8e9; +} +.m-agency-rewards img { + width: 31px; + height: 38px; + object-fit: contain; +} +.m-agency-rewards b, +.m-agency-rewards strong { + display: block; + white-space: nowrap; +} +.m-agency-rewards b { + color: #3d3430; + font-size: 12px; + font-weight: 500; +} +.m-agency-rewards strong { + margin-top: 2px; + font-size: 23px; +} +.m-agency-rewards p { + grid-column: 1 / -1; + margin: 7px 0 0; + color: #77716d; + font-size: 12px; + line-height: 1.35; +} +.m-agency-relation { + overflow: hidden; +} +.m-agent-root { + width: 112px; + margin: 6px auto 30px; + position: relative; + display: flex; + flex-direction: column; + align-items: center; +} +.m-agent-root::after { + content: ''; + width: calc(300% - 20px); + height: 25px; + border-top: 1px solid #ff8c38; + border-left: 1px solid #ff8c38; + border-right: 1px solid #ff8c38; + position: absolute; + top: 72px; + left: 50%; + transform: translateX(-50%); +} +.m-agent-root img, +.m-agent-first img { + width: 29px; + height: 35px; + object-fit: contain; +} +.m-agent-root span, +.m-agent-first span { + min-width: 80px; + margin-top: 4px; + padding: 3px 7px; + border-radius: 12px; + background: #ffe8c6; + color: #664b40; + text-align: center; + font-size: 12px; + white-space: nowrap; +} +.m-agent-tree { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 4px; +} +.m-agent-branch { + min-width: 0; +} +.m-agent-first { + display: flex; + flex-direction: column; + align-items: center; + position: relative; +} +.m-agent-first::before { + content: ''; + height: 25px; + border-left: 1px solid #ff8c38; + position: absolute; + top: -30px; +} +.m-agent-first b { + margin-top: 5px; + color: #ff641e; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} +.m-agent-children { + margin-top: 24px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + position: relative; +} +.m-agent-children::before { + content: ''; + width: 56%; + border-top: 1px solid #69a9ff; + position: absolute; + top: -13px; + left: 22%; +} +.m-agent-children > div { + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + color: #347fdc; + font-size: 11px; +} +.m-agent-children > div::before { + content: ''; + height: 13px; + border-left: 1px solid #69a9ff; + position: absolute; + top: -13px; +} +.m-agent-children img { + width: 21px; + height: 25px; + object-fit: contain; +} +.m-agent-children span, +.m-agent-children b, +.m-agent-children strong { + white-space: nowrap; +} +.m-agent-children b, +.m-agent-children strong { + font-weight: 500; +} +.m-agent-children i { + height: 22px; + margin: 3px 0; + border-left: 1px solid #65c77a; +} +.m-agent-children strong { + margin-top: 3px; + color: #29a741; +} +.m-agent-note { + margin-top: 25px !important; + font-size: 15px !important; +} + +@media (max-width: 520px) { + .m-rules-page { + padding-right: 12px; + padding-left: 12px; + } + .m-rules-banner { + min-height: 142px; + padding: 21px 20px; + } + .m-rules-banner > span { + font-size: 21px; + } + .m-rules-banner p { + gap: 22px; + margin-top: 20px; + font-size: 18px; + } + .m-rules-banner strong { + font-size: 31px; + } + .m-rules-row > *, + .m-rules-head b { + font-size: 11px; + } + .m-rules-row > span:first-child, + .m-rules-row > span:last-child { + font-size: 10px; + white-space: nowrap; + } + .m-rule-level { + gap: 1px; + } + .m-rule-level img { + width: 22px; + height: 22px; + flex-basis: 22px; + } + .m-agency-page { + padding-right: 12px; + padding-left: 12px; + } + .m-agency-hero { + width: calc(100% + 24px); + margin-right: -12px; + margin-left: -12px; + } + .m-agency-block { + padding-right: 14px; + padding-left: 14px; + } + .m-agency-rewards article { + grid-template-columns: 27px minmax(0, 1fr); + padding-right: 5px; + padding-left: 5px; + } + .m-agency-rewards img { + width: 25px; + } + .m-agent-root::after { + width: calc(270% - 10px); + } +} + +.m-withdrawal-page { + padding-top: 0; + background: linear-gradient(#ffe19e 0 269px, #fff 269px); +} +.m-withdrawal-hero { + margin: 0 -20px; + padding: 0 20px 96px; +} +.m-withdrawal-hero p { + margin: 10px 0 17px; + color: #915c20; +} +.m-withdrawal-hero > strong { + color: #5d2914; + font-size: 31px; + line-height: 1; +} +.m-withdrawal-panel { + position: relative; + z-index: 2; + margin-top: -78px; + padding: 20px 17px; + border-radius: 7px; + background: #fff; + box-shadow: 0 8px 24px rgba(74, 58, 35, 0.08); +} +.m-withdrawal-panel > label { + display: block; + font-size: 17px; +} +.m-withdrawal-amount { + height: 84px; + display: grid; + grid-template-columns: 34px 1fr auto; + align-items: end; + padding-bottom: 17px; + border-bottom: 1px solid #eee; +} +.m-withdrawal-amount b { + font-size: 33px; + line-height: 1; +} +.m-withdrawal-amount input { + width: 100%; + min-width: 0; + border: 0; + outline: 0; + font-size: 33px; + font-weight: 600; +} +.m-withdrawal-amount button { + border: 0; + background: transparent; + color: #722f17; + padding: 9px 0 0 12px; +} +.m-withdrawal-account { + width: 100%; + min-height: 66px; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 12px; + border: 0; + background: transparent; + padding: 0; + text-align: right; +} +.m-withdrawal-account > span:first-child { + text-align: left; +} +.m-withdrawal-account .n-icon { + color: #dedede; +} +.m-bank-choice { + min-width: 0; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 7px; +} +.m-bank-choice i { + width: 24px; + height: 24px; + border-radius: 50%; + display: grid; + place-items: center; + background: #0f57b6; + color: #fff; + font-size: 11px; + font-style: normal; +} +.m-bank-choice small { + color: var(--m-muted); +} +.m-withdrawal-panel .m-primary-btn { + margin-top: 18px; +} +.m-withdrawal-loading { + position: fixed; + left: 50%; + top: 50%; + z-index: 100; + width: 96px; + min-height: 96px; + transform: translate(-50%, -50%); + border-radius: 4px; + display: grid; + place-items: center; + align-content: center; + gap: 7px; + background: rgba(44, 44, 44, 0.82); + color: #fff; +} +.m-withdrawal-loading img { + width: 37px; + height: 37px; + object-fit: contain; + animation: m-orbit-spin 1s linear infinite; +} +@keyframes m-orbit-spin { + to { + transform: rotate(360deg); + } +} +.m-withdraw-confirm { + width: min(90vw, 420px); + padding: 22px; +} +.m-withdraw-confirm h2 { + margin: 0 0 12px; + text-align: center; +} +.m-withdraw-confirm > div { + min-height: 42px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--m-line); +} +.m-withdraw-confirm > div strong { + color: var(--m-accent-strong); +} +.m-withdraw-confirm label { + margin: 16px 0; + display: grid; + gap: 8px; +} +.m-withdraw-confirm input { + height: 48px; + border: 1px solid var(--m-line); + border-radius: 6px; + padding: 0 12px; + outline: 0; +} + +.m-finance-records { + padding-inline: 0; + background: #f7f7f7; +} +.m-finance-records .m-page-header { + padding-inline: 20px; + background: #fff; +} +.m-finance-tabs { + height: 66px; + display: grid; + grid-template-columns: repeat(3, 1fr); + background: #fff; +} +.m-finance-tabs button { + position: relative; + border: 0; + background: transparent; + font-size: 18px; +} +.m-finance-tabs button.active { + font-weight: 600; +} +.m-finance-tabs button.active::after { + content: ''; + position: absolute; + left: 50%; + bottom: 7px; + width: 44px; + height: 4px; + transform: translateX(-50%); + border-radius: 2px; + background: #986024; +} +.m-finance-list { + margin-top: 10px; + padding: 2px 14px 18px; + background: #fff; +} +.m-finance-list > button { + width: 100%; + min-height: 70px; + display: grid; + grid-template-columns: 48px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + border: 0; + background: transparent; + padding: 7px 0; + text-align: left; +} +.m-finance-icon { + width: 45px; + height: 45px; + border-radius: 50%; + display: grid; + place-items: center; + background: linear-gradient(145deg, #ffa840, #ff6913); +} +.m-finance-icon.is-withdrawal { + background: #d9b36f; +} +.m-finance-icon img { + width: 29px; + height: 29px; + object-fit: contain; +} +.m-finance-icon.is-withdrawal img { + filter: brightness(0) invert(1); +} +.m-finance-main, +.m-finance-result { + min-width: 0; + display: grid; + gap: 3px; +} +.m-finance-main b { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 17px; + font-weight: 500; +} +.m-finance-main time { + color: #777; + font-size: 14px; +} +.m-finance-result { + text-align: right; +} +.m-finance-result b { + font-size: 18px; + font-weight: 500; +} +.m-finance-result em { + font-style: normal; +} +.m-finance-result .is-success { + color: #1fbd77; +} +.m-finance-result .is-failed { + color: #ff681c; +} + +@media (min-width: 751px) { + body { + background: #e9eaec; + } + .m-app { + box-shadow: 0 0 40px rgba(35, 35, 35, 0.12); + } +} +@media (max-width: 360px) { + .m-auth { + padding-inline: 18px; + } + .m-page { + padding-inline: 14px; + } + .m-auth-panel { + padding-inline: 20px; + } + .m-auth-tabs { + margin-inline: -20px; + } + .m-withdrawal-hero { + margin-inline: -14px; + padding-inline: 14px; + } +} + +.m-section-title a { + color: var(--m-accent); + font-size: 14px; + text-decoration: none; +} + +/* Olons 2 task claim */ +.m-claim2 { + min-height: 100dvh; + background: #f7f7f7; +} +.m-claim2-hero { + position: relative; + width: 100%; + background: #ffe98c; +} +.m-claim2-hero > img { + display: block; + width: 100%; + height: auto; +} +.m-claim2-hero > button { + position: absolute; + left: 27%; + bottom: 13.2%; + width: 50%; + min-height: 54px; + padding: 0; + border: 0; + background: transparent; +} +.m-claim2-hero > button img { + display: block; + width: 100%; +} +.m-claim2-hero > button:disabled { + filter: grayscale(0.25); + opacity: 0.72; +} +.m-claim2-content { + padding: 20px 20px 32px; + background: #f8f8f8; + border-radius: 16px 16px 0 0; +} +.m-claim2-stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.m-claim2-stats > div { + min-height: 104px; + padding: 14px 12px; + border-radius: 8px; + background: #fff0e5; + display: grid; + grid-template-columns: 50px 1fr; + grid-template-rows: auto 1fr; + align-items: center; + column-gap: 10px; +} +.m-claim2-stats img { + grid-row: 1 / 3; + width: 50px; + height: 50px; + object-fit: contain; +} +.m-claim2-stats span { + color: #c8883c; + font-size: 16px; +} +.m-claim2-stats strong { + color: #7f4004; + font-size: 20px; + overflow: hidden; + text-overflow: ellipsis; +} +.m-claim2-warning { + margin-top: 14px; + padding: 12px 14px; + border-radius: 8px; + background: #fff0ec; + color: #b75636; + display: flex; + align-items: center; + gap: 8px; +} +.m-claim2-warning span { + min-width: 0; + flex: 1; +} +.m-claim2-warning button { + flex: 0 0 auto; + border: 0; + background: transparent; + color: #b75636; + font-weight: 600; +} +.m-claim2-rules { + margin-top: 24px; +} +.m-claim2-rules h2 { + margin: 0 0 14px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 19px; + font-weight: 600; +} +.m-claim2-rules h2 span { + width: 38px; + height: 1px; + background: #555; +} +.m-claim2-rules ol { + margin: 0; + padding-left: 22px; + color: #6e6e6e; + font-size: 15px; + line-height: 1.8; +} +.m-claim-result { + width: min(100vw, 750px); + max-height: 88dvh; + overflow: auto; + padding: 32px 30px 28px; + border-radius: 20px 20px 0 0; + background: #fff; + text-align: center; + position: fixed; + left: 50%; + bottom: 0; + transform: translateX(-50%); +} +.m-claim-result h2 { + margin: 6px 0 8px; + font-size: 24px; +} +.m-claim-result > p { + margin: 0 0 22px; + color: #9b9b9b; +} +.m-result-icon { + width: 76px; + height: 76px; + margin-top: 0; + object-fit: contain; +} +.m-result-product { + display: grid; + grid-template-columns: 104px 1fr; + gap: 10px; + padding: 8px; + border-radius: 8px; + background: #f5f5f5; + text-align: left; +} +.m-result-product > div { + width: 104px; + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; + background: #ececec; + display: grid; + place-items: center; +} +.m-result-product img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-result-product .n-icon { + color: #bbb; + font-size: 38px; +} +.m-result-product section { + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +} +.m-result-product small { + padding: 3px 7px; + border-radius: 3px; + background: #ff2843; + color: #fff; +} +.m-result-product b { + width: 100%; + margin: 7px 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.m-result-product strong, +.m-result-details dd { + color: #ff2743; +} +.m-result-details { + margin: 14px 0; + padding: 14px; + border-radius: 8px; + background: #f5f5f5; +} +.m-result-details div { + min-height: 38px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.m-result-details dt, +.m-result-details dd { + margin: 0; +} +.m-result-details dd { + max-width: 68%; + color: #999; + overflow-wrap: anywhere; +} +.m-result-reason { + margin: 18px 0; + padding: 18px; + border-radius: 8px; + background: #fff2f2; + text-align: left; +} +.m-result-reason b, +.m-result-reason span { + display: block; +} +.m-result-reason b { + margin-bottom: 6px; + color: #874500; +} +.m-result-actions { + display: flex; + gap: 12px; + margin-top: 20px; +} +.m-result-primary, +.m-result-secondary { + min-height: 48px; + flex: 1; + border: 0; + border-radius: 25px; + font-size: 17px; +} +.m-result-primary { + color: #fff; + background: linear-gradient(90deg, #ff9f2d, #f4511d); +} +.m-result-secondary { + color: #d98c2c; + background: #fff0df; +} +.m-match-visual { + width: 155px; + height: 155px; + margin: 22px auto 16px; + position: relative; + display: grid; + place-items: center; +} +.m-match-visual .orbit { + position: absolute; + width: 100%; + height: 100%; + animation: m-orbit-spin 4.5s linear infinite; +} +.m-match-visual .gift { + width: 108px; + height: 108px; + object-fit: contain; +} +@keyframes m-orbit-spin { + to { transform: rotate(360deg); } +} +.m-match-progress { + width: calc(100% - 74px); + height: 12px; + margin: 0 auto; + border-radius: 8px; + background: #ffe9d4; + overflow: hidden; +} +.m-match-progress i { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, #ffb420, #ff8518); + transition: width 0.4s ease; +} +.m-match-percent { + display: block; + margin: 8px 0 18px; + font-size: 20px; +} +.m-match-steps { + width: min(270px, 90%); + margin: 0 auto 18px; + padding: 0; + list-style: none; + text-align: left; +} +.m-match-steps li { + min-height: 38px; + color: #aaa; + display: flex; + align-items: center; + gap: 16px; +} +.m-match-steps span { + width: 24px; + height: 24px; + border: 2px solid #ccc; + border-radius: 50%; + display: grid; + place-items: center; + color: transparent; +} +.m-match-steps li.active { + color: #333; +} +.m-match-steps li.active span { + border-color: #ffb21f; + background: #ffb21f; + color: #fff; +} + +/* Olons 2 manual task mall */ +.m-task-mall { + min-height: 100dvh; + padding: calc(18px + env(safe-area-inset-top)) 20px 32px; + background-color: #fffaf4; + background-size: 100% auto; + background-repeat: no-repeat; +} +.m-mall-search { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 12px; +} +.m-mall-search h1 { + margin: 0; + font-size: 28px; +} +.m-mall-search label { + height: 48px; + min-width: 0; + padding: 0 14px; + border-radius: 25px; + background: #fff; + color: #aaa; + display: flex; + align-items: center; + gap: 8px; +} +.m-mall-search label .n-icon { + font-size: 24px; +} +.m-mall-search input { + width: 100%; + min-width: 0; + border: 0; + outline: 0; + background: transparent; +} +.m-mall-search > button, +.m-mall-claim { + border: 0; + color: #fff; + background: #ff8b16; +} +.m-mall-search > button { + min-width: 72px; + height: 48px; + border-radius: 25px; + font-size: 17px; +} +.m-mall-banner { + display: block; + width: 100%; + margin-top: 18px; + border-radius: 8px; +} +.m-mall-tabs { + min-height: 62px; + display: flex; + align-items: center; + gap: 24px; +} +.m-mall-tabs button { + min-height: 44px; + padding: 0; + border: 0; + background: transparent; + font-size: 19px; +} +.m-mall-tabs button.active { + color: #ff6d50; + font-weight: 600; +} +.m-mall-tabs > .n-icon { + margin-left: auto; + font-size: 24px; +} +.m-mall-list { + display: grid; + gap: 12px; +} +.m-mall-task { + min-height: 128px; + padding: 12px; + border-radius: 8px; + background: #fff; + display: grid; + grid-template-columns: 104px minmax(0, 1fr) auto; + gap: 12px; + align-items: center; +} +.m-mall-cover { + width: 104px; + aspect-ratio: 1; + padding: 0; + border: 0; + border-radius: 8px; + overflow: hidden; + background: #f0f0f0; + position: relative; + display: grid; + place-items: center; +} +.m-mall-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-mall-cover .n-icon { + color: #bbb; + font-size: 38px; +} +.m-mall-cover span { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 4px; + background: rgba(109, 48, 11, 0.88); + color: #fff; + font-size: 12px; +} +.m-mall-task > div { + min-width: 0; +} +.m-mall-task h2 { + display: -webkit-box; + margin: 0 0 7px; + overflow: hidden; + font-size: 17px; + line-height: 1.35; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} +.m-mall-task strong { + display: block; + color: #ff684d; + font-size: 21px; +} +.m-mall-task strong small { + font-size: 14px; +} +.m-mall-task p { + margin: 5px 0 0; + color: #b5b5b5; +} +.m-mall-claim { + min-width: 66px; + height: 38px; + border-radius: 20px; + font-size: 16px; +} + +/* Olons 2 leaderboard */ +.m-leaderboard { + min-height: 100dvh; + padding: 0 20px 36px; + background-color: #fffaf4; + background-size: 100% 100%; +} +.m-rank-podium { + min-height: 255px; + display: flex; + align-items: flex-end; + justify-content: center; + gap: 3px; + padding-top: 54px; +} +.m-rank-winner { + width: 31%; + height: 176px; + padding: 39px 8px 8px; + background-size: 100% 100%; + background-repeat: no-repeat; + position: relative; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} +.m-rank-winner.rank-1 { + width: 35%; + height: 215px; + padding-top: 46px; +} +.m-rank-avatar { + width: 68px; + height: 68px; + border-radius: 50%; + overflow: hidden; + background: linear-gradient(145deg, #ffe19e, #ff964a); + display: grid; + place-items: center; + color: #fff; + font-size: 25px; +} +.rank-1 .m-rank-avatar { + width: 80px; + height: 80px; +} +.m-rank-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-rank-winner > b { + width: 28px; + height: 28px; + margin: -12px 0 2px; + border-radius: 50%; + background: #ffad00; + color: #fff; + display: grid; + place-items: center; + position: relative; +} +.m-rank-winner > strong, +.m-rank-winner > em { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-style: normal; +} +.m-rank-winner > small { + margin-top: 4px; + padding: 4px 9px; + border-radius: 15px; + background: #fff0d4; + color: #c7812e; +} +.m-rank-table { + margin-top: 12px; + padding: 16px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.92); +} +.m-rank-table header, +.m-rank-row { + display: grid; + grid-template-columns: 48px minmax(0, 1.5fr) minmax(82px, 1fr) 70px; + align-items: center; + gap: 7px; +} +.m-rank-table header { + padding-bottom: 8px; + font-size: 14px; +} +.m-rank-row { + min-height: 58px; +} +.m-rank-user { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; +} +.m-rank-user > span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.m-rank-user-avatar { + width: 38px; + height: 38px; + flex: 0 0 auto; + border-radius: 50%; + overflow: hidden; + background: #f2c17f; + display: grid; + place-items: center; +} +.m-rank-user-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-rank-user-avatar i { + color: #fff; + font-style: normal; +} +.m-rank-row strong { + font-size: 17px; + font-weight: 500; +} +.m-rank-row > em { + padding: 5px 8px; + border-radius: 16px; + background: #fff0d4; + color: #d18b37; + font-style: normal; + text-align: center; +} + +/* Home leaderboard */ +.m-section-title a { + color: #aaa; + text-decoration: none; + font-size: 14px; +} +.m-home-rank-list { + margin-top: 13px; + display: grid; + gap: 8px; +} +.m-home-rank-list a { + min-width: 0; + display: grid; + grid-template-columns: 30px 34px minmax(0, 1fr) 72px 82px; + align-items: center; + gap: 7px; + color: var(--m-ink); + text-decoration: none; +} +.m-home-rank-list > a > b { + width: 24px; + height: 24px; + border-radius: 50%; + display: grid; + place-items: center; + color: #fff; + background: #aeb1b6; + font-weight: 500; +} +.m-home-rank-list > a > b.rank-1 { + color: #e5a229; + background: transparent; + font-size: 24px; +} +.m-home-rank-list > a > b.rank-2, +.m-home-rank-list > a > b.rank-3 { + background: linear-gradient(145deg, #efbd7c, #b9712b); +} +.m-home-rank-avatar { + width: 34px; + height: 34px; + border-radius: 50%; + overflow: hidden; + background: #f4c47f; + display: grid; + place-items: center; +} +.m-home-rank-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.m-home-rank-avatar i { + color: #fff; + font-style: normal; +} +.m-home-rank-list strong, +.m-home-rank-list em { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.m-home-rank-list strong { + font-weight: 500; +} +.m-home-rank-list em { + font-style: normal; + text-align: right; + font-weight: 600; +} +.m-home-rank-list small { + padding: 6px 8px; + border-radius: 16px; + color: #95601d; + background: #fff0d4; + text-align: center; + white-space: nowrap; +} +.m-home-rank-empty { + margin: 20px 0 4px; + color: var(--m-muted); + text-align: center; +} + +/* Company qualification */ +.m-qualification { + background: #fbfaf8; +} +.m-qualification-card, +.m-qualification-brands { + position: relative; + min-height: 190px; + margin-bottom: 14px; + padding: 20px 150px 20px 20px; + border-radius: 8px; + background: #fff; + overflow: hidden; +} +.m-qualification-card header, +.m-qualification-brands header { + display: flex; + align-items: center; + gap: 9px; +} +.m-qualification-card header span, +.m-qualification-brands header span { + padding: 3px 5px; + border-radius: 4px; + color: #ff8317; + background: #fff4e7; + font-size: 16px; +} +.m-qualification-card h2, +.m-qualification-brands h2 { + margin: 0; + font-size: 20px; +} +.m-qualification-card > strong { + display: block; + margin-top: 9px; + color: #ff8317; + font-size: 18px; +} +.m-qualification-card p { + position: relative; + z-index: 1; + margin: 8px 0 0; + color: #777; + line-height: 1.6; +} +.m-qualification-card > img { + position: absolute; + right: 10px; + bottom: 12px; + width: 130px; + max-height: 130px; + object-fit: contain; +} +.m-qualification-brands { + padding: 20px; +} +.m-qualification-brands > div { + margin-top: 22px; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 28px 14px; + align-items: center; + text-align: center; +} +.m-qualification-brands b { + min-width: 0; + color: #d71820; + font-size: 18px; + overflow-wrap: anywhere; +} +.m-qualification-brands b small { + display: block; + margin-top: 2px; + color: #333; + font-size: 10px; +} +.m-qualification-brands .iqiyi { color: #08c95c; } +.m-qualification-brands .tencent { color: #1493df; } +.m-qualification-brands .midea { color: #178cc8; font-size: 22px; } +.m-qualification-brands .dly { color: #e60012; } +.m-qualification-brands .supor { color: #e9490b; } +.m-qualification-brands .ctf { color: #9f052d; } +.m-qualification-brands .xtep { color: #e5232c; } +.m-qualification-brands .anta { color: #111; font-style: italic; } + +@media (max-width: 390px) { + .m-claim2-content, + .m-task-mall, + .m-leaderboard { + padding-inline: 14px; + } + .m-mall-task { + grid-template-columns: 92px minmax(0, 1fr) auto; + gap: 9px; + padding: 10px; + } + .m-mall-cover { + width: 92px; + } + .m-mall-search { + gap: 8px; + } + .m-mall-search h1 { + font-size: 23px; + } + .m-mall-search > button { + min-width: 60px; + } + .m-claim-result { + padding-inline: 20px; + } + .m-rank-table header, + .m-rank-row { + grid-template-columns: 38px minmax(0, 1.4fr) 72px 58px; + gap: 5px; + } + .m-rank-table { + padding-inline: 8px; + } + .m-home-rank-list a { + grid-template-columns: 26px 32px minmax(0, 1fr) 54px 72px; + gap: 5px; + } + .m-home-rank-list small { + padding-inline: 5px; + } + .m-qualification-card { + padding-right: 112px; + } + .m-qualification-card > img { + width: 105px; + } +} diff --git a/web/src/mobile/views/AgencyView.vue b/web/src/mobile/views/AgencyView.vue new file mode 100644 index 0000000..83f1ffe --- /dev/null +++ b/web/src/mobile/views/AgencyView.vue @@ -0,0 +1,59 @@ + + + diff --git a/web/src/mobile/views/AuthView.vue b/web/src/mobile/views/AuthView.vue new file mode 100644 index 0000000..e53c9ed --- /dev/null +++ b/web/src/mobile/views/AuthView.vue @@ -0,0 +1,141 @@ + + + diff --git a/web/src/mobile/views/ClaimView.vue b/web/src/mobile/views/ClaimView.vue new file mode 100644 index 0000000..b28ba2e --- /dev/null +++ b/web/src/mobile/views/ClaimView.vue @@ -0,0 +1,274 @@ + + + diff --git a/web/src/mobile/views/CompanyQualificationView.vue b/web/src/mobile/views/CompanyQualificationView.vue new file mode 100644 index 0000000..2fbcf3b --- /dev/null +++ b/web/src/mobile/views/CompanyQualificationView.vue @@ -0,0 +1,69 @@ + + + diff --git a/web/src/mobile/views/FinanceRecordsView.vue b/web/src/mobile/views/FinanceRecordsView.vue new file mode 100644 index 0000000..71322bc --- /dev/null +++ b/web/src/mobile/views/FinanceRecordsView.vue @@ -0,0 +1,120 @@ + + + diff --git a/web/src/mobile/views/FormView.vue b/web/src/mobile/views/FormView.vue new file mode 100644 index 0000000..2e37b97 --- /dev/null +++ b/web/src/mobile/views/FormView.vue @@ -0,0 +1,382 @@ + + + diff --git a/web/src/mobile/views/HomeView.vue b/web/src/mobile/views/HomeView.vue new file mode 100644 index 0000000..c9fdfb3 --- /dev/null +++ b/web/src/mobile/views/HomeView.vue @@ -0,0 +1,182 @@ + + + diff --git a/web/src/mobile/views/LeaderboardView.vue b/web/src/mobile/views/LeaderboardView.vue new file mode 100644 index 0000000..232b86d --- /dev/null +++ b/web/src/mobile/views/LeaderboardView.vue @@ -0,0 +1,86 @@ + + + diff --git a/web/src/mobile/views/MallView.vue b/web/src/mobile/views/MallView.vue new file mode 100644 index 0000000..a0268d0 --- /dev/null +++ b/web/src/mobile/views/MallView.vue @@ -0,0 +1,150 @@ + + + diff --git a/web/src/mobile/views/OrdersView.vue b/web/src/mobile/views/OrdersView.vue new file mode 100644 index 0000000..2e3e717 --- /dev/null +++ b/web/src/mobile/views/OrdersView.vue @@ -0,0 +1,255 @@ + + + diff --git a/web/src/mobile/views/PlatformIntroView.vue b/web/src/mobile/views/PlatformIntroView.vue new file mode 100644 index 0000000..49a4df5 --- /dev/null +++ b/web/src/mobile/views/PlatformIntroView.vue @@ -0,0 +1,48 @@ + + + diff --git a/web/src/mobile/views/ProfileView.vue b/web/src/mobile/views/ProfileView.vue new file mode 100644 index 0000000..4889778 --- /dev/null +++ b/web/src/mobile/views/ProfileView.vue @@ -0,0 +1,103 @@ + + + diff --git a/web/src/mobile/views/RechargeView.vue b/web/src/mobile/views/RechargeView.vue new file mode 100644 index 0000000..5fe5106 --- /dev/null +++ b/web/src/mobile/views/RechargeView.vue @@ -0,0 +1,117 @@ + + + diff --git a/web/src/mobile/views/RulesView.vue b/web/src/mobile/views/RulesView.vue new file mode 100644 index 0000000..01e4c6f --- /dev/null +++ b/web/src/mobile/views/RulesView.vue @@ -0,0 +1,51 @@ + + + diff --git a/web/src/mobile/views/SupportView.vue b/web/src/mobile/views/SupportView.vue new file mode 100644 index 0000000..8e79bf7 --- /dev/null +++ b/web/src/mobile/views/SupportView.vue @@ -0,0 +1,109 @@ + + + diff --git a/web/src/mobile/views/WithdrawalView.vue b/web/src/mobile/views/WithdrawalView.vue new file mode 100644 index 0000000..fffa9a6 --- /dev/null +++ b/web/src/mobile/views/WithdrawalView.vue @@ -0,0 +1,178 @@ + + + diff --git a/web/src/mobile/views/platformIntroContent.ts b/web/src/mobile/views/platformIntroContent.ts new file mode 100644 index 0000000..8a67e9e --- /dev/null +++ b/web/src/mobile/views/platformIntroContent.ts @@ -0,0 +1,248 @@ +export interface PlatformIntroSection { + title: string; + paragraphs: string[]; +} + +export const platformIntroLead = + '本协议阐述之条款和条件适用于您使用本网站所提供的各种工具和服务(下称“服务”)。'; + +export const platformIntroSections: PlatformIntroSection[] = [ + { + title: '一、接受条款', + paragraphs: [ + '通过《涛声》或 APP 进入即表示您同意自己已经与《涛声》(简称公司)订立本协议,且您将受本协议的条款和条件(“条款”)约束。公司可随时自行全权决定更改“条款”。如您不同意相关变更,必须停止使用“服务”。经修订的“条款”一经网站公布,立即自动生效。您应在第一次登录后仔细阅读修订后的“条款”,并有权选择停止继续使用“服务”;一旦您继续使用“服务”,则表示您已接受经修订的“条款”。当您与公司发生争议时,应以最新的服务协议为准。除另行明确声明外,任何使“服务”范围扩大或功能增强的新内容均受本协议约束。除非经公司的授权高层管理人员签订书面协议,本协议不得另行作出修订。', + ], + }, + { + title: '二、谁可使用本网站', + paragraphs: [ + '“服务”仅供能够根据相关法律订立具有法律约束力的合约的个人或公司使用。因此,您的年龄必须在十八周岁或以上,才可使用本公司服务。如不符合本项条件,请勿使用“服务”。公司可随时自行全权决定拒绝向任何人士提供“服务”。“服务”不会提供给被暂时或永久中止资格的会员。', + ], + }, + { + title: '三、收费', + paragraphs: [ + '本公司承诺不会收取平台用户的任何费用,无需押金,无需保证金,并且不会以任何方式联系用户索要任何费用。本公司联系方式请认准在线客服。用户需要自行充值到平台里面,方可进行代购。', + ], + }, + { + title: '四、您的资料和提供买卖的物品', + paragraphs: [ + '“您的资料”包括您在注册、交易或列举物品过程中,在任何公开信息场合或通过任何电子邮件形式,向本公司或其他用户提供的任何资料,包括数据、文本、软件、音乐、声响、照片、图画、影像、词句或其他材料。您应对“您的资料”真实性负全部责任,而本公司仅作为您在网上发布和刊登“您的资料”的被动渠道。', + '但是,倘若本公司认为您的信息可能使本公司承担任何法律或道义上的责任,或可能使本公司(全部或部分地)失去本公司的互联网服务供应商或其他供应商的服务,或您未在公司规定的期限内登录或再次登录网站,则本公司可自行全权决定对您的信息采取本公司认为必要或适当的任何行动,包括但不限于删除该类资料。您特此保证,您对提交的资料拥有全部权利,包括全部版权。您确认,网站没有责任去认定或决定您提交给本公司的信息哪些是应当受到保护的;对享有“服务”的其他用户使用您的信息,本公司也不必负责。', + ], + }, + { + title: '五、注册义务', + paragraphs: [ + '如您在公司网站注册,您必须同意:', + '(一)根据公司网址所刊载的会员信息表格的要求,提供关于您或贵公司的真实、准确、完整和反映当前情况的信息;', + '(二)维持并及时更新会员资料,使其保持真实、准确、完整和反映当前情况。倘若您提供任何不真实、不准确、不完整或不能反映当前情况的信息,或公司有合理理由怀疑该类资料不真实、不准确、不完整或不能反映当前情况的,公司有权暂停或终止您的注册身份及信息,并拒绝您在目前或将来对“服务”(或其任何部分)任何形式的使用。如您代表一家公司或其他法律主体在本公司登记,则您必须声明和保证,您有权使该公司或其他法律主体受本协议“条款”约束。', + ], + }, + { + title: '六、会员注册名、密码和保密', + paragraphs: [ + '登记过程中,您将选择会员注册名和密码。您须自行负责对您的会员注册名和密码保密,且须对您在会员注册名和密码下发生的所有活动承担责任。您同意:', + '(一)如发现任何人未经授权使用您的会员注册名或密码,或发生违反保密规定的任何其他情况,您会立即通知公司;', + '(二)确保您在每个上网时段结束时,以正确步骤离开网站。网站不能也不会对因您未能遵守本款规定而发生的任何损失或损毁负责。', + ], + }, + { + title: '七、关于您的信息的规则', + paragraphs: [ + '您同意,您的个人信息和您提供在公司网站上交易的任何“资料”(泛指一切可供依法交易的、有形的或无形的、以各种形态存在的某种具体物品、权利、利益、票据、证券、服务或行为。本协议中“物品”一词均含此义):', + '(一)不会有欺诈成分,与售卖伪造或盗窃无涉;', + '(二)不会侵犯任何第三者对该物品享有的物权,或版权、专利、商标、商业秘密或其他知识产权、隐私权、名誉权;', + '(三)不会违反任何法律、法规、条例或规章,包括但不限于关于规范出口管理、贸易配额、保护消费者、不正当竞争或虚假广告的法律、法规、条例或规章;', + '(四)不会含有诽谤(包括商业诽谤)、非法恐吓或非法骚扰的内容;', + '(五)不会含有淫秽或任何儿童色情内容;', + '(六)不会含有蓄意毁坏、恶意干扰、秘密截取或侵占任何系统、数据或个人资料的病毒、伪装破坏程序、电脑蠕虫、定时程序炸弹或其他电脑程序;', + '(七)不会直接或间接与下述各项货物或服务连接,或包含对下述各项货物或服务的描述:1. 本协议项下禁止的货物或服务;2. 您无权连接或包含的货物或服务;', + '(八)不会含有诽谤(包括商业诽谤)、非法恐吓或非法骚扰的内容;', + '(九)不会在与任何连锁信件、大量胡乱邮寄的电子邮件、滥发电子邮件或任何复制或多余的信息有关的方面使用“服务”;', + '(十)不会未经其他人士同意,利用“服务”收集其他人士的电子邮件地址及其他信息;', + '(十一)不会利用“服务”制作虚假的电子邮件地址,或以其他形式试图在发送人的身份或信息来源方面误导其他人士。', + ], + }, + { + title: '八、被禁止物品', + paragraphs: [ + '您不得在本公司网站公布或通过本公司网站买卖:', + '(一)可能使本公司违反任何相关法律、法规、条例或规章的任何物品;', + '(二)公司认为应禁止或不适合通过本网站买卖的任何物品。', + ], + }, + { + title: '九、您授予本公司的许可使用权', + paragraphs: [ + '您授予本公司独家的、全球通用的、永久的、免费的许可使用权利(并有权在多个层面对该权利进行再授权),使本公司有权全部或部分地使用、复制、修订、改写、发布、翻译、分发、执行和展示“您的资料”或制作其派生作品,和/或以现在已知或日后开发的任何形式、媒体或技术,将“您的资料”纳入其他作品内。', + ], + }, + { + title: '十、隐私', + paragraphs: [ + '尽管有第九条所规定的许可使用权,我们将仅根据本公司的隐私声明使用您提供的信息。本公司隐私声明的全部条款属于本协议的一部分,因此,您必须仔细阅读。请注意,您一旦自愿地在公司网站披露您的信息,该类资料即可能被其他人士获取和使用。', + ], + }, + { + title: '十一、终止或访问限制', + paragraphs: [ + '您同意,在公司未向您收费的情况下,本公司可自行全权决定以任何理由,包括但不限于公司认为您已违反本协议的字面意义和精神,或您以不符合本协议字面意义和精神的方式行事,或您在超过 90 天的时间内未以您的账号及密码登录网站,终止您的“服务”密码、账户(或其任何部分)或您对“服务”的使用,并删除和丢弃您在使用“服务”中提交的信息。', + '您同意,在公司向您收费的情况下,公司应基于合理的怀疑且经电子邮件通知的情况下实施上述终止服务的行为。公司同时可自行全权决定,在发出通知或不发出通知的情况下,随时停止提供“服务”或其任何部分。您同意,根据本协议的任何规定终止您使用“服务”之措施可在不发出事先通知的情况下实施,并承认和同意,公司可立即使您的账户无效,或撤销您的账户以及在您的账户内的所有相关资料和档案,和/或禁止您进一步接入该等档案或“服务”。', + '账号终止后,公司没有义务为您保留原账号中或与之相关的任何信息,或转发任何未曾阅读或发送的信息给您或第三方。此外,您同意,公司不会就终止您接入“服务”而对您或任何第三者承担任何责任。第 12、13、14 和 22 条应在本协议终止后继续有效。', + ], + }, + { + title: '十二、违反规则会有什么后果?', + paragraphs: [ + '在不限制其他补救措施的前提下,发生下述任一情况,本公司可立即发出警告,暂时终止、永久终止或终止您的会员资格,删除您的任何现有产品信息,以及您在网站上展示的任何其他资料:', + '(一)您违反本协议;', + '(二)本公司无法核实或鉴定您向本公司提供的任何资料;', + '(三)本公司相信您的行为可能会使您、本公司用户或通过本公司或本公司网站提供服务的第三者服务供应商发生任何法律责任。在不限制任何其他补救措施的前提下,倘若发现您从事涉及本公司网站的诈骗活动,公司可暂停或终止您的账户。', + ], + }, + { + title: '十三、服务“按现状”提供', + paragraphs: [ + '本公司会尽一切努力使您在使用公司网站的过程中得到良好体验。遗憾的是,本公司不能随时预见到任何技术上的问题或其他困难。该等困难可能会导致数据损失或其他服务中断。为此,您明确理解和同意,您使用“服务”的风险由您自行承担。“服务”以“按现状”和“按可得到”的基础提供。公司明确声明不作出任何种类的所有明示或暗示的保证,包括但不限于关于适销性、适用于某一特定用途和无侵权行为等方面的保证。', + '公司对下述内容不作保证:(一)“服务”会符合您的要求;(二)“服务”不会中断,且适时、安全和不带任何错误;(三)通过使用“服务”而可能获取的结果将是准确或可信赖的;(四)您通过“服务”而购买或获取的任何产品、服务、资料或其他材料的质量将符合您的预期。', + '通过使用“服务”而下载或以其他形式获取任何材料是由您自行全权决定进行的,且与此有关的风险由您自行承担。对于因您下载任何该等材料而发生的电脑系统损毁或数据损失,您将自行承担责任。您从公司或通过“服务”获取的任何口头或书面意见或资料,均不产生未在本协议内明确载明的任何保证。', + ], + }, + { + title: '十四、责任范围', + paragraphs: [ + '您明确理解和同意,公司不对因下述任一情况而发生的任何损害赔偿承担责任,包括但不限于利润、商誉、使用、数据等方面的损失或其他无形损失的损害赔偿(无论公司是否已被告知该等损害赔偿的可能性):', + '(一)使用或未能使用“服务”;', + '(二)因通过或从“服务”购买或获取任何货物、样品、数据、资料或服务,或通过或从“服务”接收任何信息或缔结任何交易所产生的获取替代货物和服务的费用;', + '(三)未经批准接入或更改您的传输资料或数据;', + '(四)任何第三者对“服务”的声明或关于“服务”的行为;', + '(五)因任何原因而引起的与“服务”有关的其他事宜,包括疏忽。', + ], + }, + { + title: '十五、订单规则', + paragraphs: [ + '(一)订单有多个类型,每一类型针对人群都有所不同。', + '(二)普通订单为常规订单,普通订单偶尔有超出会员账户本金余额。平台最多 2 个订单统一结算,客户接取此类订单时需要注意规则。以上所有订单类型适用于个人与团队。(注:团队任务需要团队成员所有人完成任务,否则视为未完成任务。)', + '(三)违规者自行承担后果,包括但不限于违约金 50%、风险金等。', + '(四)促销联单和福利订单是统一完成后返款。', + '(五)福利单需要在规定的三个小时内完成。', + '(六)出现违规操作、超时等,可能导致信用分扣除。信用分需要在 25 小时之内补齐,如超过 25 小时没有补齐,会导致用户信用分再次下降。', + ], + }, + { + title: '十六、赔偿', + paragraphs: [ + '您同意,因您违反本协议或经在此提及而纳入本协议的其他文件,或因您违反法律或侵害第三方权利,而使第三方对本公司及其子公司、分公司、董事、职员、代理人提出索赔要求(包括司法费用和其他专业人士的费用),您必须赔偿本公司及其子公司、分公司、董事、职员、代理人,使其免遭损失。', + ], + }, + { + title: '十七、遵守法律', + paragraphs: ['您应遵守与您使用“服务条款”以及与您提供商贸信息有关的所有相关法律、法规、条例和规章。'], + }, + { + title: '十八、代理关系', + paragraphs: [ + '您与本公司仅为独立订约人关系。本协议无意结成或创设任何代理、合伙、合营、雇用与被雇用或特许权授予与被授予关系。', + ], + }, + { + title: '十九、广告和金融服务', + paragraphs: [ + '您与在“服务”上或通过“服务”物色的刊登广告人士通讯、进行业务往来或参与其推广活动,包括就相关货物或服务付款和交付,以及与该等业务往来相关的任何其他条款、条件、保证或声明,仅限于在您和该刊登广告人士之间发生。您同意,对于因任何该等业务往来或因在“服务”上出现该等刊登广告人士而发生的任何种类的损失或损毁,本公司无需负责或承担任何责任。', + '您如打算通过“服务”创设或参与与任何公司、股票行情、投资或证券有关的任何服务,或通过“服务”收取或要求与任何公司、股票行情、投资或证券有关的任何新闻信息、警戒性信息或其他资料,敬请注意,本公司不会就通过“服务”传送的任何该等资料的准确性、有用性、可用性或可获利性负责或承担任何责任,且不会对根据该等资料而作出的任何交易或投资决策负责或承担任何责任。', + ], + }, + { + title: '二十、链接', + paragraphs: [ + '“服务”或第三者均可提供与其他万维网网站或资源的链接。由于本网站并不控制该等网站和资源,您承认并同意,本公司并不对该等外在网站或资源的可用性负责,且不认可该等网站或资源上或可从该等网站或资源获取的任何内容、宣传、产品、服务或其他材料,也不对其负责或承担任何责任。', + '您进一步承认和同意,对于任何因使用或信赖从此类网站或资源上获取的内容、宣传、产品、服务或其他材料而造成或声称造成的任何直接或间接损失,本公司均不承担责任。', + ], + }, + { + title: '二十一、其他规定', + paragraphs: [ + '本协议构成您和公司之间的全部协议,规定了您对“服务”的使用,并取代您和公司先前订立的任何书面或口头协议。本协议各方面应受中华人民共和国大陆地区法律的管辖。倘若本协议任何规定被裁定为无效或不可强制执行,该项规定应被撤销,而其余规定应予执行。条款标题仅为方便参阅而设,并不以任何方式界定、限制、解释或描述该条款的范围或限度。本公司未就您或其他人士的某项违约行为采取行动,并不表明本公司撤回就任何继后或类似的违约事件采取行动的权利。', + ], + }, + { + title: '二十二、通知', + paragraphs: [ + '除非另有明确规定,任何通知应以电子邮件形式发送。就本公司而言,电子邮件地址为 dovekf@acca.com.cn;就您而言,发送到您在登记过程中向本公司提供的电子邮件地址,或有关方指明的其他地址。在电子邮件发出二十四(24)小时后,通知应被视为已送达,除非发送人被告知相关电子邮件地址已作废。', + '或者,本公司可通过邮资预付挂号邮件并要求回执的方式,将通知发到您在登记过程中向本公司提供的地址。在该情况下,在付邮当日三(3)天后通知被视为已送达。', + ], + }, + { + title: '二十三、不可抗力', + paragraphs: [ + '对于因本公司合理控制范围以外的原因,包括但不限于自然灾害、罢工或骚乱、物质短缺或定量配给、暴动、战争行为、政府行为、通讯或其他设施故障或严重伤亡事故等,致使本公司延迟或未能履约的,本公司不对您承担任何责任。', + ], + }, + { + title: '二十四、转让', + paragraphs: ['本公司转让本协议无需经您同意。'], + }, + { + title: '二十五、其他规定', + paragraphs: [ + '本协议构成您和本公司之间的全部协议,规定了您对“服务”的使用,并取代您和本公司先前订立的任何书面或口头协议。本协议各方面应受中华人民共和国大陆地区法律的管辖。倘若本协议任何规定被裁定为无效或不可强制执行,该项规定应被撤销,而其余规定应予执行。条款标题仅为方便参阅而设,并不以任何方式界定、限制、解释或描述该条款的范围或限度。本公司未就您或其他人士的某项违约行为采取行动,并不表明本公司撤回就任何继后或类似的违约事件采取行动的权利。', + ], + }, + { + title: '二十六、平台提现要求', + paragraphs: [ + '平台提现硬性要求信用分值为 100 分。', + '温馨提示:同一个人银行卡信息请勿重复注册抢单,经发现将会冻结账号 ID。', + '个人多账户代购会导致商家封店,影响商家信誉度以及销量单作废。本平台禁止一卡绑定多账户;一卡绑定多账户将给予资金冻结 90 天、账号永久封号处理。本平台为防止恶意洗钱或套现等不正当行为,兼职会员账户完成账户金额相对应的流水即可全额提款。', + ], + }, +]; + +export const platformFAQs: PlatformIntroSection[] = [ + { + title: '1. 关于充值', + paragraphs: [ + '充值在“我的”—“我的钱包”界面进行。点击“充值”按钮,然后填写转账人的姓名和需转账的金额,点击去转账进行汇款充值。按照平台提供的账户汇款后,务必提交转账到账成功截图,然后点击“提交”即可。', + '为保证充值快速到账,烦请您所填转账人姓名和汇款金额一定要和汇款时一致。如在充值过程中遇到无法解决的问题,请及时联系人工客服。由于充值信息量过大,充值前请务必仔细核对本平台的账户卡号。本平台不定期更换账号,每一次充值都需要跟客服获取商家卡号;充错卡号不予在平台上充值,后果自行负责。有不明白的问题可以咨询在线客服。', + ], + }, + { + title: '2. 关于提现', + paragraphs: [ + '在提现之前,烦请您先在 APP 绑定提现信息。在平台工作时间内,可在“我的”—“我的钱包”界面进行提现。点击“提现”按钮,输入您想要提现的金额和提现密码即可进行提现。21 点之前提现都是 0—5 分钟到账,繁忙高峰期 30 分钟内到账;21 点以后提现 T+1 到账。', + ], + }, + { + title: '3. 关于领取', + paragraphs: [ + '在 APP 内完善好个人真实资料和真实收货地址,因为系统审核必须填写。客户需绑定自己可以接受短信、开通手机银行的银行卡,方便查收款项和对商家卡号进行转账。已经充值到账后,即可进行代购。在代购成功后,系统会根据客户会员等级的不同分配不同的拍单时间和金额,具体参考会员等级。正常会员系统会在 15—20 分钟内派单。', + ], + }, + { + title: '4. 关于订单冻结', + paragraphs: [ + '平台根据客户的账户金额和云数据派单,偶尔订单会出现大过账户本金的情况。客户只需补全账户负数,系统即可第一时间结算。订单未完成将被冻结,30 天自动解冻并扣除 30% 的违约金;继续完成未完成的商品或者订单,返本金到您绑定的账户银行卡内或平台账户内。未能理解的可以咨询在线客服。', + ], + }, + { + title: '5. 关于代理', + paragraphs: [ + '平台用户推荐新人即可成为平台代理,同时可以获得额外的动态奖励。直推一级用户奖励为一级用户每天所得佣金的 16%,二级用户奖励为二级用户每天所得佣金的 8%,三级用户奖励为三级用户每天所得佣金的 4%。(如您邀请的用户当日佣金是 100 元,您可以获取其佣金 16% 的奖励,即 16 元;二级为 8%。)详情可咨询在线客服。', + ], + }, + { + title: '6. 大额返款', + paragraphs: ['平台金额大于十五万时,统一返款至平台账户,方便会员自由运用资金。'], + }, + { + title: '7. 领取时间', + paragraphs: ['平台运营时间为每天 9:00—21:00,用户在平台运营时间内均可进行代购。'], + }, + { + title: '8. 订单超时', + paragraphs: ['领取订单超出一个小时未能完成,系统即可认定超时,需通过超时冲单取代。'], + }, +]; diff --git a/web/src/router/index.ts b/web/src/router/index.ts index d703b71..b8f01e5 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -112,6 +112,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('../mobile/views/AgencyView.vue'), meta: { title: '代理合作' }, }, + { + path: 'qualification', + name: 'MobileQualification', + component: () => import('../mobile/views/CompanyQualificationView.vue'), + meta: { title: '公司资质' }, + }, { path: 'profile/records', name: 'MobileFinanceRecords', diff --git a/web/src/vue-shim.d.ts b/web/src/vue-shim.d.ts new file mode 100644 index 0000000..b82632e --- /dev/null +++ b/web/src/vue-shim.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent, Record, unknown>; + export default component; +}